From 8c959a0177e77a65b0de04ee31e1414cf991eb21 Mon Sep 17 00:00:00 2001 From: Al Francis Date: Sun, 24 May 2026 12:43:35 -0700 Subject: [PATCH] test: add deployment smoke checks --- playwright.config.ts | 28 +++++++++----- tests/e2e/deployment-smoke.spec.ts | 61 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/deployment-smoke.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 7d7ba6d..2d3b295 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,10 +2,16 @@ import { defineConfig, devices } from "@playwright/test"; /** * Playwright E2E configuration for Vanguard VDP. - * Tests run against the Next.js dev server on localhost:3000. + * + * By default runs against the local Next.js dev server (localhost:3000). + * Set BASE_URL to run against a remote environment, e.g.: + * BASE_URL=https://vanguard.laet4x.com npx playwright test * * @see https://playwright.dev/docs/test-configuration */ +const BASE_URL = process.env.BASE_URL ?? "http://localhost:3000"; +const isRemote = BASE_URL.startsWith("https://") || BASE_URL.startsWith("http://") && !BASE_URL.includes("localhost"); + export default defineConfig({ testDir: "./tests/e2e", fullyParallel: true, @@ -15,7 +21,7 @@ export default defineConfig({ reporter: "html", use: { - baseURL: "http://localhost:3000", + baseURL: BASE_URL, trace: "on-first-retry", screenshot: "only-on-failure", }, @@ -27,11 +33,15 @@ export default defineConfig({ }, ], - /* Start the Next.js dev server before running tests */ - webServer: { - command: "npm run dev", - url: "http://localhost:3000", - reuseExistingServer: !process.env.CI, - timeout: 120_000, - }, + /* Only spin up the local dev server when not targeting a remote URL */ + ...(isRemote + ? {} + : { + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + }), }); diff --git a/tests/e2e/deployment-smoke.spec.ts b/tests/e2e/deployment-smoke.spec.ts new file mode 100644 index 0000000..e07cec5 --- /dev/null +++ b/tests/e2e/deployment-smoke.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +const expectedHeaders = [ + "x-frame-options", + "x-content-type-options", + "referrer-policy", + "permissions-policy", + "content-security-policy", +]; + +test.describe("Deployment smoke test", () => { + test("public pages respond without server errors", async ({ request }) => { + const paths = ["/", "/policy", "/hall-of-fame", "/submit", "/sign-in"]; + + for (const path of paths) { + const response = await request.get(path, { maxRedirects: 0 }); + + expect( + response.status(), + `${path} should not return a server error`, + ).toBeLessThan(500); + } + }); + + test("homepage renders the core public shell", async ({ page }) => { + await page.goto("/"); + + const header = page.locator("header"); + + await expect(page).toHaveTitle(/Vanguard/i); + await expect(header).toBeVisible(); + await expect(header.getByRole("link", { name: /Hall of Fame/i })).toBeVisible(); + await expect(header.getByRole("link", { name: /Submit/i })).toBeVisible(); + await expect(page.locator("footer")).toBeVisible(); + }); + + test("security headers are present on page responses", async ({ request }) => { + const response = await request.get("/"); + + expect(response.ok()).toBeTruthy(); + + for (const header of expectedHeaders) { + expect(response.headers()[header], `${header} should be present`).toBeTruthy(); + } + + expect(response.headers()["x-frame-options"]).toBe("DENY"); + expect(response.headers()["x-content-type-options"]).toBe("nosniff"); + }); + + test("robots.txt disallows protected application routes", async ({ request }) => { + const response = await request.get("/robots.txt"); + const body = await response.text(); + + expect(response.ok()).toBeTruthy(); + expect(body).toContain("User-agent: *"); + + for (const path of ["/admin", "/triage", "/dashboard", "/api", "/sign-in", "/sign-up"]) { + expect(body).toContain(`Disallow: ${path}`); + } + }); +});