littleshop/LittleShop.Tests/Unit/ProductServiceTests.cs
sysadmin a281bb2896 Implement complete e-commerce functionality with shipping and order management
Features Added:
- Standard e-commerce properties (Price, Weight, shipping fields)
- Order management with Create/Edit views and shipping information
- ShippingRates system for weight-based shipping calculations
- Comprehensive test coverage with JWT authentication tests
- Sample data seeder with 5 orders demonstrating full workflow
- Photo upload functionality for products
- Multi-cryptocurrency payment support (BTC, XMR, USDT, etc.)

Database Changes:
- Added ShippingRates table
- Added shipping fields to Orders (Name, Address, City, PostCode, Country)
- Renamed properties to standard names (BasePrice to Price, ProductWeight to Weight)
- Added UpdatedAt timestamps to models

UI Improvements:
- Added Create/Edit views for Orders
- Added ShippingRates management UI
- Updated navigation menu with Shipping option
- Enhanced Order Details view with shipping information

Sample Data:
- 3 Categories (Electronics, Clothing, Books)
- 5 Products with various prices
- 5 Shipping rates (Royal Mail options)
- 5 Orders in different statuses (Pending to Delivered)
- 3 Crypto payments demonstrating payment flow

Security:
- All API endpoints secured with JWT authentication
- No public endpoints - client apps must authenticate
- Privacy-focused design with minimal data collection

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 17:37:24 +01:00

313 lines
9.4 KiB
C#

using FluentAssertions;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
using LittleShop.Services;
using LittleShop.Enums;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
using Xunit;
using LittleShop.Mapping;
namespace LittleShop.Tests.Unit;
public class ProductServiceTests : IDisposable
{
private readonly LittleShopContext _context;
private readonly IProductService _productService;
private readonly IMapper _mapper;
public ProductServiceTests()
{
// Set up in-memory database
var options = new DbContextOptionsBuilder<LittleShopContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new LittleShopContext(options);
// Set up AutoMapper
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
_mapper = mappingConfig.CreateMapper();
// Create service
_productService = new ProductService(_context, _mapper);
}
[Fact]
public async Task GetAllProductsAsync_ReturnsAllProducts()
{
// Arrange
var category = await CreateTestCategory();
var products = new[]
{
CreateTestProduct("Product 1", category.Id, 10.00m),
CreateTestProduct("Product 2", category.Id, 20.00m),
CreateTestProduct("Product 3", category.Id, 30.00m, isActive: false)
};
_context.Products.AddRange(products);
await _context.SaveChangesAsync();
// Act
var result = await _productService.GetAllProductsAsync();
// Assert
result.Should().HaveCount(3);
result.Should().Contain(p => p.Name == "Product 1");
result.Should().Contain(p => p.Name == "Product 2");
result.Should().Contain(p => p.Name == "Product 3");
}
[Fact]
public async Task GetProductByIdAsync_WithValidId_ReturnsProduct()
{
// Arrange
var category = await CreateTestCategory();
var productId = Guid.NewGuid();
var product = new Product
{
Id = productId,
Name = "Test Product",
Description = "Test Description",
Price = 99.99m,
CategoryId = category.Id,
IsActive = true,
Weight = 1.5m,
WeightUnit = ProductWeightUnit.Kilograms
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
// Act
var result = await _productService.GetProductByIdAsync(productId);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(productId);
result.Name.Should().Be("Test Product");
result.Price.Should().Be(99.99m);
}
[Fact]
public async Task GetProductsByCategoryAsync_ReturnsOnlyProductsInCategory()
{
// Arrange
var category1 = await CreateTestCategory("Category 1");
var category2 = await CreateTestCategory("Category 2");
var products = new[]
{
CreateTestProduct("Product 1", category1.Id, 10.00m),
CreateTestProduct("Product 2", category1.Id, 20.00m),
CreateTestProduct("Product 3", category2.Id, 30.00m)
};
_context.Products.AddRange(products);
await _context.SaveChangesAsync();
// Act
var result = await _productService.GetProductsByCategoryAsync(category1.Id);
// Assert
result.Should().HaveCount(2);
result.Should().OnlyContain(p => p.CategoryId == category1.Id);
}
[Fact]
public async Task CreateProductAsync_WithValidData_CreatesProduct()
{
// Arrange
var category = await CreateTestCategory();
var createDto = new CreateProductDto
{
Name = "New Product",
Description = "New Description",
Price = 49.99m,
CategoryId = category.Id,
Weight = 2.5m,
WeightUnit = ProductWeightUnit.Kilograms
};
// Act
var result = await _productService.CreateProductAsync(createDto);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("New Product");
result.Price.Should().Be(49.99m);
result.IsActive.Should().BeTrue();
// Verify in database
var dbProduct = await _context.Products.FindAsync(result.Id);
dbProduct.Should().NotBeNull();
dbProduct!.Name.Should().Be("New Product");
}
[Fact]
public async Task UpdateProductAsync_WithValidData_UpdatesProduct()
{
// Arrange
var category = await CreateTestCategory();
var productId = Guid.NewGuid();
var product = new Product
{
Id = productId,
Name = "Original Name",
Description = "Original Description",
Price = 10.00m,
CategoryId = category.Id,
IsActive = true,
Weight = 1.0m,
WeightUnit = ProductWeightUnit.Kilograms
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
var updateDto = new UpdateProductDto
{
Name = "Updated Name",
Description = "Updated Description",
Price = 20.00m,
CategoryId = category.Id,
Weight = 2.0m,
WeightUnit = ProductWeightUnit.Pounds,
IsActive = false
};
// Act
var result = await _productService.UpdateProductAsync(productId, updateDto);
// Assert
result.Should().BeTrue();
// Verify in database
var dbProduct = await _context.Products.FindAsync(productId);
dbProduct!.Name.Should().Be("Updated Name");
dbProduct.Price.Should().Be(20.00m);
dbProduct.Weight.Should().Be(2.0m);
dbProduct.WeightUnit.Should().Be(ProductWeightUnit.Pounds);
dbProduct.IsActive.Should().BeFalse();
}
[Fact]
public async Task DeleteProductAsync_WithValidId_DeletesProduct()
{
// Arrange
var category = await CreateTestCategory();
var productId = Guid.NewGuid();
var product = CreateTestProduct("To Delete", category.Id, 10.00m);
product.Id = productId;
_context.Products.Add(product);
await _context.SaveChangesAsync();
// Act
var result = await _productService.DeleteProductAsync(productId);
// Assert
result.Should().BeTrue();
// Verify in database
var dbProduct = await _context.Products.FindAsync(productId);
dbProduct.Should().BeNull();
}
[Fact]
public async Task AddProductPhotoAsync_AddsPhotoToProduct()
{
// Arrange
var category = await CreateTestCategory();
var productId = Guid.NewGuid();
var product = CreateTestProduct("Product", category.Id, 10.00m);
product.Id = productId;
_context.Products.Add(product);
await _context.SaveChangesAsync();
var photoDto = new CreateProductPhotoDto
{
ProductId = productId,
PhotoUrl = "/uploads/test-photo.jpg",
AltText = "Test Photo",
DisplayOrder = 1
};
// Act
var result = await _productService.AddProductPhotoAsync(photoDto);
// Assert
result.Should().NotBeNull();
result.PhotoUrl.Should().Be("/uploads/test-photo.jpg");
result.AltText.Should().Be("Test Photo");
// Verify in database
var dbProduct = await _context.Products
.Include(p => p.Photos)
.FirstOrDefaultAsync(p => p.Id == productId);
dbProduct!.Photos.Should().HaveCount(1);
}
[Fact]
public async Task GetProductsBySearchAsync_ReturnsMatchingProducts()
{
// Arrange
var category = await CreateTestCategory();
var products = new[]
{
CreateTestProduct("Laptop Computer", category.Id, 999.00m),
CreateTestProduct("Desktop Computer", category.Id, 799.00m),
CreateTestProduct("Mouse Pad", category.Id, 9.99m)
};
_context.Products.AddRange(products);
await _context.SaveChangesAsync();
// Act - Search for "Computer"
var result = await _productService.SearchProductsAsync("Computer");
// Assert
result.Should().HaveCount(2);
result.Should().Contain(p => p.Name.Contains("Computer"));
result.Should().NotContain(p => p.Name == "Mouse Pad");
}
private async Task<Category> CreateTestCategory(string name = "Test Category")
{
var category = new Category
{
Id = Guid.NewGuid(),
Name = name,
Description = "Test Description",
IsActive = true
};
_context.Categories.Add(category);
await _context.SaveChangesAsync();
return category;
}
private Product CreateTestProduct(string name, Guid categoryId, decimal price, bool isActive = true)
{
return new Product
{
Id = Guid.NewGuid(),
Name = name,
Description = $"Description for {name}",
Price = price,
CategoryId = categoryId,
IsActive = isActive,
Weight = 1.0m,
WeightUnit = ProductWeightUnit.Kilograms
};
}
public void Dispose()
{
_context.Dispose();
}
}