mercury-web/e2e/smoke.spec.ts

96 lines
4 KiB
TypeScript
Raw 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 this intercepts the network at the
* browser layer (page.route) rather than standing up a stub server — the
* Next dev server (and all of its own client/server code, including the
* httpOnly-cookie auth route and the middleware guard) runs for real; only
* the two things that would otherwise reach the Go backend
* (`/api/auth/login`, `/api/v1/*`) are fulfilled with fixtures.
*
* Also asserts the two security-relevant properties the review flagged:
* - the session cookie set on login is HttpOnly
* - no response body observed during the flow contains a raw bearer token
* (the Next auth route is supposed to strip `token` before it reaches the
* browser — see src/app/api/auth/login/route.ts)
*/
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);
});