fix: E2E test isolation and analytics consent bugs

- Isolate test web server on port 2349 so E2E tests run alongside Docker
- Use fresh SQLite database per test run via DB_PATH, fixing missing
  roles/audit_log tables from stale migration state
- Fix analytics consent redirect loop when ANALYTICS_ENABLED=false by
  auto-declining consent instead of bare redirect
- Dismiss analytics consent via API in auth setup and RBAC test user
  creation so the consent page never blocks UI tests
- Make Vite port and proxy target configurable via env vars
This commit is contained in:
ashim-hq
2026-04-24 01:24:15 +08:00
parent fe384ddfe1
commit a062aa9e5f
6 changed files with 72 additions and 11 deletions
@@ -8,7 +8,8 @@ const t = en.analytics;
export function AnalyticsConsentPage() { export function AnalyticsConsentPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { config, configLoaded, fetchConfig, acceptAnalytics, remindLater } = useAnalyticsStore(); const { config, configLoaded, fetchConfig, acceptAnalytics, declineAnalytics, remindLater } =
useAnalyticsStore();
useEffect(() => { useEffect(() => {
fetchConfig(); fetchConfig();
@@ -16,9 +17,9 @@ export function AnalyticsConsentPage() {
useEffect(() => { useEffect(() => {
if (configLoaded && !config?.enabled) { if (configLoaded && !config?.enabled) {
navigate("/", { replace: true }); declineAnalytics().then(() => navigate("/", { replace: true }));
} }
}, [configLoaded, config, navigate]); }, [configLoaded, config, navigate, declineAnalytics]);
if (!configLoaded) { if (!configLoaded) {
return ( return (
+2 -2
View File
@@ -12,9 +12,9 @@ export default defineConfig({
dedupe: ["react", "react-dom"], dedupe: ["react", "react-dom"],
}, },
server: { server: {
port: 1349, port: Number(process.env.PORT) || 1349,
proxy: { proxy: {
"/api": "http://localhost:13490", "/api": process.env.VITE_API_URL || "http://localhost:13490",
}, },
}, },
build: { build: {
+12 -4
View File
@@ -2,6 +2,9 @@ import path from "node:path";
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
const authFile = path.join(__dirname, "test-results", ".auth", "user.json"); 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({ export default defineConfig({
testDir: "./tests/e2e", testDir: "./tests/e2e",
@@ -20,7 +23,7 @@ export default defineConfig({
workers: 1, workers: 1,
reporter: "html", reporter: "html",
use: { use: {
baseURL: "http://localhost:1349", baseURL: `http://localhost:${TEST_WEB_PORT}`,
screenshot: "only-on-failure", screenshot: "only-on-failure",
trace: "retain-on-failure", trace: "retain-on-failure",
}, },
@@ -40,23 +43,28 @@ export default defineConfig({
], ],
webServer: [ 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, port: 13490,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
env: { env: {
PORT: "13490",
AUTH_ENABLED: "true", AUTH_ENABLED: "true",
DEFAULT_USERNAME: "admin", DEFAULT_USERNAME: "admin",
DEFAULT_PASSWORD: "admin", DEFAULT_PASSWORD: "admin",
RATE_LIMIT_PER_MIN: "50000", RATE_LIMIT_PER_MIN: "50000",
SKIP_MUST_CHANGE_PASSWORD: "true", SKIP_MUST_CHANGE_PASSWORD: "true",
ANALYTICS_ENABLED: "false",
DB_PATH: testDbPath,
}, },
timeout: 30_000, timeout: 30_000,
}, },
{ {
command: "pnpm --filter @ashim/web dev", command: "pnpm --filter @ashim/web dev",
port: 1349, port: TEST_WEB_PORT,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
env: {
PORT: String(TEST_WEB_PORT),
VITE_API_URL: "http://localhost:13490",
},
timeout: 30_000, timeout: 30_000,
}, },
], ],
+15 -2
View File
@@ -14,8 +14,21 @@ setup("authenticate", async ({ page }) => {
await page.getByLabel("Password").fill("admin"); await page.getByLabel("Password").fill("admin");
await page.getByRole("button", { name: /login/i }).click(); await page.getByRole("button", { name: /login/i }).click();
// Wait for the full-page redirect to "/" // Wait for login to complete and grab the token in one step
await page.waitForURL("/", { timeout: 15_000 }); 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("/"); await expect(page).toHaveURL("/");
// Save storage state (includes localStorage with the token) // Save storage state (includes localStorage with the token)
+13
View File
@@ -85,6 +85,19 @@ async function createUserWithRole(
if (!changeRes.ok) { if (!changeRes.ok) {
throw new Error(`Failed to clear mustChangePassword for ${username}: ${changeRes.status}`); 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. */ /** Delete a user by username if it exists. */
+26
View File
@@ -68,6 +68,19 @@ async function ensureTestUser(adminToken: string): Promise<void> {
if (!changeRes.ok) { if (!changeRes.ok) {
throw new Error(`Failed to clear mustChangePassword: ${changeRes.status}`); 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. */ /** Delete the test user if it exists. */
@@ -204,6 +217,19 @@ base.describe("RBAC - Editor sees collaborative tabs", () => {
newPassword: "EditorTest1", 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 () => { base.afterAll(async () => {