101 lines
4.4 KiB
TypeScript
101 lines
4.4 KiB
TypeScript
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);
|
||
});
|