Files
NetForge/e2e/uat.spec.ts
kevin-asprec dcffed4a0d feat: add Docker deployment, UAT test suites, and fix signup middleware bug
- Add Dockerfile, docker-entrypoint.sh, and .dockerignore for containerized deployment
- Fix middleware to exclude /api/tenants/signup from auth (P0 signup bug)
- Add Playwright E2E tests (16 browser tests) and curl-based API test script (80 tests)
- Add playwright config and dev dependency
- Update .gitignore with proper exclusions
- Add v1 milestone audit report and ISP system PRD

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:20:54 +08:00

275 lines
9.7 KiB
TypeScript

import { test, expect, Page } from "@playwright/test";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function loginAs(page: Page, email: string, password: string) {
await page.goto("/login");
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');
// Wait for redirect away from login
await page.waitForURL((url) => !url.pathname.includes("/login"), {
timeout: 10000,
});
}
// ---------------------------------------------------------------------------
// 1. Login Page
// ---------------------------------------------------------------------------
test.describe("Login Page", () => {
test("renders login form with email and password fields", async ({
page,
}) => {
await page.goto("/login");
await expect(page.locator("h1")).toContainText("NetForge");
await expect(page.locator('input[name="email"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible();
await expect(page.locator('a[href="/signup"]')).toBeVisible();
});
test("shows error on invalid credentials", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "bad@example.com");
await page.fill('input[name="password"]', "wrongpassword");
await page.click('button[type="submit"]');
// Should stay on login page and show error
await page.waitForTimeout(2000);
const url = page.url();
expect(url).toContain("/login");
// Check for error message or error in URL
const hasError =
url.includes("error") ||
(await page.locator('[role="alert"], .text-red, .error').count()) > 0;
expect(hasError).toBe(true);
});
test("successful admin login redirects to dashboard", async ({ page }) => {
await loginAs(page, "admin@demo.com", "admin123");
// Should land on dashboard or a valid authenticated page
const url = page.url();
expect(url).not.toContain("/login");
});
test("session persists across page refresh", async ({ page }) => {
await loginAs(page, "admin@demo.com", "admin123");
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Should still be on an authenticated page (not redirected to login)
const url = page.url();
expect(url).not.toContain("/login");
});
});
// ---------------------------------------------------------------------------
// 2. Signup Page
// ---------------------------------------------------------------------------
test.describe("Signup Page", () => {
test("renders signup form with all required fields", async ({ page }) => {
await page.goto("/signup");
await expect(page.locator("h1, h2").first()).toBeVisible();
// Check for key form fields
const inputs = await page.locator("input").count();
expect(inputs).toBeGreaterThanOrEqual(4); // name, email, password, confirm
await expect(page.locator('button[type="submit"]')).toBeVisible();
});
test("can register a new tenant", async ({ page }) => {
const ts = Date.now();
await page.goto("/signup");
// Fill in signup form fields
// The form has: company name, owner name, email, password, confirm password
const allInputs = page.locator("input");
const inputCount = await allInputs.count();
// Try to fill known field patterns
const companyInput = page.locator(
'input[name*="company"], input[name*="tenant"], input[name*="business"], input[placeholder*="company" i], input[placeholder*="ISP" i]'
);
if ((await companyInput.count()) > 0) {
await companyInput.first().fill(`UAT Test ISP ${ts}`);
}
const nameInputs = page.locator(
'input[name*="name"]:not([name*="company"]):not([name*="tenant"]):not([name*="business"]):not([type="email"]):not([type="password"])'
);
for (let i = 0; i < (await nameInputs.count()); i++) {
const name = await nameInputs.nth(i).getAttribute("name");
if (name?.includes("first") || name?.includes("First")) {
await nameInputs.nth(i).fill("UAT");
} else if (name?.includes("last") || name?.includes("Last")) {
await nameInputs.nth(i).fill("Tester");
} else {
await nameInputs.nth(i).fill("UAT Tester");
}
}
const emailInput = page.locator('input[type="email"], input[name="email"]');
if ((await emailInput.count()) > 0) {
await emailInput.first().fill(`uat-${ts}@test.com`);
}
const passwordInputs = page.locator('input[type="password"]');
const pwCount = await passwordInputs.count();
for (let i = 0; i < pwCount; i++) {
await passwordInputs.nth(i).fill("TestPass123!");
}
// Submit
await page.click('button[type="submit"]');
// Wait for result — should redirect to login with success or show success message
await page.waitForTimeout(3000);
const url = page.url();
const pageText = await page.textContent("body");
const success =
url.includes("registered") ||
url.includes("login") ||
url.includes("success") ||
pageText?.toLowerCase().includes("created") ||
pageText?.toLowerCase().includes("success") ||
pageText?.toLowerCase().includes("registered");
expect(success).toBe(true);
});
});
// ---------------------------------------------------------------------------
// 3. Dashboard Page (Authenticated)
// ---------------------------------------------------------------------------
test.describe("Dashboard", () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, "admin@demo.com", "admin123");
});
test("dashboard page loads", async ({ page }) => {
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
// Page should render without error
const status = page.url();
expect(status).toContain("/dashboard");
// Check for dashboard content (may be minimal since API-only was built)
const body = await page.textContent("body");
expect(body).toBeTruthy();
});
test("dashboard is not accessible when logged out", async ({ page }) => {
// Clear cookies to simulate logout
await page.context().clearCookies();
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
// Should redirect to login
const url = page.url();
expect(url).toContain("/login");
});
});
// ---------------------------------------------------------------------------
// 4. Super-Admin Panel
// ---------------------------------------------------------------------------
test.describe("Super-Admin Panel", () => {
test("super-admin can access tenant management", async ({ page }) => {
await loginAs(page, "superadmin@netforge.com", "super123");
await page.goto("/admin/tenants");
await page.waitForLoadState("networkidle");
// Should see tenant list
const body = await page.textContent("body");
expect(body).toContain("Demo ISP");
});
test("regular admin cannot access super-admin panel", async ({ page }) => {
await loginAs(page, "admin@demo.com", "admin123");
await page.goto("/admin/tenants");
await page.waitForLoadState("networkidle");
// Should be forbidden or redirected
const url = page.url();
const body = await page.textContent("body");
const blocked =
url.includes("/login") ||
url.includes("/dashboard") ||
body?.toLowerCase().includes("forbidden") ||
body?.toLowerCase().includes("denied") ||
body?.toLowerCase().includes("403");
expect(blocked).toBe(true);
});
});
// ---------------------------------------------------------------------------
// 5. API Smoke Tests via Page Context
// ---------------------------------------------------------------------------
test.describe("API via authenticated browser context", () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, "admin@demo.com", "admin123");
});
test("GET /api/subscribers returns valid JSON", async ({ page }) => {
const response = await page.goto("/api/subscribers");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(json).toHaveProperty("subscribers");
expect(json).toHaveProperty("total");
});
test("GET /api/dashboard returns dashboard summary", async ({ page }) => {
const response = await page.goto("/api/dashboard");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(json).toHaveProperty("revenue");
expect(json).toHaveProperty("subscribers");
expect(json).toHaveProperty("cashFlow");
});
test("GET /api/service-plans returns array", async ({ page }) => {
const response = await page.goto("/api/service-plans");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(Array.isArray(json)).toBe(true);
});
test("GET /api/tickets returns tickets", async ({ page }) => {
const response = await page.goto("/api/tickets");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(json).toHaveProperty("tickets");
});
test("GET /api/zones returns array", async ({ page }) => {
const response = await page.goto("/api/zones");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(Array.isArray(json)).toBe(true);
});
test("GET /api/reports/trial-balance returns balanced books", async ({
page,
}) => {
const response = await page.goto("/api/reports/trial-balance");
expect(response?.status()).toBe(200);
const json = await response?.json();
expect(json).toHaveProperty("isBalanced");
expect(json.isBalanced).toBe(true);
});
});