From 9e8d0af43540a6d775623f9662bdca931ac7473b Mon Sep 17 00:00:00 2001 From: counterweight Date: Mon, 22 Dec 2025 20:12:19 +0100 Subject: [PATCH] Phase 4.2: Add Exchange E2E tests New exchange.spec.ts with comprehensive E2E coverage: - Exchange page access and UI elements (price, buy/sell, slider) - Slot selection with availability - Confirmation form with trade details - Access control (regular user vs admin) - My Trades page - Admin Trades page with tabs - Exchange API permission tests --- frontend/e2e/exchange.spec.ts | 376 ++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 frontend/e2e/exchange.spec.ts diff --git a/frontend/e2e/exchange.spec.ts b/frontend/e2e/exchange.spec.ts new file mode 100644 index 0000000..e213aa6 --- /dev/null +++ b/frontend/e2e/exchange.spec.ts @@ -0,0 +1,376 @@ +import { test, expect, Page } from "@playwright/test"; +import { getTomorrowDateStr } from "./helpers/date"; +import { API_URL, REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpers/auth"; + +/** + * Exchange Page E2E Tests + * + * Tests for the Bitcoin exchange trading page. + */ + +// Set up availability for a date using the API +async function setAvailability(page: Page, dateStr: string) { + const cookies = await page.context().cookies(); + const authCookie = cookies.find((c) => c.name === "auth_token"); + + if (!authCookie) { + throw new Error("No auth cookie found when trying to set availability"); + } + + const response = await page.request.put(`${API_URL}/api/admin/availability`, { + headers: { + Cookie: `auth_token=${authCookie.value}`, + "Content-Type": "application/json", + }, + data: { + date: dateStr, + slots: [{ start_time: "09:00:00", end_time: "12:00:00" }], + }, + }); + + if (!response.ok()) { + const body = await response.text(); + throw new Error(`Failed to set availability: ${response.status()} - ${body}`); + } +} + +test.describe("Exchange Page - Regular User Access", () => { + test.beforeEach(async ({ page }) => { + await clearAuth(page); + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + }); + + test("regular user can access exchange page", async ({ page }) => { + await page.goto("/exchange"); + + await expect(page).toHaveURL("/exchange"); + await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible(); + }); + + test("regular user sees Exchange link in navigation", async ({ page }) => { + await page.goto("/trades"); + + await expect(page.getByRole("link", { name: "Exchange" })).toBeVisible(); + }); + + test("exchange page shows price information", async ({ page }) => { + await page.goto("/exchange"); + + // Should show market and our price + await expect(page.getByText("Market:")).toBeVisible(); + await expect(page.getByText("Our price:")).toBeVisible(); + }); + + test("exchange page shows buy/sell toggle", async ({ page }) => { + await page.goto("/exchange"); + + await expect(page.getByRole("button", { name: "Buy BTC" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Sell BTC" })).toBeVisible(); + }); + + test("exchange page shows amount slider", async ({ page }) => { + await page.goto("/exchange"); + + // Should show amount section + await expect(page.getByText("Amount")).toBeVisible(); + await expect(page.locator('input[type="range"]')).toBeVisible(); + }); + + test("clicking buy/sell changes direction", async ({ page }) => { + await page.goto("/exchange"); + + // Click Sell BTC + await page.getByRole("button", { name: "Sell BTC" }).click(); + + // The summary should mention "You send" for sell + await expect(page.getByText(/You send/)).toBeVisible(); + }); + + test("exchange page shows date selection", async ({ page }) => { + await page.goto("/exchange"); + + await expect(page.getByRole("heading", { name: "Select a Date" })).toBeVisible(); + // Should see multiple date buttons + const dateButtons = page + .locator("button") + .filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ }); + await expect(dateButtons.first()).toBeVisible(); + }); +}); + +test.describe("Exchange Page - With Availability", () => { + test.beforeEach(async ({ page }) => { + await clearAuth(page); + // Login as admin to set availability + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + await setAvailability(page, getTomorrowDateStr()); + await clearAuth(page); + // Login as regular user + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + }); + + test("shows available slots when availability is set", async ({ page }) => { + await page.goto("/exchange"); + + // Get tomorrow's display name to click the correct button + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" }); + + // Click tomorrow's date using the weekday name + const dateButton = page + .locator("button") + .filter({ hasText: new RegExp(`^${weekday}`) }) + .first(); + await dateButton.click(); + + // Wait for "Available Slots" section to appear + await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible(); + + // Wait for loading to finish + await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); + + // Should see some slot buttons + const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); + await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); + }); + + test("clicking slot shows confirmation form", async ({ page }) => { + await page.goto("/exchange"); + + // Get tomorrow's display name + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" }); + + // Click tomorrow's date + const dateButton = page + .locator("button") + .filter({ hasText: new RegExp(`^${weekday}`) }) + .first(); + await dateButton.click(); + + // Wait for any slot to appear + await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); + const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); + await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); + + // Click first slot + await slotButtons.first().click(); + + // Should show confirmation form + await expect(page.getByText("Confirm Trade")).toBeVisible(); + await expect(page.getByRole("button", { name: /Confirm/ })).toBeVisible(); + }); + + test("confirmation shows trade details", async ({ page }) => { + await page.goto("/exchange"); + + // Get tomorrow's display name + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" }); + + // Click tomorrow's date + const dateButton = page + .locator("button") + .filter({ hasText: new RegExp(`^${weekday}`) }) + .first(); + await dateButton.click(); + + // Wait for slots to load + await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); + const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); + await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); + + // Click second slot + await slotButtons.nth(1).click(); + + // Should show confirmation with trade details + await expect(page.getByText("Confirm Trade")).toBeVisible(); + await expect(page.getByText("Time:")).toBeVisible(); + await expect(page.getByText("Direction:")).toBeVisible(); + await expect(page.getByText("EUR:")).toBeVisible(); + await expect(page.getByText("BTC:")).toBeVisible(); + await expect(page.getByText("Rate:")).toBeVisible(); + }); +}); + +test.describe("Exchange Page - Access Control", () => { + test("admin cannot access exchange page", async ({ page }) => { + await clearAuth(page); + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + + await page.goto("/exchange"); + + // Should be redirected away + await expect(page).not.toHaveURL("/exchange"); + }); + + test("admin does not see Exchange link", async ({ page }) => { + await clearAuth(page); + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + + await page.goto("/admin/trades"); + + await expect(page.getByRole("link", { name: "Exchange" })).not.toBeVisible(); + }); + + test("unauthenticated user redirected to login", async ({ page }) => { + await clearAuth(page); + + await page.goto("/exchange"); + + await expect(page).toHaveURL("/login"); + }); +}); + +test.describe("Trades Page", () => { + test.beforeEach(async ({ page }) => { + await clearAuth(page); + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + }); + + test("regular user can access trades page", async ({ page }) => { + await page.goto("/trades"); + + await expect(page).toHaveURL("/trades"); + await expect(page.getByRole("heading", { name: "My Trades" })).toBeVisible(); + }); + + test("trades page shows empty state when no trades", async ({ page }) => { + await page.goto("/trades"); + + // Either shows empty state message or trades list + const content = page.locator("body"); + await expect(content).toBeVisible(); + }); + + test("trades page shows Start trading link when empty", async ({ page }) => { + await page.goto("/trades"); + + // Wait for content to load + await page.waitForTimeout(1000); + + // Check if it shows empty state with link, or trades exist + const startTradingLink = page.getByRole("link", { name: "Start trading" }); + const isLinkVisible = await startTradingLink.isVisible().catch(() => false); + + if (isLinkVisible) { + await expect(startTradingLink).toHaveAttribute("href", "/exchange"); + } + }); +}); + +test.describe("Admin Trades Page", () => { + test.beforeEach(async ({ page }) => { + await clearAuth(page); + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + }); + + test("admin can access trades page", async ({ page }) => { + await page.goto("/admin/trades"); + + await expect(page).toHaveURL("/admin/trades"); + await expect(page.getByRole("heading", { name: "Trades" })).toBeVisible(); + }); + + test("admin trades page shows tabs", async ({ page }) => { + await page.goto("/admin/trades"); + + await expect(page.getByRole("button", { name: /Upcoming/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /History/ })).toBeVisible(); + }); + + test("regular user cannot access admin trades page", async ({ page }) => { + await clearAuth(page); + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + + await page.goto("/admin/trades"); + + // Should be redirected away + await expect(page).not.toHaveURL("/admin/trades"); + }); +}); + +test.describe("Exchange API", () => { + test("regular user can get price via API", async ({ page, request }) => { + await clearAuth(page); + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + + const cookies = await page.context().cookies(); + const authCookie = cookies.find((c) => c.name === "auth_token"); + + if (authCookie) { + const response = await request.get(`${API_URL}/api/exchange/price`, { + headers: { + Cookie: `auth_token=${authCookie.value}`, + }, + }); + + expect(response.status()).toBe(200); + const data = await response.json(); + expect(data.config).toBeDefined(); + expect(data.config.eur_min).toBeDefined(); + expect(data.config.eur_max).toBeDefined(); + } + }); + + test("admin cannot get price via API", async ({ page, request }) => { + await clearAuth(page); + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + + const cookies = await page.context().cookies(); + const authCookie = cookies.find((c) => c.name === "auth_token"); + + if (authCookie) { + const response = await request.get(`${API_URL}/api/exchange/price`, { + headers: { + Cookie: `auth_token=${authCookie.value}`, + }, + }); + + expect(response.status()).toBe(403); + } + }); + + test("regular user can get trades via API", async ({ page, request }) => { + await clearAuth(page); + await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); + + const cookies = await page.context().cookies(); + const authCookie = cookies.find((c) => c.name === "auth_token"); + + if (authCookie) { + const response = await request.get(`${API_URL}/api/trades`, { + headers: { + Cookie: `auth_token=${authCookie.value}`, + }, + }); + + expect(response.status()).toBe(200); + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + } + }); + + test("admin can get upcoming trades via API", async ({ page, request }) => { + await clearAuth(page); + await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); + + const cookies = await page.context().cookies(); + const authCookie = cookies.find((c) => c.name === "auth_token"); + + if (authCookie) { + const response = await request.get(`${API_URL}/api/admin/trades/upcoming`, { + headers: { + Cookie: `auth_token=${authCookie.value}`, + }, + }); + + expect(response.status()).toBe(200); + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + } + }); +});