littleshop/LittleShop.Tests/Integration/OrdersControllerTests.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

510 lines
17 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using LittleShop.Controllers;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Enums;
using LittleShop.Models;
using LittleShop.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace LittleShop.Tests.Integration;
public class OrdersControllerTests : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly TestWebApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOptions;
public OrdersControllerTests(TestWebApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
}
[Fact]
public async Task GetAllOrders_WithAdminToken_ReturnsAllOrders()
{
// Arrange
var token = JwtTokenHelper.GenerateAdminJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
await SeedTestOrders();
// Act
var response = await _client.GetAsync("/api/orders");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var orders = JsonSerializer.Deserialize<List<OrderDto>>(content, _jsonOptions);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
orders.Should().NotBeNull();
orders.Should().HaveCountGreaterThan(0);
}
[Fact]
public async Task GetAllOrders_WithUserToken_ReturnsUnauthorized()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken(role: "User");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/api/orders");
// Assert
response.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden);
}
[Fact]
public async Task GetOrderById_WithAdminToken_ReturnsOrder()
{
// Arrange
var token = JwtTokenHelper.GenerateAdminJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var orderId = await SeedOrderAndGetId();
// Act
var response = await _client.GetAsync($"/api/orders/{orderId}");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var order = JsonSerializer.Deserialize<OrderDto>(content, _jsonOptions);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
order.Should().NotBeNull();
order!.Id.Should().Be(orderId);
}
[Fact]
public async Task GetOrdersByIdentity_WithValidToken_ReturnsOrders()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var identityReference = "test-identity-" + Guid.NewGuid();
await SeedOrdersForIdentity(identityReference);
// Act
var response = await _client.GetAsync($"/api/orders/by-identity/{identityReference}");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var orders = JsonSerializer.Deserialize<List<OrderDto>>(content, _jsonOptions);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
orders.Should().NotBeNull();
orders.Should().OnlyContain(o => o.IdentityReference == identityReference);
}
[Fact]
public async Task CreateOrder_WithValidData_ReturnsCreatedOrder()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var productIds = await SeedProductsAndGetIds();
var createOrderDto = new CreateOrderDto
{
IdentityReference = "test-identity-" + Guid.NewGuid(),
ShippingName = "John Doe",
ShippingAddress = "123 Test Street",
ShippingCity = "Test City",
ShippingPostCode = "TE5 7CD",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new CreateOrderItemDto { ProductId = productIds[0], Quantity = 2 },
new CreateOrderItemDto { ProductId = productIds[1], Quantity = 1 }
}
};
var json = JsonSerializer.Serialize(createOrderDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/orders", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
var order = JsonSerializer.Deserialize<OrderDto>(responseContent, _jsonOptions);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
order.Should().NotBeNull();
order!.IdentityReference.Should().Be(createOrderDto.IdentityReference);
order.Items.Should().HaveCount(2);
}
[Fact]
public async Task CreateOrder_WithInvalidProductId_ReturnsBadRequest()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var createOrderDto = new CreateOrderDto
{
IdentityReference = "test-identity-" + Guid.NewGuid(),
ShippingName = "John Doe",
ShippingAddress = "123 Test Street",
ShippingCity = "Test City",
ShippingPostCode = "TE5 7CD",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new CreateOrderItemDto { ProductId = Guid.NewGuid(), Quantity = 1 }
}
};
var json = JsonSerializer.Serialize(createOrderDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/orders", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task UpdateOrderStatus_WithAdminToken_UpdatesStatus()
{
// Arrange
var token = JwtTokenHelper.GenerateAdminJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var orderId = await SeedOrderAndGetId();
var updateDto = new UpdateOrderStatusDto
{
Status = OrderStatus.Shipped,
TrackingNumber = "TRACK123456"
};
var json = JsonSerializer.Serialize(updateDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PutAsync($"/api/orders/{orderId}/status", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
}
[Fact]
public async Task CreatePayment_WithValidOrder_ReturnsPaymentInfo()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var orderId = await SeedOrderAndGetId();
var createPaymentDto = new CreatePaymentDto
{
Currency = CryptoCurrency.BTC
};
var json = JsonSerializer.Serialize(createPaymentDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync($"/api/orders/{orderId}/payments", content);
// Assert - May return error if BTCPay not configured, but should authenticate
response.StatusCode.Should().NotBe(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task CancelOrder_WithValidIdentity_CancelsOrder()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var identityReference = "test-identity-" + Guid.NewGuid();
var orderId = await SeedOrderAndGetId(identityReference);
var cancelDto = new CancelOrderDto
{
IdentityReference = identityReference
};
var json = JsonSerializer.Serialize(cancelDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync($"/api/orders/{orderId}/cancel", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
}
[Fact]
public async Task CancelOrder_WithWrongIdentity_ReturnsBadRequest()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var orderId = await SeedOrderAndGetId("correct-identity");
var cancelDto = new CancelOrderDto
{
IdentityReference = "wrong-identity"
};
var json = JsonSerializer.Serialize(cancelDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync($"/api/orders/{orderId}/cancel", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task PaymentWebhook_WithValidData_ReturnsOk()
{
// Arrange
var token = JwtTokenHelper.GenerateJwtToken();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var webhookDto = new PaymentWebhookDto
{
InvoiceId = "INV123456",
Status = PaymentStatus.Confirmed,
Amount = 100.00m,
TransactionHash = "tx123456789"
};
var json = JsonSerializer.Serialize(webhookDto, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/orders/payments/webhook", content);
// Assert - Will return BadRequest if invoice not found, but should authenticate
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.BadRequest);
}
private async Task SeedTestOrders()
{
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
// Clear existing data
context.Orders.RemoveRange(context.Orders);
context.Products.RemoveRange(context.Products);
context.Categories.RemoveRange(context.Categories);
await context.SaveChangesAsync();
// Add test category and product
var category = new Category
{
Id = Guid.NewGuid(),
Name = "Test Category",
Description = "Test",
IsActive = true
};
context.Categories.Add(category);
var product = new Product
{
Id = Guid.NewGuid(),
Name = "Test Product",
Description = "Test",
Price = 99.99m,
CategoryId = category.Id,
IsActive = true,
Weight = 1,
WeightUnit = ProductWeightUnit.Kilograms
};
context.Products.Add(product);
// Add test orders
var orders = new[]
{
new Order
{
Id = Guid.NewGuid(),
IdentityReference = "test-identity-1",
Status = OrderStatus.PendingPayment,
ShippingName = "Customer 1",
ShippingAddress = "Address 1",
ShippingCity = "City 1",
ShippingPostCode = "PC1",
ShippingCountry = "Country 1",
TotalAmount = 99.99m,
CreatedAt = DateTime.UtcNow
},
new Order
{
Id = Guid.NewGuid(),
IdentityReference = "test-identity-2",
Status = OrderStatus.Processing,
ShippingName = "Customer 2",
ShippingAddress = "Address 2",
ShippingCity = "City 2",
ShippingPostCode = "PC2",
ShippingCountry = "Country 2",
TotalAmount = 199.99m,
CreatedAt = DateTime.UtcNow
}
};
context.Orders.AddRange(orders);
await context.SaveChangesAsync();
}
private async Task<Guid> SeedOrderAndGetId(string identityReference = "test-identity")
{
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
// Add category and product if not exists
var category = context.Categories.FirstOrDefault() ?? new Category
{
Id = Guid.NewGuid(),
Name = "Test Category",
Description = "Test",
IsActive = true
};
if (!context.Categories.Any())
context.Categories.Add(category);
var product = context.Products.FirstOrDefault() ?? new Product
{
Id = Guid.NewGuid(),
Name = "Test Product",
Description = "Test",
Price = 99.99m,
CategoryId = category.Id,
IsActive = true,
Weight = 1,
WeightUnit = ProductWeightUnit.Kilograms
};
if (!context.Products.Any())
context.Products.Add(product);
var order = new Order
{
Id = Guid.NewGuid(),
IdentityReference = identityReference,
Status = OrderStatus.Pending,
ShippingName = "Test Customer",
ShippingAddress = "Test Address",
ShippingCity = "Test City",
ShippingPostCode = "TE5 7CD",
ShippingCountry = "United Kingdom",
TotalAmount = 99.99m,
CreatedAt = DateTime.UtcNow
};
context.Orders.Add(order);
await context.SaveChangesAsync();
return order.Id;
}
private async Task SeedOrdersForIdentity(string identityReference)
{
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
var orders = new[]
{
new Order
{
Id = Guid.NewGuid(),
IdentityReference = identityReference,
Status = OrderStatus.PendingPayment,
ShippingName = "Customer",
ShippingAddress = "Address",
ShippingCity = "City",
ShippingPostCode = "PC",
ShippingCountry = "Country",
TotalAmount = 50.00m,
CreatedAt = DateTime.UtcNow
},
new Order
{
Id = Guid.NewGuid(),
IdentityReference = identityReference,
Status = OrderStatus.Shipped,
ShippingName = "Customer",
ShippingAddress = "Address",
ShippingCity = "City",
ShippingPostCode = "PC",
ShippingCountry = "Country",
TotalAmount = 75.00m,
CreatedAt = DateTime.UtcNow.AddDays(-5)
}
};
context.Orders.AddRange(orders);
await context.SaveChangesAsync();
}
private async Task<List<Guid>> SeedProductsAndGetIds()
{
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
var category = new Category
{
Id = Guid.NewGuid(),
Name = "Test Category",
Description = "Test",
IsActive = true
};
context.Categories.Add(category);
var products = new[]
{
new Product
{
Id = Guid.NewGuid(),
Name = "Product 1",
Description = "Test",
Price = 25.00m,
CategoryId = category.Id,
IsActive = true,
Weight = 0.5m,
WeightUnit = ProductWeightUnit.Kilograms
},
new Product
{
Id = Guid.NewGuid(),
Name = "Product 2",
Description = "Test",
Price = 35.00m,
CategoryId = category.Id,
IsActive = true,
Weight = 1.0m,
WeightUnit = ProductWeightUnit.Kilograms
}
};
context.Products.AddRange(products);
await context.SaveChangesAsync();
return products.Select(p => p.Id).ToList();
}
}