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>
339 lines
11 KiB
C#
339 lines
11 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using FluentAssertions;
|
|
using LittleShop.Data;
|
|
using LittleShop.DTOs;
|
|
using LittleShop.Models;
|
|
using LittleShop.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Xunit;
|
|
|
|
namespace LittleShop.Tests.Integration;
|
|
|
|
public class CatalogControllerTests : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
private readonly TestWebApplicationFactory _factory;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
|
|
public CatalogControllerTests(TestWebApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
// Add default JWT token for authenticated requests
|
|
var token = JwtTokenHelper.GenerateJwtToken();
|
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCategories_WithAuthentication_ReturnsOnlyActiveCategories()
|
|
{
|
|
// Arrange
|
|
await SeedTestData();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync("/api/catalog/categories");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var categories = JsonSerializer.Deserialize<List<CategoryDto>>(content, _jsonOptions);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
categories.Should().NotBeNull();
|
|
categories.Should().HaveCountGreaterThan(0);
|
|
categories.Should().OnlyContain(c => c.IsActive);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCategoryById_WithValidId_ReturnsCategory()
|
|
{
|
|
// Arrange
|
|
var categoryId = await SeedCategoryAndGetId("Test Category");
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/categories/{categoryId}");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var category = JsonSerializer.Deserialize<CategoryDto>(content, _jsonOptions);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
category.Should().NotBeNull();
|
|
category!.Id.Should().Be(categoryId);
|
|
category.Name.Should().Be("Test Category");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCategoryById_WithInvalidId_Returns404()
|
|
{
|
|
// Arrange
|
|
var invalidId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/categories/{invalidId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCategoryById_WithInactiveCategory_Returns404()
|
|
{
|
|
// Arrange
|
|
var categoryId = await SeedCategoryAndGetId("Inactive Category", isActive: false);
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/categories/{categoryId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProducts_WithoutCategoryFilter_ReturnsAllActiveProducts()
|
|
{
|
|
// Arrange
|
|
await SeedTestData();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync("/api/catalog/products");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var products = JsonSerializer.Deserialize<List<ProductDto>>(content, _jsonOptions);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
products.Should().NotBeNull();
|
|
products.Should().HaveCountGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProducts_WithCategoryFilter_ReturnsOnlyProductsInCategory()
|
|
{
|
|
// Arrange
|
|
var categoryId = await SeedCategoryWithProducts();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/products?categoryId={categoryId}");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var products = JsonSerializer.Deserialize<List<ProductDto>>(content, _jsonOptions);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
products.Should().NotBeNull();
|
|
products.Should().HaveCountGreaterThan(0);
|
|
products.Should().OnlyContain(p => p.CategoryId == categoryId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProductById_WithValidId_ReturnsProduct()
|
|
{
|
|
// Arrange
|
|
var productId = await SeedProductAndGetId();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/products/{productId}");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var product = JsonSerializer.Deserialize<ProductDto>(content, _jsonOptions);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
product.Should().NotBeNull();
|
|
product!.Id.Should().Be(productId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProductById_WithInvalidId_Returns404()
|
|
{
|
|
// Arrange
|
|
var invalidId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/products/{invalidId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetProductById_WithInactiveProduct_Returns404()
|
|
{
|
|
// Arrange
|
|
var productId = await SeedProductAndGetId(isActive: false);
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/catalog/products/{productId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
private async Task SeedTestData()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
|
|
|
// Clear existing data
|
|
context.Categories.RemoveRange(context.Categories);
|
|
context.Products.RemoveRange(context.Products);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Add test categories
|
|
var categories = new[]
|
|
{
|
|
new Category { Id = Guid.NewGuid(), Name = "Electronics", Description = "Electronic devices", IsActive = true },
|
|
new Category { Id = Guid.NewGuid(), Name = "Books", Description = "Books and literature", IsActive = true },
|
|
new Category { Id = Guid.NewGuid(), Name = "Archived", Description = "Archived category", IsActive = false }
|
|
};
|
|
|
|
context.Categories.AddRange(categories);
|
|
|
|
// Add test products
|
|
var products = new[]
|
|
{
|
|
new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Laptop",
|
|
Description = "High-performance laptop",
|
|
Price = 999.99m,
|
|
CategoryId = categories[0].Id,
|
|
IsActive = true,
|
|
Weight = 2.5m,
|
|
WeightUnit = Enums.ProductWeightUnit.Kilograms
|
|
},
|
|
new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Programming Book",
|
|
Description = "Learn programming",
|
|
Price = 49.99m,
|
|
CategoryId = categories[1].Id,
|
|
IsActive = true,
|
|
Weight = 500,
|
|
WeightUnit = Enums.ProductWeightUnit.Grams
|
|
}
|
|
};
|
|
|
|
context.Products.AddRange(products);
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task<Guid> SeedCategoryAndGetId(string name, bool isActive = true)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
|
|
|
var category = new Category
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = name,
|
|
Description = "Test category",
|
|
IsActive = isActive
|
|
};
|
|
|
|
context.Categories.Add(category);
|
|
await context.SaveChangesAsync();
|
|
|
|
return category.Id;
|
|
}
|
|
|
|
private async Task<Guid> SeedCategoryWithProducts()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
|
|
|
var category = new Category
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Test Category",
|
|
Description = "Category with products",
|
|
IsActive = true
|
|
};
|
|
|
|
context.Categories.Add(category);
|
|
|
|
var products = new[]
|
|
{
|
|
new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Product 1",
|
|
Description = "First product",
|
|
Price = 19.99m,
|
|
CategoryId = category.Id,
|
|
IsActive = true,
|
|
Weight = 100,
|
|
WeightUnit = Enums.ProductWeightUnit.Grams
|
|
},
|
|
new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Product 2",
|
|
Description = "Second product",
|
|
Price = 29.99m,
|
|
CategoryId = category.Id,
|
|
IsActive = true,
|
|
Weight = 200,
|
|
WeightUnit = Enums.ProductWeightUnit.Grams
|
|
}
|
|
};
|
|
|
|
context.Products.AddRange(products);
|
|
await context.SaveChangesAsync();
|
|
|
|
return category.Id;
|
|
}
|
|
|
|
private async Task<Guid> SeedProductAndGetId(bool isActive = true)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
|
|
|
var category = new Category
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Product Category",
|
|
Description = "Category for product",
|
|
IsActive = true
|
|
};
|
|
|
|
context.Categories.Add(category);
|
|
|
|
var product = new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Test Product",
|
|
Description = "Test product description",
|
|
Price = 99.99m,
|
|
CategoryId = category.Id,
|
|
IsActive = isActive,
|
|
Weight = 1.5m,
|
|
WeightUnit = Enums.ProductWeightUnit.Kilograms
|
|
};
|
|
|
|
context.Products.Add(product);
|
|
await context.SaveChangesAsync();
|
|
|
|
return product.Id;
|
|
}
|
|
} |