mercury-web/e2e/smoke.spec.ts

101 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from "@playwright/test";
import * as fx from "../src/test/fixtures";
/**
* One smoke path: /login → submit → /home renders the real dashboard.
*
* There's no live Go backend in CI, so `page.route` intercepts
* `/api/auth/login` and `/api/v1/*` at the BROWSER network layer, before
* either request ever reaches the Next server — the real
* `src/app/api/auth/login/route.ts` handler (which calls the Go backend,
* strips `token`, and calls `buildSetCookie`) never runs in this test; this
* spec fabricates the `Set-Cookie` header and the token-free `{user}` body
* itself. What DOES run for real: the Next dev server's page rendering, the
* client-side login form + navigation, and — critically — the
* `src/middleware.ts` route guard, which reads the `mercury_session` cookie
* this test's fake `Set-Cookie` puts in the browser's real cookie jar to
* decide whether `/home` is reachable. So this smoke covers the
* login → navigate → guarded-route → dashboard-render path end to end; it
* does NOT cover the real auth route's token-stripping/cookie-building logic
* (that's covered separately by src/app/api/auth/auth.route.test.ts). The
* httpOnly/no-raw-token assertions below validate this test's own mocked
* response shape, i.e. they document the contract the real route must also
* satisfy — they are not independent proof that the real route satisfies it.
*/
test("login → home renders the dashboard; session cookie is httpOnly; no raw token leaks", async ({
page,
context,
}) => {
const responseBodies: string[] = [];
page.on("response", (response) => {
response
.text()
.then((body) => responseBodies.push(body))
.catch(() => {
/* non-text bodies (redirects, static assets, etc.) — not relevant here */
});
});
await page.route("**/api/auth/login", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
headers: {
"set-cookie": "mercury_session=e2e-fake-session; Path=/; HttpOnly; SameSite=Lax",
},
body: JSON.stringify({ user: fx.user }),
});
});
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const path = url.pathname.replace(/^\/api\/v1/, "");
const body: unknown =
path === "/networth"
? fx.netWorth
: path === "/analyze"
? url.searchParams.has("from") || url.searchParams.has("to")
? fx.analyzeToday
: fx.analyzeMonth
: path === "/budget"
? fx.budget
: path === "/transactions"
? { transactions: [fx.txn] }
: path === "/categories"
? { categories: fx.categories }
: path === "/subscriptions"
? { subscriptions: fx.subscriptions, bills: fx.bills }
: path === "/manual-assets"
? { manualAssets: fx.manualAssets }
: path === "/lending"
? { entries: [fx.lending] }
: path === "/settings"
? fx.settings
: path === "/connections"
? { connections: fx.connections }
: path === "/me"
? { user: fx.user }
: {};
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body) });
});
await page.goto("/login");
await page.getByLabel("Имэйл хаяг").fill("test@example.com");
await page.getByLabel("Нууц үг").fill("password12345");
await page.getByRole("button", { name: "Нэвтрэх" }).click();
await page.waitForURL("**/home");
// The dashboard header wordmark, and the real safe-to-spend figure derived
// from the intercepted /networth + /analyze + /budget fixtures (900,000
// monthLimit 800,000 discretionaryExpense — see buildHome.test.ts).
await expect(page.getByText("MERCURY")).toBeVisible();
await expect(page.getByText("100,000₮")).toBeVisible();
const cookies = await context.cookies();
const session = cookies.find((c) => c.name === "mercury_session");
expect(session, "mercury_session cookie should be set after login").toBeTruthy();
expect(session?.httpOnly).toBe(true);
const leaked = responseBodies.some((body) => /"token"\s*:\s*"/.test(body));
expect(leaked, "no observed response body should contain a raw token field").toBe(false);
});