littleshop/LittleShop.Tests/Unit/ProductVariantServiceTests.cs
SysAdmin 034b8facee Implement product multi-buys and variants system
Major restructuring of product variations:
- Renamed ProductVariation to ProductMultiBuy for quantity-based pricing (e.g., "3 for £25")
- Added new ProductVariant model for string-based options (colors, flavors)
- Complete separation of multi-buy pricing from variant selection

Features implemented:
- Multi-buy deals with automatic price-per-unit calculation
- Product variants for colors/flavors/sizes with stock tracking
- TeleBot checkout supports both multi-buys and variant selection
- Shopping cart correctly calculates multi-buy bundle prices
- Order system tracks selected variants and multi-buy choices
- Real-time bot activity monitoring with SignalR
- Public bot directory page with QR codes for Telegram launch
- Admin dashboard shows multi-buy and variant metrics

Technical changes:
- Updated all DTOs, services, and controllers
- Fixed cart total calculation for multi-buy bundles
- Comprehensive test coverage for new functionality
- All existing tests passing with new features

Database changes:
- Migrated ProductVariations to ProductMultiBuys
- Added ProductVariants table
- Updated OrderItems to track variants

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 00:30:12 +01:00

312 lines
9.2 KiB
C#

using FluentAssertions;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
using LittleShop.Services;
using LittleShop.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Moq;
using Xunit;
namespace LittleShop.Tests.Unit;
public class ProductVariantServiceTests : IDisposable
{
private readonly LittleShopContext _context;
private readonly IProductService _productService;
private readonly Mock<IWebHostEnvironment> _mockEnvironment;
public ProductVariantServiceTests()
{
var options = new DbContextOptionsBuilder<LittleShopContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new LittleShopContext(options);
_mockEnvironment = new Mock<IWebHostEnvironment>();
_mockEnvironment.Setup(e => e.WebRootPath).Returns("/test/wwwroot");
_productService = new ProductService(_context, _mockEnvironment.Object);
}
[Fact]
public async Task CreateProductVariant_ShouldCreateSuccessfully()
{
// Arrange
var product = await CreateTestProduct();
var createDto = new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Red",
VariantType = "Color",
SortOrder = 1,
StockLevel = 50
};
// Act
var result = await _productService.CreateProductVariantAsync(createDto);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Red");
result.VariantType.Should().Be("Color");
result.StockLevel.Should().Be(50);
result.IsActive.Should().BeTrue();
}
[Fact]
public async Task CreateProductVariant_MultipleTypes_ShouldCreateSuccessfully()
{
// Arrange
var product = await CreateTestProduct();
// Create color variants
var redVariant = await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Red",
VariantType = "Color",
SortOrder = 1
});
var blueVariant = await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Blue",
VariantType = "Color",
SortOrder = 2
});
// Act - Get product with variants
var result = await _productService.GetProductByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.Variants.Should().HaveCount(2);
result.Variants.Should().AllSatisfy(v => v.VariantType.Should().Be("Color"));
result.Variants.Should().Contain(v => v.Name == "Red");
result.Variants.Should().Contain(v => v.Name == "Blue");
}
[Fact]
public async Task CreateProductVariant_FlavorVariants_ShouldWork()
{
// Arrange
var product = await CreateTestProduct();
// Create flavor variants
await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Vanilla",
VariantType = "Flavor",
SortOrder = 1
});
await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Chocolate",
VariantType = "Flavor",
SortOrder = 2
});
await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Strawberry",
VariantType = "Flavor",
SortOrder = 3
});
// Act
var result = await _productService.GetProductByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.Variants.Should().HaveCount(3);
result.Variants.Should().BeInAscendingOrder(v => v.SortOrder);
result.Variants.Select(v => v.Name).Should().BeEquivalentTo(
new[] { "Vanilla", "Chocolate", "Strawberry" }
);
}
[Fact]
public async Task UpdateProductVariant_ShouldUpdateSuccessfully()
{
// Arrange
var product = await CreateTestProduct();
var variant = await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Original",
VariantType = "Size",
StockLevel = 100
});
var updateDto = new UpdateProductVariantDto
{
Name = "Updated",
SortOrder = 10,
StockLevel = 50,
IsActive = false
};
// Act
var success = await _productService.UpdateProductVariantAsync(variant.Id, updateDto);
// Assert
success.Should().BeTrue();
// Get the updated variant to verify changes
var updated = await _productService.GetProductVariantByIdAsync(variant.Id);
updated.Should().NotBeNull();
updated!.Name.Should().Be("Updated");
updated.SortOrder.Should().Be(10);
updated.StockLevel.Should().Be(50);
updated.IsActive.Should().BeFalse();
}
[Fact]
public async Task DeleteProductVariant_ShouldSoftDelete()
{
// Arrange
var product = await CreateTestProduct();
var variant = await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "ToDelete",
VariantType = "Color"
});
// Act
await _productService.DeleteProductVariantAsync(variant.Id);
// Assert
var deletedVariant = await _context.ProductVariants
.IgnoreQueryFilters()
.FirstOrDefaultAsync(v => v.Id == variant.Id);
deletedVariant.Should().NotBeNull();
deletedVariant!.IsActive.Should().BeFalse();
}
[Fact]
public async Task ProductWithVariants_StockTracking_ShouldWork()
{
// Arrange
var product = await CreateTestProduct();
// Create variants with different stock levels
var variants = new[]
{
new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Small",
VariantType = "Size",
StockLevel = 10,
SortOrder = 1
},
new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Medium",
VariantType = "Size",
StockLevel = 25,
SortOrder = 2
},
new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Large",
VariantType = "Size",
StockLevel = 5,
SortOrder = 3
}
};
foreach (var variantDto in variants)
{
await _productService.CreateProductVariantAsync(variantDto);
}
// Act
var result = await _productService.GetProductByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.Variants.Should().HaveCount(3);
result.Variants.Sum(v => v.StockLevel).Should().Be(40);
result.Variants.First(v => v.Name == "Medium").StockLevel.Should().Be(25);
result.Variants.First(v => v.Name == "Large").StockLevel.Should().Be(5);
}
[Fact]
public async Task GetActiveVariants_ShouldOnlyReturnActive()
{
// Arrange
var product = await CreateTestProduct();
// Create active variant
await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Active",
VariantType = "Status"
});
// Create inactive variant
var inactiveVariant = await _productService.CreateProductVariantAsync(new CreateProductVariantDto
{
ProductId = product.Id,
Name = "Inactive",
VariantType = "Status"
});
await _productService.UpdateProductVariantAsync(inactiveVariant.Id, new UpdateProductVariantDto
{
IsActive = false
});
// Act
var result = await _productService.GetProductByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.Variants.Should().HaveCount(1);
result.Variants.First().Name.Should().Be("Active");
}
private async Task<Product> CreateTestProduct()
{
var category = new Category
{
Id = Guid.NewGuid(),
Name = "Test Category",
IsActive = true
};
_context.Categories.Add(category);
var product = new Product
{
Id = Guid.NewGuid(),
Name = "Test Product",
Price = 10.00m,
CategoryId = category.Id,
IsActive = true,
Weight = 1.0m,
WeightUnit = ProductWeightUnit.Kilograms
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return product;
}
public void Dispose()
{
_context.Dispose();
}
}