diff --git a/apps/web/src/pages/analytics-consent-page.tsx b/apps/web/src/pages/analytics-consent-page.tsx index 57de1e10..79dfc38a 100644 --- a/apps/web/src/pages/analytics-consent-page.tsx +++ b/apps/web/src/pages/analytics-consent-page.tsx @@ -8,7 +8,8 @@ const t = en.analytics; export function AnalyticsConsentPage() { const navigate = useNavigate(); - const { config, configLoaded, fetchConfig, acceptAnalytics, remindLater } = useAnalyticsStore(); + const { config, configLoaded, fetchConfig, acceptAnalytics, declineAnalytics, remindLater } = + useAnalyticsStore(); useEffect(() => { fetchConfig(); @@ -16,9 +17,9 @@ export function AnalyticsConsentPage() { useEffect(() => { if (configLoaded && !config?.enabled) { - navigate("/", { replace: true }); + declineAnalytics().then(() => navigate("/", { replace: true })); } - }, [configLoaded, config, navigate]); + }, [configLoaded, config, navigate, declineAnalytics]); if (!configLoaded) { return ( diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 144a799a..3b5263db 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -12,9 +12,9 @@ export default defineConfig({ dedupe: ["react", "react-dom"], }, server: { - port: 1349, + port: Number(process.env.PORT) || 1349, proxy: { - "/api": "http://localhost:13490", + "/api": process.env.VITE_API_URL || "http://localhost:13490", }, }, build: { diff --git a/playwright.config.ts b/playwright.config.ts index d46ef8ec..cb9fba06 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,9 @@ import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; const authFile = path.join(__dirname, "test-results", ".auth", "user.json"); +const testDbPath = path.join(__dirname, "test-results", ".e2e-db", "ashim.db"); + +const TEST_WEB_PORT = 2349; export default defineConfig({ testDir: "./tests/e2e", @@ -20,7 +23,7 @@ export default defineConfig({ workers: 1, reporter: "html", use: { - baseURL: "http://localhost:1349", + baseURL: `http://localhost:${TEST_WEB_PORT}`, screenshot: "only-on-failure", trace: "retain-on-failure", }, @@ -40,23 +43,28 @@ export default defineConfig({ ], webServer: [ { - command: "pnpm --filter @ashim/api dev", + command: `rm -f "${testDbPath}" "${testDbPath}-shm" "${testDbPath}-wal" && mkdir -p "${path.dirname(testDbPath)}" && pnpm --filter @ashim/api dev`, port: 13490, reuseExistingServer: !process.env.CI, env: { - PORT: "13490", AUTH_ENABLED: "true", DEFAULT_USERNAME: "admin", DEFAULT_PASSWORD: "admin", RATE_LIMIT_PER_MIN: "50000", SKIP_MUST_CHANGE_PASSWORD: "true", + ANALYTICS_ENABLED: "false", + DB_PATH: testDbPath, }, timeout: 30_000, }, { command: "pnpm --filter @ashim/web dev", - port: 1349, + port: TEST_WEB_PORT, reuseExistingServer: !process.env.CI, + env: { + PORT: String(TEST_WEB_PORT), + VITE_API_URL: "http://localhost:13490", + }, timeout: 30_000, }, ], diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts index e5b3beb4..f5265b87 100644 --- a/tests/e2e/auth.setup.ts +++ b/tests/e2e/auth.setup.ts @@ -14,8 +14,21 @@ setup("authenticate", async ({ page }) => { await page.getByLabel("Password").fill("admin"); await page.getByRole("button", { name: /login/i }).click(); - // Wait for the full-page redirect to "/" - await page.waitForURL("/", { timeout: 15_000 }); + // Wait for login to complete and grab the token in one step + const handle = await page.waitForFunction(() => localStorage.getItem("ashim-token"), null, { + timeout: 15_000, + }); + const token = await handle.jsonValue(); + + // Dismiss analytics consent via API so it won't block any test + const apiBase = process.env.API_URL || "http://localhost:13490"; + await page.request.put(`${apiBase}/api/v1/user/analytics`, { + headers: { Authorization: `Bearer ${token}` }, + data: { enabled: false }, + }); + + // Now navigate to "/" - consent guard is satisfied + await page.goto("/"); await expect(page).toHaveURL("/"); // Save storage state (includes localStorage with the token) diff --git a/tests/e2e/rbac-full.spec.ts b/tests/e2e/rbac-full.spec.ts index 1ed20429..e5db85e2 100644 --- a/tests/e2e/rbac-full.spec.ts +++ b/tests/e2e/rbac-full.spec.ts @@ -85,6 +85,19 @@ async function createUserWithRole( if (!changeRes.ok) { throw new Error(`Failed to clear mustChangePassword for ${username}: ${changeRes.status}`); } + + // Re-login (change-password invalidates sessions) and dismiss analytics consent + const reLogin = await fetch(`${API}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + const reLoginData = await reLogin.json(); + await fetch(`${API}/api/v1/user/analytics`, { + method: "PUT", + headers: authJson(reLoginData.token), + body: JSON.stringify({ enabled: false }), + }); } /** Delete a user by username if it exists. */ diff --git a/tests/e2e/rbac.spec.ts b/tests/e2e/rbac.spec.ts index b76091ff..90b0c29d 100644 --- a/tests/e2e/rbac.spec.ts +++ b/tests/e2e/rbac.spec.ts @@ -68,6 +68,19 @@ async function ensureTestUser(adminToken: string): Promise { if (!changeRes.ok) { throw new Error(`Failed to clear mustChangePassword: ${changeRes.status}`); } + + // Re-login (change-password invalidates sessions) and dismiss analytics consent + const reLogin = await fetch(`${API}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: TEST_USER, password: TEST_PASSWORD }), + }); + const reLoginData = await reLogin.json(); + await fetch(`${API}/api/v1/user/analytics`, { + method: "PUT", + headers: authJson(reLoginData.token), + body: JSON.stringify({ enabled: false }), + }); } /** Delete the test user if it exists. */ @@ -204,6 +217,19 @@ base.describe("RBAC - Editor sees collaborative tabs", () => { newPassword: "EditorTest1", }), }); + + // Re-login and dismiss analytics consent + const reLogin = await fetch(`${API}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: "editortest", password: "EditorTest1" }), + }); + const reLoginData = await reLogin.json(); + await fetch(`${API}/api/v1/user/analytics`, { + method: "PUT", + headers: authJson(reLoginData.token), + body: JSON.stringify({ enabled: false }), + }); }); base.afterAll(async () => {