96 lines
4 KiB
TypeScript
96 lines
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 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);
|
||
});
|