arbret/frontend/e2e/admin-pricing.spec.ts
counterweight 7f547d667d
Fix e2e tests for pricing page
- Update selectors to use input indices instead of labels (labels not associated)
- Fix validation error status expectation (400 instead of 422)
- Update exchange.spec.ts to check new config fields (eur_min_buy, etc.)
2025-12-26 20:59:10 +01:00

298 lines
11 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpers/auth";
import { getBackendUrl } from "./helpers/backend-url";
/**
* Admin Pricing Page E2E Tests
*
* Tests for the admin pricing configuration page.
*/
test.describe("Admin Pricing Page - Admin Access", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
await clearAuth(page);
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
});
test("admin can access pricing page and UI elements are correct", async ({ page }) => {
// Test navigation link
await page.goto("/admin/trades");
const pricingLink = page.locator('a[href="/admin/pricing"]');
await expect(pricingLink).toBeVisible();
// Test page access and structure
await page.goto("/admin/pricing");
await expect(page).toHaveURL("/admin/pricing");
await expect(page.getByRole("heading", { name: "Pricing Configuration" })).toBeVisible();
await expect(page.getByText("Configure premium pricing and trade amount limits")).toBeVisible();
// Check all form fields are present (using text + input selector since labels aren't associated)
await expect(page.getByText(/Premium for BUY/i)).toBeVisible();
await expect(page.locator('input[type="number"]').first()).toBeVisible();
await expect(page.getByText(/Premium for SELL/i)).toBeVisible();
await expect(page.getByText(/Small Trade Threshold/i)).toBeVisible();
await expect(page.getByText(/Extra Premium for Small Trades/i)).toBeVisible();
await expect(page.getByText(/Trade Amount Limits.*BUY/i)).toBeVisible();
await expect(page.getByText(/Minimum Amount/i).first()).toBeVisible();
await expect(page.getByText(/Maximum Amount/i).first()).toBeVisible();
await expect(page.getByText(/Trade Amount Limits.*SELL/i)).toBeVisible();
// Check save button is present
await expect(page.getByRole("button", { name: /Save Changes/i })).toBeVisible();
});
test("can view current pricing configuration", async ({ page }) => {
await page.goto("/admin/pricing");
// Wait for data to load
await page.waitForLoadState("networkidle");
// All input fields should have values (not empty)
const inputs = page.locator('input[type="number"]');
const count = await inputs.count();
expect(count).toBeGreaterThan(0);
// Check that premium fields have values (inputs are after labels)
const inputs = page.locator('input[type="number"]');
const buyValue = await inputs.nth(0).inputValue(); // First input is premium_buy
const sellValue = await inputs.nth(1).inputValue(); // Second input is premium_sell
expect(buyValue).not.toBe("");
expect(sellValue).not.toBe("");
});
test("can update pricing configuration", async ({ page }) => {
await page.goto("/admin/pricing");
await page.waitForLoadState("networkidle");
// Get current values (first input is premium_buy)
const inputs = page.locator('input[type="number"]');
const premiumBuyInput = inputs.nth(0);
const currentBuyValue = await premiumBuyInput.inputValue();
const newBuyValue = currentBuyValue === "5" ? "6" : "5";
// Update premium buy value
await premiumBuyInput.clear();
await premiumBuyInput.fill(newBuyValue);
// Set up response listener before clicking save
const putPromise = page.waitForResponse(
(resp) => resp.url().includes("/api/admin/pricing") && resp.request().method() === "PUT"
);
const getPromise = page.waitForResponse(
(resp) => resp.url().includes("/api/admin/pricing") && resp.request().method() === "GET"
);
// Click save button (with confirmation)
await page.getByRole("button", { name: /Save Changes/i }).click();
// Confirm the save action
await expect(page.getByText(/Are you sure/i)).toBeVisible();
await page.getByRole("button", { name: /confirm|yes|ok/i }).click();
// Wait for API calls to complete
await putPromise;
await getPromise;
// Check for success message
await expect(page.getByText(/saved successfully/i)).toBeVisible({ timeout: 5000 });
// Verify the value was updated
await page.waitForLoadState("networkidle");
const updatedValue = await premiumBuyInput.inputValue();
expect(updatedValue).toBe(newBuyValue);
});
test("validation prevents invalid values", async ({ page }) => {
await page.goto("/admin/pricing");
await page.waitForLoadState("networkidle");
// Test premium range validation (should be -100 to 100)
const inputs = page.locator('input[type="number"]');
const premiumBuyInput = inputs.nth(0); // First input is premium_buy
await premiumBuyInput.clear();
await premiumBuyInput.fill("150"); // Invalid: > 100
// Try to save
await page.getByRole("button", { name: /Save Changes/i }).click();
await expect(page.getByText(/confirm|yes|ok/i)).toBeVisible();
await page.getByRole("button", { name: /confirm|yes|ok/i }).click();
// Should show validation error
await expect(page.getByText(/must be between.*-100.*100/i)).toBeVisible({ timeout: 2000 });
// Test min < max validation (inputs 4 and 5 are min/max buy)
const inputs = page.locator('input[type="number"]');
const minBuyInput = inputs.nth(4); // Min buy is 5th input (after 4 premium/threshold inputs)
const maxBuyInput = inputs.nth(5); // Max buy is 6th input
const currentMin = await minBuyInput.inputValue();
const currentMax = await maxBuyInput.inputValue();
// Set min >= max (should fail)
await minBuyInput.clear();
await minBuyInput.fill(currentMax);
await maxBuyInput.clear();
await maxBuyInput.fill(currentMin);
// Try to save
await page.getByRole("button", { name: /Save Changes/i }).click();
await expect(page.getByText(/confirm|yes|ok/i)).toBeVisible();
await page.getByRole("button", { name: /confirm|yes|ok/i }).click();
// Should show validation error
await expect(page.getByText(/minimum must be less than maximum/i)).toBeVisible({
timeout: 2000,
});
});
test("form fields update correctly when values change", async ({ page }) => {
await page.goto("/admin/pricing");
await page.waitForLoadState("networkidle");
// Small trade threshold is the 3rd input (after premium_buy and premium_sell)
const inputs = page.locator('input[type="number"]');
const smallTradeThresholdInput = inputs.nth(2);
const currentThreshold = await smallTradeThresholdInput.inputValue();
// Update threshold
const newThreshold = currentThreshold === "200" ? "250" : "200";
await smallTradeThresholdInput.clear();
await smallTradeThresholdInput.fill(newThreshold);
// Verify the value is set
const updatedThreshold = await smallTradeThresholdInput.inputValue();
expect(updatedThreshold).toBe(newThreshold);
});
});
test.describe("Admin Pricing Page - Access Control", () => {
test("regular user cannot access pricing page", async ({ page }) => {
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
// Regular user should not see pricing link
await page.goto("/");
const pricingLink = page.locator('a[href="/admin/pricing"]');
await expect(pricingLink).toHaveCount(0);
// Direct access should redirect
await page.goto("/admin/pricing");
await expect(page).not.toHaveURL("/admin/pricing");
});
test("unauthenticated user cannot access pricing page", async ({ page }) => {
await clearAuth(page);
await page.goto("/admin/pricing");
await expect(page).toHaveURL("/login");
});
});
test.describe("Admin Pricing API", () => {
test("admin can access pricing API, regular user cannot", async ({ page, request }) => {
// Test admin API access
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) {
// Test GET pricing config
const getResponse = await request.get(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
});
expect(getResponse.status()).toBe(200);
const data = await getResponse.json();
expect(data).toHaveProperty("premium_buy");
expect(data).toHaveProperty("premium_sell");
expect(data).toHaveProperty("eur_min_buy");
expect(data).toHaveProperty("eur_max_buy");
}
// Test regular user API access
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
const regularCookies = await page.context().cookies();
const regularAuthCookie = regularCookies.find((c) => c.name === "auth_token");
if (regularAuthCookie) {
const response = await request.get(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${regularAuthCookie.value}`,
},
});
expect(response.status()).toBe(403);
}
});
test("admin can update pricing via API with validation", 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) {
// First get current config
const getResponse = await request.get(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
});
const currentConfig = await getResponse.json();
// Update with valid values
const updateResponse = await request.put(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
"Content-Type": "application/json",
},
data: {
premium_buy: 6,
premium_sell: 6,
small_trade_threshold_eur: 20000,
small_trade_extra_premium: 2,
eur_min_buy: 10000,
eur_max_buy: 300000,
eur_min_sell: 10000,
eur_max_sell: 300000,
},
});
expect(updateResponse.status()).toBe(200);
const updatedData = await updateResponse.json();
expect(updatedData.premium_buy).toBe(6);
// Test validation - invalid premium (> 100)
const invalidResponse = await request.put(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
"Content-Type": "application/json",
},
data: {
premium_buy: 150, // Invalid
premium_sell: 6,
small_trade_threshold_eur: 20000,
small_trade_extra_premium: 2,
eur_min_buy: 10000,
eur_max_buy: 300000,
eur_min_sell: 10000,
eur_max_sell: 300000,
},
});
expect(invalidResponse.status()).toBe(400); // BadRequestError returns 400
// Restore original values
await request.put(`${getBackendUrl()}/api/admin/pricing`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
"Content-Type": "application/json",
},
data: currentConfig,
});
}
});
});