319 lines
12 KiB
TypeScript
319 lines
12 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, view UI elements and current configuration", 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
|
|
// Set up response listener before navigation
|
|
const responsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes("/api/admin/pricing") && resp.request().method() === "GET"
|
|
);
|
|
await page.goto("/admin/pricing");
|
|
await expect(page).toHaveURL("/admin/pricing");
|
|
|
|
// Wait for API call to complete and form to render
|
|
await responsePromise;
|
|
// Wait for form inputs to be visible (indicates form has loaded)
|
|
await expect(page.locator('input[type="number"]').first()).toBeVisible({ timeout: 10000 });
|
|
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.*Buys/i)).toBeVisible();
|
|
await expect(page.locator('input[type="number"]').first()).toBeVisible();
|
|
await expect(page.getByText(/Premium.*Sells/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.*Buying/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.*Selling/i)).toBeVisible();
|
|
|
|
// Check save button is present
|
|
await expect(page.getByRole("button", { name: /Save Changes/i })).toBeVisible();
|
|
|
|
// Check that form fields have loaded values
|
|
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 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 and form fields update correctly", async ({ page }) => {
|
|
// Set up response listener before navigation
|
|
const responsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes("/api/admin/pricing") && resp.request().method() === "GET"
|
|
);
|
|
await page.goto("/admin/pricing");
|
|
|
|
// Wait for API call to complete and form to render
|
|
await responsePromise;
|
|
await expect(page.getByRole("heading", { name: "Pricing Configuration" })).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Wait for inputs to be visible
|
|
const inputs = page.locator('input[type="number"]');
|
|
await expect(inputs.first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// Test that form fields update correctly when values change
|
|
const smallTradeThresholdInput = inputs.nth(2);
|
|
const currentThreshold = await smallTradeThresholdInput.inputValue();
|
|
const newThreshold = currentThreshold === "200" ? "250" : "200";
|
|
await smallTradeThresholdInput.clear();
|
|
await smallTradeThresholdInput.fill(newThreshold);
|
|
|
|
// Verify the value is set immediately
|
|
const updatedThreshold = await smallTradeThresholdInput.inputValue();
|
|
expect(updatedThreshold).toBe(newThreshold);
|
|
|
|
// Now test updating and saving a different field
|
|
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);
|
|
|
|
// Wait a bit for any validation to clear
|
|
await page.waitForTimeout(200);
|
|
|
|
// Verify save button is enabled before clicking
|
|
const saveButton = page.getByRole("button", { name: /Save Changes/i });
|
|
await expect(saveButton).toBeEnabled({ timeout: 5000 });
|
|
|
|
// Click save button (enters confirmation mode)
|
|
await saveButton.click();
|
|
|
|
// Confirm the save action (button text changes to "Confirm")
|
|
const confirmButton = page.getByRole("button", { name: /Confirm/i });
|
|
await expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
|
|
// Click confirm and wait for network to be idle
|
|
await confirmButton.click();
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Check for success message
|
|
await expect(page.getByText(/saved successfully/i))
|
|
.toBeVisible({ timeout: 10000 })
|
|
.catch(() => {
|
|
// If success message doesn't appear immediately, wait a bit more
|
|
});
|
|
|
|
// Verify the value was updated - re-query inputs after save
|
|
await page.waitForTimeout(500); // Small delay for state update
|
|
const updatedInputs = page.locator('input[type="number"]');
|
|
await expect(updatedInputs.nth(0)).toBeVisible({ timeout: 5000 });
|
|
const updatedValue = await updatedInputs.nth(0).inputValue();
|
|
expect(updatedValue).toBe(newBuyValue);
|
|
});
|
|
|
|
test("validation prevents invalid values", async ({ page }) => {
|
|
// Set up response listener before navigation
|
|
const responsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes("/api/admin/pricing") && resp.request().method() === "GET"
|
|
);
|
|
await page.goto("/admin/pricing");
|
|
|
|
// Wait for API call to complete and form to render
|
|
await responsePromise;
|
|
await expect(page.getByRole("heading", { name: "Pricing Configuration" })).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Wait for inputs to be visible
|
|
const inputs = page.locator('input[type="number"]');
|
|
await expect(inputs.first()).toBeVisible({ timeout: 5000 });
|
|
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.getByRole("button", { name: /Confirm/i })).toBeVisible({ timeout: 2000 });
|
|
await page.getByRole("button", { name: /Confirm/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 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.getByRole("button", { name: /Confirm/i })).toBeVisible({ timeout: 2000 });
|
|
await page.getByRole("button", { name: /Confirm/i }).click();
|
|
|
|
// Should show validation error (there are two - one for min, one for max)
|
|
await expect(page.getByText(/minimum must be less than maximum/i).first()).toBeVisible({
|
|
timeout: 2000,
|
|
});
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
}
|
|
});
|
|
});
|