Features Added: - Standard e-commerce properties (Price, Weight, shipping fields) - Order management with Create/Edit views and shipping information - ShippingRates system for weight-based shipping calculations - Comprehensive test coverage with JWT authentication tests - Sample data seeder with 5 orders demonstrating full workflow - Photo upload functionality for products - Multi-cryptocurrency payment support (BTC, XMR, USDT, etc.) Database Changes: - Added ShippingRates table - Added shipping fields to Orders (Name, Address, City, PostCode, Country) - Renamed properties to standard names (BasePrice to Price, ProductWeight to Weight) - Added UpdatedAt timestamps to models UI Improvements: - Added Create/Edit views for Orders - Added ShippingRates management UI - Updated navigation menu with Shipping option - Enhanced Order Details view with shipping information Sample Data: - 3 Categories (Electronics, Clothing, Books) - 5 Products with various prices - 5 Shipping rates (Royal Mail options) - 5 Orders in different statuses (Pending to Delivered) - 3 Crypto payments demonstrating payment flow Security: - All API endpoints secured with JWT authentication - No public endpoints - client apps must authenticate - Privacy-focused design with minimal data collection Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
322 lines
9.8 KiB
C#
322 lines
9.8 KiB
C#
using Microsoft.Playwright;
|
|
using Xunit;
|
|
using FluentAssertions;
|
|
using LittleShop.Tests.Infrastructure;
|
|
|
|
namespace LittleShop.Tests.UI;
|
|
|
|
[Collection("Playwright")]
|
|
public class AdminPanelTests : IClassFixture<TestWebApplicationFactory>, 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<string>();
|
|
_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<string>();
|
|
_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");
|
|
}
|
|
} |