diff --git a/Areas/Admin/Controllers/OrdersController.cs b/Areas/Admin/Controllers/OrdersController.cs deleted file mode 100644 index 28302c7..0000000 --- a/Areas/Admin/Controllers/OrdersController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using LittleShop.Services; -using LittleShop.DTOs; - -namespace LittleShop.Areas.Admin.Controllers; - -[Area("Admin")] -[Authorize(Policy = "AdminOnly")] -public class OrdersController : Controller -{ - private readonly IOrderService _orderService; - - public OrdersController(IOrderService orderService) - { - _orderService = orderService; - } - - public async Task Index() - { - var orders = await _orderService.GetAllOrdersAsync(); - return View(orders.OrderByDescending(o => o.CreatedAt)); - } - - public async Task Details(Guid id) - { - var order = await _orderService.GetOrderByIdAsync(id); - if (order == null) - { - return NotFound(); - } - - return View(order); - } - - [HttpPost] - public async Task UpdateStatus(Guid id, UpdateOrderStatusDto model) - { - var success = await _orderService.UpdateOrderStatusAsync(id, model); - if (!success) - { - return NotFound(); - } - - return RedirectToAction(nameof(Details), new { id }); - } -} \ No newline at end of file diff --git a/LittleShop.Tests/BasicTests.cs b/LittleShop.Tests/BasicTests.cs new file mode 100644 index 0000000..7f3cefd --- /dev/null +++ b/LittleShop.Tests/BasicTests.cs @@ -0,0 +1,20 @@ +using Xunit; +using FluentAssertions; + +namespace LittleShop.Tests; + +public class BasicTests +{ + [Fact] + public void BasicTest_ShouldPass() + { + // Arrange + var expected = 4; + + // Act + var result = 2 + 2; + + // Assert + result.Should().Be(expected); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Infrastructure/JwtTokenHelper.cs b/LittleShop.Tests/Infrastructure/JwtTokenHelper.cs new file mode 100644 index 0000000..dff0fa9 --- /dev/null +++ b/LittleShop.Tests/Infrastructure/JwtTokenHelper.cs @@ -0,0 +1,65 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace LittleShop.Tests.Infrastructure; + +public static class JwtTokenHelper +{ + public static string GenerateJwtToken( + string userId = "test-user-id", + string username = "testuser", + string role = "User", + int expirationMinutes = 60, + string secretKey = "YourSuperSecretKeyThatIsAtLeast32CharactersLong!", + string issuer = "LittleShop", + string audience = "LittleShop") + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(secretKey); + + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, role) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddMinutes(expirationMinutes), + Issuer = issuer, + Audience = audience, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + + public static string GenerateExpiredJwtToken( + string userId = "test-user-id", + string username = "testuser", + string role = "User") + { + return GenerateJwtToken(userId, username, role, expirationMinutes: -60); + } + + public static string GenerateInvalidJwtToken() + { + // Generate token with wrong secret key + return GenerateJwtToken(secretKey: "WrongSecretKeyThatIsAtLeast32CharactersLong!"); + } + + public static string GenerateAdminJwtToken() + { + return GenerateJwtToken( + userId: "admin-user-id", + username: "admin", + role: "Admin"); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Infrastructure/TestWebApplicationFactory.cs b/LittleShop.Tests/Infrastructure/TestWebApplicationFactory.cs new file mode 100644 index 0000000..ed898cb --- /dev/null +++ b/LittleShop.Tests/Infrastructure/TestWebApplicationFactory.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using LittleShop.Data; +using System.Linq; + +namespace LittleShop.Tests.Infrastructure; + +public class TestWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + // Remove the existing DbContext registration + var descriptor = services.SingleOrDefault( + d => d.ServiceType == typeof(DbContextOptions)); + + if (descriptor != null) + { + services.Remove(descriptor); + } + + // Add in-memory database for testing + services.AddDbContext(options => + { + options.UseInMemoryDatabase("InMemoryDbForTesting"); + }); + + // Build service provider + var sp = services.BuildServiceProvider(); + + // Create scope for database initialization + using (var scope = sp.CreateScope()) + { + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService(); + var logger = scopedServices.GetRequiredService>(); + + // Ensure database is created + db.Database.EnsureCreated(); + + try + { + // Seed test data if needed + SeedTestData(db); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred seeding the database with test data."); + } + } + }); + + builder.UseEnvironment("Testing"); + } + + private static void SeedTestData(LittleShopContext context) + { + // Seed test data will be added as needed for specific tests + context.SaveChanges(); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Integration/CatalogControllerTests.cs b/LittleShop.Tests/Integration/CatalogControllerTests.cs new file mode 100644 index 0000000..eb1e62f --- /dev/null +++ b/LittleShop.Tests/Integration/CatalogControllerTests.cs @@ -0,0 +1,339 @@ +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 +{ + 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>(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(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>(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>(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(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(); + + // 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 SeedCategoryAndGetId(string name, bool isActive = true) + { + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + 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 SeedCategoryWithProducts() + { + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + 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 SeedProductAndGetId(bool isActive = true) + { + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + 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; + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Integration/OrdersControllerTests.cs b/LittleShop.Tests/Integration/OrdersControllerTests.cs new file mode 100644 index 0000000..087a818 --- /dev/null +++ b/LittleShop.Tests/Integration/OrdersControllerTests.cs @@ -0,0 +1,510 @@ +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 +{ + 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>(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(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>(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 + { + 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(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 + { + 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(); + + // 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 SeedOrderAndGetId(string identityReference = "test-identity") + { + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + // 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(); + + 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> SeedProductsAndGetIds() + { + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + 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(); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/LittleShop.Tests.csproj b/LittleShop.Tests/LittleShop.Tests.csproj new file mode 100644 index 0000000..1842bb8 --- /dev/null +++ b/LittleShop.Tests/LittleShop.Tests.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LittleShop.Tests/README.md b/LittleShop.Tests/README.md new file mode 100644 index 0000000..55a079d --- /dev/null +++ b/LittleShop.Tests/README.md @@ -0,0 +1,92 @@ +# LittleShop Test Suite + +## ✅ Test Infrastructure Complete + +### **Test Coverage Implemented** + +#### 🔒 **Security Testing** +- **AuthenticationEnforcementTests.cs**: Verifies ALL endpoints require JWT authentication +- **JwtTokenHelper.cs**: Helper for generating test JWT tokens (valid, expired, invalid) +- All catalog endpoints now require Bearer authentication + +#### 🔧 **API Integration Testing** +- **CatalogControllerTests.cs**: Tests category and product endpoints with authentication +- **OrdersControllerTests.cs**: Tests order lifecycle and payment endpoints +- **TestWebApplicationFactory.cs**: In-memory database for isolated testing + +#### 🎭 **UI Testing (Playwright)** +- **AdminPanelTests.cs**: + - Login/logout flows + - 404 error detection + - Network error monitoring + - Console error capture + - CRUD operation validation + +#### ⚡ **Unit Testing** +- **CategoryServiceTests.cs**: Business logic for category management +- **ProductServiceTests.cs**: Product service operations +- Test data builders for consistent test data generation + +### **Key Features** +1. **All endpoints secured** - No anonymous access to API +2. **JWT authentication** - Token generation and validation helpers +3. **In-memory database** - Fast, isolated test execution +4. **Playwright UI tests** - Catches 404s, JavaScript errors, network failures +5. **Comprehensive coverage** - Security, integration, UI, and unit tests + +### **Test Configuration** +- `appsettings.Testing.json` - Test-specific configuration +- Uses xUnit, FluentAssertions, Moq, Playwright +- ASP.NET Core Test Host for integration testing + +## ⚠️ **Note: Model Property Adjustments Needed** + +The test files reference standard e-commerce properties that need to be mapped to your actual model properties: + +### **Product Model Mapping** +- Test uses `Price` → Model has `BasePrice` +- Test uses `Weight` → Model has `ProductWeight` +- Test uses `WeightUnit` → Model has `ProductWeightUnit` + +### **Order Model** +- Tests expect shipping fields (`ShippingName`, `ShippingAddress`, etc.) +- Current model doesn't include shipping information +- Consider adding shipping fields to Order model or adjusting tests + +### **Missing Properties** +- `Category.UpdatedAt` - Not in current model +- `Product.UpdatedAt` - Not in current model +- `Order.Items` → Should use `OrderItems` + +## **Running Tests** + +```bash +# Run all tests +dotnet test + +# Run specific test category +dotnet test --filter Category=Security +dotnet test --filter FullyQualifiedName~AuthenticationEnforcementTests + +# Run with coverage +dotnet test --collect:"XPlat Code Coverage" + +# Run Playwright tests +dotnet test --filter FullyQualifiedName~AdminPanelTests +``` + +## **Test Categories** + +1. **Security Tests**: Authentication/authorization enforcement +2. **Integration Tests**: API endpoint testing with auth +3. **UI Tests**: Playwright browser automation +4. **Unit Tests**: Service layer business logic + +## **Next Steps** + +To make tests fully functional: +1. Either update model properties to match test expectations +2. Or update tests to use actual model property names +3. Add shipping fields to Order model if needed for e-commerce functionality + +The comprehensive test infrastructure is in place and ready for these adjustments! \ No newline at end of file diff --git a/LittleShop.Tests/Security/AuthenticationEnforcementTests.cs b/LittleShop.Tests/Security/AuthenticationEnforcementTests.cs new file mode 100644 index 0000000..6a6e8c9 --- /dev/null +++ b/LittleShop.Tests/Security/AuthenticationEnforcementTests.cs @@ -0,0 +1,184 @@ +using System.Net; +using System.Net.Http.Headers; +using FluentAssertions; +using LittleShop.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace LittleShop.Tests.Security; + +public class AuthenticationEnforcementTests : IClassFixture +{ + private readonly HttpClient _client; + private readonly TestWebApplicationFactory _factory; + + public AuthenticationEnforcementTests(TestWebApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false + }); + } + + [Theory] + [InlineData("/api/catalog/categories")] + [InlineData("/api/catalog/categories/00000000-0000-0000-0000-000000000001")] + [InlineData("/api/catalog/products")] + [InlineData("/api/catalog/products/00000000-0000-0000-0000-000000000001")] + public async Task CatalogEndpoints_WithoutAuthentication_ShouldReturn401(string url) + { + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Theory] + [InlineData("/api/orders")] + [InlineData("/api/orders/00000000-0000-0000-0000-000000000001")] + public async Task AdminOrderEndpoints_WithoutAuthentication_ShouldReturn401(string url) + { + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Theory] + [InlineData("/api/orders/by-identity/test-identity")] + [InlineData("/api/orders/by-identity/test-identity/00000000-0000-0000-0000-000000000001")] + [InlineData("/api/orders/00000000-0000-0000-0000-000000000001/payments")] + [InlineData("/api/orders/payments/00000000-0000-0000-0000-000000000001/status")] + public async Task PublicOrderEndpoints_WithoutAuthentication_ShouldReturn401(string url) + { + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task PostOrder_WithoutAuthentication_ShouldReturn401() + { + // Act + var response = await _client.PostAsync("/api/orders", null); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task PostPayment_WithoutAuthentication_ShouldReturn401() + { + // Act + var response = await _client.PostAsync("/api/orders/00000000-0000-0000-0000-000000000001/payments", null); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task PaymentWebhook_WithoutAuthentication_ShouldReturn401() + { + // Act + var response = await _client.PostAsync("/api/orders/payments/webhook", null); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Theory] + [InlineData("/api/catalog/categories")] + [InlineData("/api/catalog/products")] + public async Task CatalogEndpoints_WithValidJwtToken_ShouldReturn200(string url) + { + // Arrange + var token = JwtTokenHelper.GenerateJwtToken(); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Theory] + [InlineData("/api/catalog/categories")] + [InlineData("/api/catalog/products")] + public async Task CatalogEndpoints_WithExpiredJwtToken_ShouldReturn401(string url) + { + // Arrange + var token = JwtTokenHelper.GenerateExpiredJwtToken(); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Theory] + [InlineData("/api/catalog/categories")] + [InlineData("/api/catalog/products")] + public async Task CatalogEndpoints_WithInvalidJwtToken_ShouldReturn401(string url) + { + // Arrange + var token = JwtTokenHelper.GenerateInvalidJwtToken(); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Theory] + [InlineData("/api/catalog/categories")] + [InlineData("/api/catalog/products")] + public async Task CatalogEndpoints_WithMalformedToken_ShouldReturn401(string url) + { + // Arrange + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "not-a-valid-jwt-token"); + + // Act + var response = await _client.GetAsync(url); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AdminEndpoint_WithUserToken_ShouldReturnForbiddenOrUnauthorized() + { + // 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 AdminEndpoint_WithAdminToken_ShouldReturn200() + { + // Arrange + var token = JwtTokenHelper.GenerateAdminJwtToken(); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await _client.GetAsync("/api/orders"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/TestUtilities/TestDataBuilder.cs b/LittleShop.Tests/TestUtilities/TestDataBuilder.cs new file mode 100644 index 0000000..d83f724 --- /dev/null +++ b/LittleShop.Tests/TestUtilities/TestDataBuilder.cs @@ -0,0 +1,295 @@ +using LittleShop.DTOs; +using LittleShop.Enums; +using LittleShop.Models; +using System; +using System.Collections.Generic; + +namespace LittleShop.Tests.TestUtilities; + +public static class TestDataBuilder +{ + public static Category CreateCategory(string? name = null, bool isActive = true) + { + return new Category + { + Id = Guid.NewGuid(), + Name = name ?? $"Category-{Guid.NewGuid()}", + Description = "Test category description", + IsActive = isActive, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + public static Product CreateProduct(Guid? categoryId = null, string? name = null, decimal? price = null, bool isActive = true) + { + return new Product + { + Id = Guid.NewGuid(), + Name = name ?? $"Product-{Guid.NewGuid()}", + Description = "Test product description", + Price = price ?? 99.99m, + CategoryId = categoryId ?? Guid.NewGuid(), + IsActive = isActive, + Weight = 1.5m, + WeightUnit = ProductWeightUnit.Kilograms, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + public static Order CreateOrder(string? identityReference = null, OrderStatus status = OrderStatus.PendingPayment) + { + var reference = identityReference ?? $"identity-{Guid.NewGuid()}"; + return new Order + { + Id = Guid.NewGuid(), + IdentityReference = reference, + Status = status, + ShippingName = "Test Customer", + ShippingAddress = "123 Test Street", + ShippingCity = "Test City", + ShippingPostCode = "TE5 7CD", + ShippingCountry = "United Kingdom", + TotalAmount = 199.99m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Items = new List() + }; + } + + public static OrderItem CreateOrderItem(Guid orderId, Guid productId, int quantity = 1, decimal price = 99.99m) + { + return new OrderItem + { + Id = Guid.NewGuid(), + OrderId = orderId, + ProductId = productId, + Quantity = quantity, + UnitPrice = price, + TotalPrice = price * quantity + }; + } + + public static User CreateUser(string? username = null, string role = "User") + { + var user = username ?? $"user-{Guid.NewGuid()}"; + return new User + { + Id = Guid.NewGuid(), + Username = user, + Email = $"{user}@test.com", + PasswordHash = "hashed-password", + Role = role, + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + public static CryptoPayment CreateCryptoPayment(Guid orderId, CryptoCurrency currency = CryptoCurrency.BTC, PaymentStatus status = PaymentStatus.Pending) + { + return new CryptoPayment + { + Id = Guid.NewGuid(), + OrderId = orderId, + Currency = currency, + Amount = 0.0025m, + CryptoAddress = $"bc1q{Guid.NewGuid().ToString().Replace("-", "").Substring(0, 39)}", + Status = status, + ExchangeRate = 40000.00m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + public static CreateOrderDto CreateOrderDto(List? productIds = null) + { + var items = new List(); + + if (productIds != null) + { + foreach (var productId in productIds) + { + items.Add(new CreateOrderItemDto + { + ProductId = productId, + Quantity = 1 + }); + } + } + else + { + items.Add(new CreateOrderItemDto + { + ProductId = Guid.NewGuid(), + Quantity = 1 + }); + } + + return new CreateOrderDto + { + IdentityReference = $"test-identity-{Guid.NewGuid()}", + ShippingName = "Test Customer", + ShippingAddress = "123 Test Street", + ShippingCity = "Test City", + ShippingPostCode = "TE5 7CD", + ShippingCountry = "United Kingdom", + Items = items + }; + } + + public static CreateProductDto CreateProductDto(Guid? categoryId = null) + { + return new CreateProductDto + { + Name = $"Product-{Guid.NewGuid()}", + Description = "Test product description", + Price = 99.99m, + CategoryId = categoryId ?? Guid.NewGuid(), + Weight = 1.5m, + WeightUnit = ProductWeightUnit.Kilograms + }; + } + + public static CreateCategoryDto CreateCategoryDto() + { + return new CreateCategoryDto + { + Name = $"Category-{Guid.NewGuid()}", + Description = "Test category description" + }; + } + + public static ProductPhoto CreateProductPhoto(Guid productId, int displayOrder = 1) + { + return new ProductPhoto + { + Id = Guid.NewGuid(), + ProductId = productId, + PhotoUrl = $"/uploads/products/{Guid.NewGuid()}.jpg", + AltText = "Test product photo", + DisplayOrder = displayOrder, + CreatedAt = DateTime.UtcNow + }; + } + + // Builder class for complex test scenarios + public class OrderBuilder + { + private Order _order; + private List _products = new(); + + public OrderBuilder() + { + _order = CreateOrder(); + } + + public OrderBuilder WithStatus(OrderStatus status) + { + _order.Status = status; + return this; + } + + public OrderBuilder WithIdentity(string identityReference) + { + _order.IdentityReference = identityReference; + return this; + } + + public OrderBuilder WithShipping(string name, string address, string city, string postCode, string country) + { + _order.ShippingName = name; + _order.ShippingAddress = address; + _order.ShippingCity = city; + _order.ShippingPostCode = postCode; + _order.ShippingCountry = country; + return this; + } + + public OrderBuilder AddItem(Product product, int quantity) + { + _products.Add(product); + var item = CreateOrderItem(_order.Id, product.Id, quantity, product.Price); + _order.Items.Add(item); + _order.TotalAmount = _order.Items.Sum(i => i.TotalPrice); + return this; + } + + public OrderBuilder WithPayment(CryptoCurrency currency, PaymentStatus status) + { + var payment = CreateCryptoPayment(_order.Id, currency, status); + _order.Payments ??= new List(); + _order.Payments.Add(payment); + return this; + } + + public Order Build() + { + return _order; + } + + public (Order order, List products) BuildWithProducts() + { + return (_order, _products); + } + } + + // Bulk data generation for stress testing + public static class BulkDataGenerator + { + public static List GenerateCategories(int count) + { + var categories = new List(); + for (int i = 0; i < count; i++) + { + categories.Add(CreateCategory($"Category {i + 1}")); + } + return categories; + } + + public static List GenerateProducts(List categories, int productsPerCategory) + { + var products = new List(); + foreach (var category in categories) + { + for (int i = 0; i < productsPerCategory; i++) + { + products.Add(CreateProduct( + category.Id, + $"{category.Name} - Product {i + 1}", + Random.Shared.Next(10, 1000))); + } + } + return products; + } + + public static List GenerateOrders(int count, List availableProducts) + { + var orders = new List(); + var statuses = Enum.GetValues(); + + for (int i = 0; i < count; i++) + { + var builder = new OrderBuilder() + .WithIdentity($"customer-{i}") + .WithStatus(statuses[Random.Shared.Next(statuses.Length)]); + + // Add random products to order + var productCount = Random.Shared.Next(1, Math.Min(5, availableProducts.Count)); + var selectedProducts = availableProducts + .OrderBy(x => Random.Shared.Next()) + .Take(productCount); + + foreach (var product in selectedProducts) + { + builder.AddItem(product, Random.Shared.Next(1, 3)); + } + + orders.Add(builder.Build()); + } + + return orders; + } + } +} \ No newline at end of file diff --git a/LittleShop.Tests/UI/AdminPanelTests.cs b/LittleShop.Tests/UI/AdminPanelTests.cs new file mode 100644 index 0000000..cc4bf2f --- /dev/null +++ b/LittleShop.Tests/UI/AdminPanelTests.cs @@ -0,0 +1,322 @@ +using Microsoft.Playwright; +using Xunit; +using FluentAssertions; +using LittleShop.Tests.Infrastructure; + +namespace LittleShop.Tests.UI; + +[Collection("Playwright")] +public class AdminPanelTests : IClassFixture, IAsyncLifetime +{ + private readonly TestWebApplicationFactory _factory; + private IPlaywright? _playwright; + private IBrowser? _browser; + private IBrowserContext? _context; + private IPage? _page; + private readonly string _baseUrl; + + public AdminPanelTests(TestWebApplicationFactory factory) + { + _factory = factory; + _baseUrl = "https://localhost:5001"; // Adjust based on your test configuration + } + + public async Task InitializeAsync() + { + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = true // Set to false for debugging + }); + _context = await _browser.NewContextAsync(new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true // For self-signed certificates in test + }); + _page = await _context.NewPageAsync(); + } + + public async Task DisposeAsync() + { + if (_page != null) await _page.CloseAsync(); + if (_context != null) await _context.CloseAsync(); + if (_browser != null) await _browser.CloseAsync(); + _playwright?.Dispose(); + } + + [Fact] + public async Task LoginPage_ShouldLoadWithoutErrors() + { + // Arrange & Act + var response = await _page!.GotoAsync($"{_baseUrl}/Admin/Account/Login"); + + // Assert + response.Should().NotBeNull(); + response!.Status.Should().Be(200); + + // Check for login form elements + var usernameInput = await _page.QuerySelectorAsync("input[name='Username']"); + var passwordInput = await _page.QuerySelectorAsync("input[name='Password']"); + var submitButton = await _page.QuerySelectorAsync("button[type='submit']"); + + usernameInput.Should().NotBeNull(); + passwordInput.Should().NotBeNull(); + submitButton.Should().NotBeNull(); + } + + [Fact] + public async Task Login_WithValidCredentials_ShouldRedirectToDashboard() + { + // Arrange + await _page!.GotoAsync($"{_baseUrl}/Admin/Account/Login"); + + // Act + await _page.FillAsync("input[name='Username']", "admin"); + await _page.FillAsync("input[name='Password']", "admin"); + await _page.ClickAsync("button[type='submit']"); + + // Wait for navigation + await _page.WaitForURLAsync($"{_baseUrl}/Admin/Dashboard"); + + // Assert + _page.Url.Should().Contain("/Admin/Dashboard"); + + // Check dashboard loaded + var dashboardTitle = await _page.TextContentAsync("h1"); + dashboardTitle.Should().Contain("Dashboard"); + } + + [Fact] + public async Task Login_WithInvalidCredentials_ShouldShowError() + { + // Arrange + await _page!.GotoAsync($"{_baseUrl}/Admin/Account/Login"); + + // Act + await _page.FillAsync("input[name='Username']", "wronguser"); + await _page.FillAsync("input[name='Password']", "wrongpass"); + await _page.ClickAsync("button[type='submit']"); + + // Assert + // Should stay on login page + _page.Url.Should().Contain("/Admin/Account/Login"); + + // Check for error message + var errorMessage = await _page.QuerySelectorAsync(".text-danger, .alert-danger"); + errorMessage.Should().NotBeNull(); + } + + [Fact] + public async Task AdminPages_WithoutLogin_ShouldRedirectToLogin() + { + // Arrange & Act + var pagesToTest = new[] + { + "/Admin/Dashboard", + "/Admin/Categories", + "/Admin/Products", + "/Admin/Orders", + "/Admin/Users" + }; + + foreach (var pageUrl in pagesToTest) + { + await _page!.GotoAsync($"{_baseUrl}{pageUrl}"); + + // Assert - Should redirect to login + _page.Url.Should().Contain("/Admin/Account/Login"); + } + } + + [Fact] + public async Task Dashboard_AfterLogin_ShouldLoadAllSections() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Act + await _page!.GotoAsync($"{_baseUrl}/Admin/Dashboard"); + + // Assert + var response = await _page.GotoAsync($"{_baseUrl}/Admin/Dashboard"); + response!.Status.Should().Be(200); + + // Check for dashboard sections + var statsCards = await _page.QuerySelectorAllAsync(".card"); + statsCards.Count.Should().BeGreaterThan(0); + + // Check navigation menu is present + var navMenu = await _page.QuerySelectorAsync(".navbar"); + navMenu.Should().NotBeNull(); + } + + [Fact] + public async Task Categories_CRUD_ShouldWorkWithoutErrors() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Navigate to Categories + await _page!.GotoAsync($"{_baseUrl}/Admin/Categories"); + var response = await _page.GotoAsync($"{_baseUrl}/Admin/Categories"); + response!.Status.Should().Be(200); + + // Create Category + await _page.ClickAsync("a[href*='Categories/Create']"); + await _page.WaitForURLAsync("**/Admin/Categories/Create"); + + await _page.FillAsync("input[name='Name']", "Test Category"); + await _page.FillAsync("textarea[name='Description']", "Test Description"); + await _page.ClickAsync("button[type='submit']"); + + // Should redirect back to index + await _page.WaitForURLAsync("**/Admin/Categories"); + + // Verify category appears in list + var categoryName = await _page.TextContentAsync("td:has-text('Test Category')"); + categoryName.Should().NotBeNull(); + } + + [Fact] + public async Task Products_Index_ShouldLoadWithoutErrors() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Act + var response = await _page!.GotoAsync($"{_baseUrl}/Admin/Products"); + + // Assert + response!.Status.Should().Be(200); + + // Check for products table or empty message + var productsTable = await _page.QuerySelectorAsync("table"); + var emptyMessage = await _page.QuerySelectorAsync("text=No products found"); + + (productsTable != null || emptyMessage != null).Should().BeTrue(); + } + + [Fact] + public async Task Orders_Index_ShouldLoadWithoutErrors() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Act + var response = await _page!.GotoAsync($"{_baseUrl}/Admin/Orders"); + + // Assert + response!.Status.Should().Be(200); + + // Check for orders table or empty message + var ordersTable = await _page.QuerySelectorAsync("table"); + var emptyMessage = await _page.QuerySelectorAsync("text=No orders found"); + + (ordersTable != null || emptyMessage != null).Should().BeTrue(); + } + + [Fact] + public async Task Users_Index_ShouldLoadWithoutErrors() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Act + var response = await _page!.GotoAsync($"{_baseUrl}/Admin/Users"); + + // Assert + response!.Status.Should().Be(200); + + // Should at least show the admin user + var usersTable = await _page.QuerySelectorAsync("table"); + usersTable.Should().NotBeNull(); + + var adminUser = await _page.TextContentAsync("td:has-text('admin')"); + adminUser.Should().NotBeNull(); + } + + [Fact] + public async Task Logout_ShouldRedirectToLogin() + { + // Arrange - Login first + await LoginAsAdmin(); + + // Act - Find and click logout + await _page!.ClickAsync("a[href*='Account/Logout']"); + + // Assert + await _page.WaitForURLAsync("**/Admin/Account/Login"); + _page.Url.Should().Contain("/Admin/Account/Login"); + } + + [Fact] + public async Task NetworkErrors_ShouldBeCaptured() + { + // Arrange + var failedRequests = new List(); + _page!.RequestFailed += (_, request) => + { + failedRequests.Add($"{request.Method} {request.Url} - {request.Failure}"); + }; + + await LoginAsAdmin(); + + // Act - Navigate through admin pages + var pagesToCheck = new[] + { + "/Admin/Dashboard", + "/Admin/Categories", + "/Admin/Products", + "/Admin/Orders", + "/Admin/Users" + }; + + foreach (var pageUrl in pagesToCheck) + { + await _page.GotoAsync($"{_baseUrl}{pageUrl}"); + } + + // Assert - No failed requests + failedRequests.Should().BeEmpty("No network requests should fail"); + } + + [Fact] + public async Task ConsoleErrors_ShouldBeCaptured() + { + // Arrange + var consoleErrors = new List(); + _page!.Console += (_, msg) => + { + if (msg.Type == "error") + { + consoleErrors.Add(msg.Text); + } + }; + + await LoginAsAdmin(); + + // Act - Navigate through admin pages + var pagesToCheck = new[] + { + "/Admin/Dashboard", + "/Admin/Categories", + "/Admin/Products" + }; + + foreach (var pageUrl in pagesToCheck) + { + await _page.GotoAsync($"{_baseUrl}{pageUrl}"); + } + + // Assert - No console errors + consoleErrors.Should().BeEmpty("No JavaScript errors should occur"); + } + + private async Task LoginAsAdmin() + { + await _page!.GotoAsync($"{_baseUrl}/Admin/Account/Login"); + await _page.FillAsync("input[name='Username']", "admin"); + await _page.FillAsync("input[name='Password']", "admin"); + await _page.ClickAsync("button[type='submit']"); + await _page.WaitForURLAsync($"{_baseUrl}/Admin/Dashboard"); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Unit/CategoryServiceTests.cs b/LittleShop.Tests/Unit/CategoryServiceTests.cs new file mode 100644 index 0000000..1149ffe --- /dev/null +++ b/LittleShop.Tests/Unit/CategoryServiceTests.cs @@ -0,0 +1,262 @@ +using FluentAssertions; +using LittleShop.Data; +using LittleShop.DTOs; +using LittleShop.Models; +using LittleShop.Services; +using Microsoft.EntityFrameworkCore; +using Moq; +using AutoMapper; +using Xunit; +using LittleShop.Mapping; + +namespace LittleShop.Tests.Unit; + +public class CategoryServiceTests : IDisposable +{ + private readonly LittleShopContext _context; + private readonly ICategoryService _categoryService; + private readonly IMapper _mapper; + + public CategoryServiceTests() + { + // Set up in-memory database + var options = new DbContextOptionsBuilder() + .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 + _categoryService = new CategoryService(_context, _mapper); + } + + [Fact] + public async Task GetAllCategoriesAsync_ReturnsAllCategories() + { + // Arrange + var categories = new[] + { + new Category { Id = Guid.NewGuid(), Name = "Category 1", Description = "Desc 1", IsActive = true }, + new Category { Id = Guid.NewGuid(), Name = "Category 2", Description = "Desc 2", IsActive = true }, + new Category { Id = Guid.NewGuid(), Name = "Category 3", Description = "Desc 3", IsActive = false } + }; + + _context.Categories.AddRange(categories); + await _context.SaveChangesAsync(); + + // Act + var result = await _categoryService.GetAllCategoriesAsync(); + + // Assert + result.Should().HaveCount(3); + result.Should().Contain(c => c.Name == "Category 1"); + result.Should().Contain(c => c.Name == "Category 2"); + result.Should().Contain(c => c.Name == "Category 3"); + } + + [Fact] + public async Task GetCategoryByIdAsync_WithValidId_ReturnsCategory() + { + // Arrange + var categoryId = Guid.NewGuid(); + var category = new Category + { + Id = categoryId, + Name = "Test Category", + Description = "Test Description", + IsActive = true + }; + + _context.Categories.Add(category); + await _context.SaveChangesAsync(); + + // Act + var result = await _categoryService.GetCategoryByIdAsync(categoryId); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(categoryId); + result.Name.Should().Be("Test Category"); + result.Description.Should().Be("Test Description"); + } + + [Fact] + public async Task GetCategoryByIdAsync_WithInvalidId_ReturnsNull() + { + // Arrange + var invalidId = Guid.NewGuid(); + + // Act + var result = await _categoryService.GetCategoryByIdAsync(invalidId); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task CreateCategoryAsync_WithValidData_CreatesCategory() + { + // Arrange + var createDto = new CreateCategoryDto + { + Name = "New Category", + Description = "New Description" + }; + + // Act + var result = await _categoryService.CreateCategoryAsync(createDto); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be("New Category"); + result.Description.Should().Be("New Description"); + result.IsActive.Should().BeTrue(); + + // Verify in database + var dbCategory = await _context.Categories.FindAsync(result.Id); + dbCategory.Should().NotBeNull(); + dbCategory!.Name.Should().Be("New Category"); + } + + [Fact] + public async Task UpdateCategoryAsync_WithValidData_UpdatesCategory() + { + // Arrange + var categoryId = Guid.NewGuid(); + var category = new Category + { + Id = categoryId, + Name = "Original Name", + Description = "Original Description", + IsActive = true + }; + + _context.Categories.Add(category); + await _context.SaveChangesAsync(); + + var updateDto = new UpdateCategoryDto + { + Name = "Updated Name", + Description = "Updated Description", + IsActive = false + }; + + // Act + var result = await _categoryService.UpdateCategoryAsync(categoryId, updateDto); + + // Assert + result.Should().BeTrue(); + + // Verify in database + var dbCategory = await _context.Categories.FindAsync(categoryId); + dbCategory!.Name.Should().Be("Updated Name"); + dbCategory.Description.Should().Be("Updated Description"); + dbCategory.IsActive.Should().BeFalse(); + } + + [Fact] + public async Task UpdateCategoryAsync_WithInvalidId_ReturnsFalse() + { + // Arrange + var invalidId = Guid.NewGuid(); + var updateDto = new UpdateCategoryDto + { + Name = "Updated Name", + Description = "Updated Description", + IsActive = true + }; + + // Act + var result = await _categoryService.UpdateCategoryAsync(invalidId, updateDto); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task DeleteCategoryAsync_WithValidId_DeletesCategory() + { + // Arrange + var categoryId = Guid.NewGuid(); + var category = new Category + { + Id = categoryId, + Name = "To Delete", + Description = "Will be deleted", + IsActive = true + }; + + _context.Categories.Add(category); + await _context.SaveChangesAsync(); + + // Act + var result = await _categoryService.DeleteCategoryAsync(categoryId); + + // Assert + result.Should().BeTrue(); + + // Verify in database + var dbCategory = await _context.Categories.FindAsync(categoryId); + dbCategory.Should().BeNull(); + } + + [Fact] + public async Task DeleteCategoryAsync_WithInvalidId_ReturnsFalse() + { + // Arrange + var invalidId = Guid.NewGuid(); + + // Act + var result = await _categoryService.DeleteCategoryAsync(invalidId); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task DeleteCategoryAsync_WithProductsAttached_ThrowsException() + { + // Arrange + var categoryId = Guid.NewGuid(); + var category = new Category + { + Id = categoryId, + Name = "Category with Products", + Description = "Has products", + IsActive = true + }; + + var product = new Product + { + Id = Guid.NewGuid(), + Name = "Product", + Description = "Product in category", + Price = 10.00m, + CategoryId = categoryId, + IsActive = true, + Weight = 1, + WeightUnit = Enums.ProductWeightUnit.Kilograms + }; + + _context.Categories.Add(category); + _context.Products.Add(product); + await _context.SaveChangesAsync(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _categoryService.DeleteCategoryAsync(categoryId); + }); + } + + public void Dispose() + { + _context.Dispose(); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/Unit/ProductServiceTests.cs b/LittleShop.Tests/Unit/ProductServiceTests.cs new file mode 100644 index 0000000..34c1306 --- /dev/null +++ b/LittleShop.Tests/Unit/ProductServiceTests.cs @@ -0,0 +1,313 @@ +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() + .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 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(); + } +} \ No newline at end of file diff --git a/LittleShop.Tests/UnitTest1.cs b/LittleShop.Tests/UnitTest1.cs new file mode 100644 index 0000000..501122b --- /dev/null +++ b/LittleShop.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace LittleShop.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/LittleShop.Tests/appsettings.Testing.json b/LittleShop.Tests/appsettings.Testing.json new file mode 100644 index 0000000..62f9174 --- /dev/null +++ b/LittleShop.Tests/appsettings.Testing.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "DefaultConnection": "DataSource=:memory:" + }, + "Jwt": { + "Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!", + "Issuer": "LittleShop", + "Audience": "LittleShop", + "ExpirationMinutes": 60 + }, + "BTCPayServer": { + "ServerUrl": "https://testnet.btcpayserver.com", + "StoreId": "test-store-id", + "ApiKey": "test-api-key" + } +} \ No newline at end of file diff --git a/LittleShop.Tests/cookies.txt b/LittleShop.Tests/cookies.txt new file mode 100644 index 0000000..4cc857d --- /dev/null +++ b/LittleShop.Tests/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8DxJ63S0c9FElmGFRyriOyyD6rmycqL7JuHuVlcOFX2R_GxlIY4qgsVAgJmK8AahopahF-vF2JcxuIuSjY5B3zuZBJrG0jFcmDO9SR2Xz3eS74gnvvfLu7llRaIbh0xleQwPmHgydzoxzZNBG6Yz0xbHGHyeL4YPCPbOZu8YxvYeKzKRbuR2hkw5iUR6mZAGSqGCkq2cCiin1c0Lu-I4v4VZAuYnz7l6yDCYBDLUJMTFa9EMS5zebh4yVTsixzLzmQMRBcbLBswGb20vjQBVSfjFhDCn-SKGkqLRmvuakfKaXVsV002NSFakFRdQWc8sdX-uTWj2I7UTNjX-yC8UUXLdl7UldMjkztkBYO2imCXwBONa9RR2W0YLfFrthx4F3PaPoxAVX8fHVBZ-QCO5iQFJUtUhDR7E_ygdKFNE-z8n_6qSSrpac2hnnbq0h2lewg diff --git a/LittleShop.Tests/test.jpg b/LittleShop.Tests/test.jpg new file mode 100644 index 0000000..43cb636 --- /dev/null +++ b/LittleShop.Tests/test.jpg @@ -0,0 +1 @@ +test image content diff --git a/LittleShop.sln b/LittleShop.sln new file mode 100644 index 0000000..c870144 --- /dev/null +++ b/LittleShop.sln @@ -0,0 +1,82 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleShop", "LittleShop\LittleShop.csproj", "{45F90A9D-4B8B-48D8-8D80-7B2335DD9072}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TeleBot", "TeleBot", "{92C8E2EB-69F0-B69F-DB5B-725FD6E47E88}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeleBot", "TeleBot\TeleBot\TeleBot.csproj", "{0B5C4E8B-0618-496A-8614-B8580B28257F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeleBotClient", "TeleBot\TeleBotClient\TeleBotClient.csproj", "{4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleShop.Tests", "LittleShop.Tests\LittleShop.Tests.csproj", "{96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|x64.ActiveCfg = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|x64.Build.0 = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|x86.ActiveCfg = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Debug|x86.Build.0 = Debug|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|Any CPU.Build.0 = Release|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|x64.ActiveCfg = Release|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|x64.Build.0 = Release|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|x86.ActiveCfg = Release|Any CPU + {45F90A9D-4B8B-48D8-8D80-7B2335DD9072}.Release|x86.Build.0 = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|x64.Build.0 = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Debug|x86.Build.0 = Debug|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|Any CPU.Build.0 = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|x64.ActiveCfg = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|x64.Build.0 = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|x86.ActiveCfg = Release|Any CPU + {0B5C4E8B-0618-496A-8614-B8580B28257F}.Release|x86.Build.0 = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|x64.ActiveCfg = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|x64.Build.0 = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|x86.ActiveCfg = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Debug|x86.Build.0 = Debug|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|Any CPU.Build.0 = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|x64.ActiveCfg = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|x64.Build.0 = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|x86.ActiveCfg = Release|Any CPU + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C}.Release|x86.Build.0 = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|x64.Build.0 = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Debug|x86.Build.0 = Debug|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|Any CPU.Build.0 = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|x64.ActiveCfg = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|x64.Build.0 = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|x86.ActiveCfg = Release|Any CPU + {96E261C3-BBEB-4FC5-B006-DCC0B514F6D9}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0B5C4E8B-0618-496A-8614-B8580B28257F} = {92C8E2EB-69F0-B69F-DB5B-725FD6E47E88} + {4ABAC8E7-9088-4D68-ADDF-E3249CBED85C} = {92C8E2EB-69F0-B69F-DB5B-725FD6E47E88} + EndGlobalSection +EndGlobal diff --git a/Areas/Admin/Controllers/AccountController.cs b/LittleShop/Areas/Admin/Controllers/AccountController.cs similarity index 100% rename from Areas/Admin/Controllers/AccountController.cs rename to LittleShop/Areas/Admin/Controllers/AccountController.cs diff --git a/Areas/Admin/Controllers/CategoriesController.cs b/LittleShop/Areas/Admin/Controllers/CategoriesController.cs similarity index 100% rename from Areas/Admin/Controllers/CategoriesController.cs rename to LittleShop/Areas/Admin/Controllers/CategoriesController.cs diff --git a/Areas/Admin/Controllers/DashboardController.cs b/LittleShop/Areas/Admin/Controllers/DashboardController.cs similarity index 100% rename from Areas/Admin/Controllers/DashboardController.cs rename to LittleShop/Areas/Admin/Controllers/DashboardController.cs diff --git a/LittleShop/Areas/Admin/Controllers/OrdersController.cs b/LittleShop/Areas/Admin/Controllers/OrdersController.cs new file mode 100644 index 0000000..935e9f1 --- /dev/null +++ b/LittleShop/Areas/Admin/Controllers/OrdersController.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using LittleShop.Services; +using LittleShop.DTOs; + +namespace LittleShop.Areas.Admin.Controllers; + +[Area("Admin")] +[Authorize(Policy = "AdminOnly")] +public class OrdersController : Controller +{ + private readonly IOrderService _orderService; + + public OrdersController(IOrderService orderService) + { + _orderService = orderService; + } + + public async Task Index() + { + var orders = await _orderService.GetAllOrdersAsync(); + return View(orders.OrderByDescending(o => o.CreatedAt)); + } + + public async Task Details(Guid id) + { + var order = await _orderService.GetOrderByIdAsync(id); + if (order == null) + { + return NotFound(); + } + + return View(order); + } + + public IActionResult Create() + { + return View(new CreateOrderDto()); + } + + [HttpPost] + public async Task Create(CreateOrderDto model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var order = await _orderService.CreateOrderAsync(model); + return RedirectToAction(nameof(Details), new { id = order.Id }); + } + + public async Task Edit(Guid id) + { + var order = await _orderService.GetOrderByIdAsync(id); + if (order == null) + { + return NotFound(); + } + + return View(order); + } + + [HttpPost] + public async Task Edit(Guid id, OrderDto model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var updateDto = new UpdateOrderStatusDto + { + Status = model.Status, + TrackingNumber = model.TrackingNumber, + Notes = model.Notes + }; + + var success = await _orderService.UpdateOrderStatusAsync(id, updateDto); + if (!success) + { + return NotFound(); + } + + return RedirectToAction(nameof(Details), new { id }); + } + + [HttpPost] + public async Task UpdateStatus(Guid id, UpdateOrderStatusDto model) + { + var success = await _orderService.UpdateOrderStatusAsync(id, model); + if (!success) + { + return NotFound(); + } + + return RedirectToAction(nameof(Details), new { id }); + } +} \ No newline at end of file diff --git a/Areas/Admin/Controllers/ProductsController.cs b/LittleShop/Areas/Admin/Controllers/ProductsController.cs similarity index 94% rename from Areas/Admin/Controllers/ProductsController.cs rename to LittleShop/Areas/Admin/Controllers/ProductsController.cs index 7daa2fa..aefd0ac 100644 --- a/Areas/Admin/Controllers/ProductsController.cs +++ b/LittleShop/Areas/Admin/Controllers/ProductsController.cs @@ -34,7 +34,7 @@ public class ProductsController : Controller [HttpPost] public async Task Create(CreateProductDto model) { - Console.WriteLine($"Received Product: Name='{model?.Name}', Description='{model?.Description}', Price={model?.BasePrice}"); + Console.WriteLine($"Received Product: Name='{model?.Name}', Description='{model?.Description}', Price={model?.Price}"); Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}"); if (!ModelState.IsValid) @@ -64,9 +64,9 @@ public class ProductsController : Controller { Name = product.Name, Description = product.Description, - ProductWeightUnit = product.ProductWeightUnit, - ProductWeight = product.ProductWeight, - BasePrice = product.BasePrice, + WeightUnit = product.WeightUnit, + Weight = product.Weight, + Price = product.Price, CategoryId = product.CategoryId, IsActive = product.IsActive }; diff --git a/LittleShop/Areas/Admin/Controllers/ShippingRatesController.cs b/LittleShop/Areas/Admin/Controllers/ShippingRatesController.cs new file mode 100644 index 0000000..c60d335 --- /dev/null +++ b/LittleShop/Areas/Admin/Controllers/ShippingRatesController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using LittleShop.Services; +using LittleShop.DTOs; + +namespace LittleShop.Areas.Admin.Controllers; + +[Area("Admin")] +[Authorize(Policy = "AdminOnly")] +public class ShippingRatesController : Controller +{ + private readonly IShippingRateService _shippingRateService; + private readonly ILogger _logger; + + public ShippingRatesController(IShippingRateService shippingRateService, ILogger logger) + { + _shippingRateService = shippingRateService; + _logger = logger; + } + + public async Task Index() + { + var rates = await _shippingRateService.GetAllShippingRatesAsync(); + return View(rates); + } + + public IActionResult Create() + { + return View(new CreateShippingRateDto()); + } + + [HttpPost] + public async Task Create(CreateShippingRateDto model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var rate = await _shippingRateService.CreateShippingRateAsync(model); + _logger.LogInformation("Created shipping rate {RateId}", rate.Id); + + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(Guid id) + { + var rate = await _shippingRateService.GetShippingRateByIdAsync(id); + if (rate == null) + { + return NotFound(); + } + + var model = new UpdateShippingRateDto + { + Name = rate.Name, + Description = rate.Description, + Country = rate.Country, + MinWeight = rate.MinWeight, + MaxWeight = rate.MaxWeight, + Price = rate.Price, + MinDeliveryDays = rate.MinDeliveryDays, + MaxDeliveryDays = rate.MaxDeliveryDays, + IsActive = rate.IsActive + }; + + ViewData["RateId"] = id; + return View(model); + } + + [HttpPost] + public async Task Edit(Guid id, UpdateShippingRateDto model) + { + if (!ModelState.IsValid) + { + ViewData["RateId"] = id; + return View(model); + } + + var success = await _shippingRateService.UpdateShippingRateAsync(id, model); + if (!success) + { + return NotFound(); + } + + _logger.LogInformation("Updated shipping rate {RateId}", id); + return RedirectToAction(nameof(Index)); + } + + [HttpPost] + public async Task Delete(Guid id) + { + var success = await _shippingRateService.DeleteShippingRateAsync(id); + if (!success) + { + return NotFound(); + } + + _logger.LogInformation("Deleted shipping rate {RateId}", id); + return RedirectToAction(nameof(Index)); + } +} \ No newline at end of file diff --git a/Areas/Admin/Controllers/UsersController.cs b/LittleShop/Areas/Admin/Controllers/UsersController.cs similarity index 100% rename from Areas/Admin/Controllers/UsersController.cs rename to LittleShop/Areas/Admin/Controllers/UsersController.cs diff --git a/Areas/Admin/Views/Account/Login.cshtml b/LittleShop/Areas/Admin/Views/Account/Login.cshtml similarity index 100% rename from Areas/Admin/Views/Account/Login.cshtml rename to LittleShop/Areas/Admin/Views/Account/Login.cshtml diff --git a/Areas/Admin/Views/Categories/Create.cshtml b/LittleShop/Areas/Admin/Views/Categories/Create.cshtml similarity index 100% rename from Areas/Admin/Views/Categories/Create.cshtml rename to LittleShop/Areas/Admin/Views/Categories/Create.cshtml diff --git a/Areas/Admin/Views/Categories/Edit.cshtml b/LittleShop/Areas/Admin/Views/Categories/Edit.cshtml similarity index 100% rename from Areas/Admin/Views/Categories/Edit.cshtml rename to LittleShop/Areas/Admin/Views/Categories/Edit.cshtml diff --git a/Areas/Admin/Views/Categories/Index.cshtml b/LittleShop/Areas/Admin/Views/Categories/Index.cshtml similarity index 100% rename from Areas/Admin/Views/Categories/Index.cshtml rename to LittleShop/Areas/Admin/Views/Categories/Index.cshtml diff --git a/Areas/Admin/Views/Dashboard/Index.cshtml b/LittleShop/Areas/Admin/Views/Dashboard/Index.cshtml similarity index 100% rename from Areas/Admin/Views/Dashboard/Index.cshtml rename to LittleShop/Areas/Admin/Views/Dashboard/Index.cshtml diff --git a/LittleShop/Areas/Admin/Views/Orders/Create.cshtml b/LittleShop/Areas/Admin/Views/Orders/Create.cshtml new file mode 100644 index 0000000..a0fbe58 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Orders/Create.cshtml @@ -0,0 +1,106 @@ +@model LittleShop.DTOs.CreateOrderDto + +@{ + ViewData["Title"] = "Create Order"; +} + +
+
+

Create Order

+
+
+ +
+
+
+
+
Customer Information
+
+
+
+ @Html.AntiForgeryToken() + @if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0) + { + + } + +
+ + + Anonymous identifier for the customer +
+ +
Shipping Information
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + Back to Orders + + +
+
+
+
+
+ +
+
+
+
Order Information
+
+
+

Create a new order manually for phone or email orders.

+
    +
  • Customer Reference: Anonymous identifier
  • +
  • Shipping Details: Required for delivery
  • +
  • Payment: Add after order creation
  • +
  • Products: Add items after creation
  • +
+ Orders created here will appear in the orders list with PendingPayment status. +
+
+
+
\ No newline at end of file diff --git a/Areas/Admin/Views/Orders/Details.cshtml b/LittleShop/Areas/Admin/Views/Orders/Details.cshtml similarity index 87% rename from Areas/Admin/Views/Orders/Details.cshtml rename to LittleShop/Areas/Admin/Views/Orders/Details.cshtml index d17a55c..e0ca428 100644 --- a/Areas/Admin/Views/Orders/Details.cshtml +++ b/LittleShop/Areas/Admin/Views/Orders/Details.cshtml @@ -10,6 +10,9 @@

Order ID: @Model.Id

+
+
+
Shipping Information
+
+
+
+
+

Name: @Model.ShippingName

+

Address: @Model.ShippingAddress

+

City: @Model.ShippingCity

+
+
+

Post Code: @Model.ShippingPostCode

+

Country: @Model.ShippingCountry

+
+
+
+
+
Order Items
@@ -85,7 +107,7 @@ - @foreach (var item in Model.OrderItems) + @foreach (var item in Model.Items) { @item.ProductName @@ -144,14 +166,14 @@
- @if (Model.CryptoPayments.Any()) + @if (Model.Payments.Any()) {
Crypto Payments
- @foreach (var payment in Model.CryptoPayments) + @foreach (var payment in Model.Payments) {
diff --git a/LittleShop/Areas/Admin/Views/Orders/Edit.cshtml b/LittleShop/Areas/Admin/Views/Orders/Edit.cshtml new file mode 100644 index 0000000..0fd0235 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Orders/Edit.cshtml @@ -0,0 +1,218 @@ +@model LittleShop.DTOs.OrderDto + +@{ + ViewData["Title"] = "Edit Order"; +} + +
+
+

Edit Order #@Model?.Id

+
+
+ +
+
+
+
+
Order Information
+
+
+
+ @Html.AntiForgeryToken() + @if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0) + { + + } + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
Shipping Information
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + Back to Details + + +
+
+
+
+ + +
+
+
Order Items
+
+
+ @if (Model?.Items?.Any() == true) + { +
+ + + + + + + + + + + @foreach (var item in Model.Items) + { + + + + + + + } + + + + + + + +
ProductQuantityUnit PriceTotal
@item.ProductName@item.Quantity£@item.UnitPrice£@item.TotalPrice
Total Amount£@Model.TotalAmount
+
+ } + else + { +

No items in this order.

+ } +
+
+
+ +
+
+
+
Order Summary
+
+
+
+
Created
+
@Model?.CreatedAt.ToString("dd/MM/yyyy HH:mm")
+ +
Updated
+
@Model?.UpdatedAt.ToString("dd/MM/yyyy HH:mm")
+ + @if (Model?.PaidAt != null) + { +
Paid
+
@Model.PaidAt.Value.ToString("dd/MM/yyyy HH:mm")
+ } + + @if (Model?.ShippedAt != null) + { +
Shipped
+
@Model.ShippedAt.Value.ToString("dd/MM/yyyy HH:mm")
+ } + +
Total Amount
+
£@Model?.TotalAmount
+
+
+
+ + + @if (Model?.Payments?.Any() == true) + { +
+
+
Payments
+
+
+ @foreach (var payment in Model.Payments) + { +
+ @payment.Currency + + @payment.Status + + Amount: @payment.RequiredAmount +
+ } +
+
+ } +
+
\ No newline at end of file diff --git a/Areas/Admin/Views/Orders/Index.cshtml b/LittleShop/Areas/Admin/Views/Orders/Index.cshtml similarity index 86% rename from Areas/Admin/Views/Orders/Index.cshtml rename to LittleShop/Areas/Admin/Views/Orders/Index.cshtml index a50b2ab..1e327fd 100644 --- a/Areas/Admin/Views/Orders/Index.cshtml +++ b/LittleShop/Areas/Admin/Views/Orders/Index.cshtml @@ -8,6 +8,11 @@

Orders

+
@@ -19,9 +24,10 @@ Order ID - Identity Reference + Customer + Shipping To Status - Total Amount + Total Created Actions @@ -31,7 +37,8 @@ { @order.Id.ToString().Substring(0, 8)... - @order.IdentityReference + @order.ShippingName + @order.ShippingCity, @order.ShippingCountry @{ var badgeClass = order.Status switch diff --git a/Areas/Admin/Views/Products/Create.cshtml b/LittleShop/Areas/Admin/Views/Products/Create.cshtml similarity index 88% rename from Areas/Admin/Views/Products/Create.cshtml rename to LittleShop/Areas/Admin/Views/Products/Create.cshtml index d548b2d..8b9ce62 100644 --- a/Areas/Admin/Views/Products/Create.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Create.cshtml @@ -40,8 +40,8 @@
- - + +
@@ -64,14 +64,14 @@
- - + +
- - diff --git a/Areas/Admin/Views/Products/Edit.cshtml b/LittleShop/Areas/Admin/Views/Products/Edit.cshtml similarity index 83% rename from Areas/Admin/Views/Products/Edit.cshtml rename to LittleShop/Areas/Admin/Views/Products/Edit.cshtml index 43dedfc..7e895ce 100644 --- a/Areas/Admin/Views/Products/Edit.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Edit.cshtml @@ -41,8 +41,8 @@
- - + +
@@ -65,21 +65,21 @@
- - + +
- - + + + + + + +
diff --git a/Areas/Admin/Views/Products/Index.cshtml b/LittleShop/Areas/Admin/Views/Products/Index.cshtml similarity index 96% rename from Areas/Admin/Views/Products/Index.cshtml rename to LittleShop/Areas/Admin/Views/Products/Index.cshtml index 726e85c..a76fa0d 100644 --- a/Areas/Admin/Views/Products/Index.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Index.cshtml @@ -56,10 +56,10 @@ @product.CategoryName - £@product.BasePrice + £@product.Price - @product.ProductWeight @product.ProductWeightUnit.ToString().ToLower() + @product.Weight @product.WeightUnit.ToString().ToLower() @if (product.IsActive) diff --git a/Areas/Admin/Views/Shared/_Layout.cshtml b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml similarity index 92% rename from Areas/Admin/Views/Shared/_Layout.cshtml rename to LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml index c3b3d75..caec6e2 100644 --- a/Areas/Admin/Views/Shared/_Layout.cshtml +++ b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml @@ -39,6 +39,11 @@ Orders +