mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(test): repair integration suite after analytics column/endpoint removal (#340)
* fix(test): repair integration suite after analytics column/endpoint removal #336 moved analytics to a build-time bake: migration 0005 dropped the users.analytics_enabled and analytics_consent_* columns and removed the PUT /api/v1/user/analytics endpoint. Two integration tests were left referencing the old shape and went red on main (13 failures): - migrate-from-sqlite.test.ts built 1.x SQLite fixtures whose users table declared the analytics columns. The generic SELECT *-based importer then tried to INSERT them into the 2.0 target, which no longer has those columns, failing with Postgres 42703 and rolling back the whole import (cascading to all 12 assertions). 1.x never had analytics columns, so the fixtures are corrected to drop them. Also removed the now-dead analytics entries from the importer's TS/BOOL conversion sets. - analytics.test.ts asserted the removed PUT endpoint returns 404 but sent the request unauthenticated, so the global auth preHandler answered 401 first. It now authenticates, reaching Fastify's not-found handler (404). Also removed the stale /api/v1/user/analytics path from openapi.yaml. Verified locally: full platform integration bucket 1029 passed / 0 failed; monorepo typecheck clean. * test(e2e): drop orphaned analytics-consent dismissal calls #336 deleted the entire analytics consent system (consent page, consent module, and PUT /api/v1/user/analytics), but six tests/e2e files still PUT to that removed endpoint to 'dismiss analytics consent.' The calls were silent no-ops (Playwright request.put / fetch don't throw on 4xx), so they passed while hitting a dead route. There is no consent prompt to dismiss anymore, so remove the calls: - auth.setup.ts / qa-auth.setup.ts: keep the waitForFunction that syncs on login completion, drop the now-unused token capture, the dead PUT, and the stale 'consent guard' comments. - rbac / rbac-full / gui-settings-rbac / gui-settings-expanded specs: the re-login blocks existed solely to obtain a token for the PUT (reLoginData was used nowhere else and the block was the tail of each helper), so remove the whole block. The meaningful create-user/login/change-password work is untouched. Verified: no /api/v1/user/analytics refs remain in tests/e2e; biome clean (no unused vars).
This commit is contained in:
@@ -8,17 +8,9 @@ export interface MigrationResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// columns storing epoch-seconds integers in 1.x
|
// columns storing epoch-seconds integers in 1.x
|
||||||
const TS = new Set([
|
const TS = new Set(["created_at", "updated_at", "expires_at", "completed_at", "last_used_at"]);
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"expires_at",
|
|
||||||
"completed_at",
|
|
||||||
"last_used_at",
|
|
||||||
"analytics_consent_shown_at",
|
|
||||||
"analytics_consent_remind_at",
|
|
||||||
]);
|
|
||||||
// columns storing 0/1 booleans in 1.x
|
// columns storing 0/1 booleans in 1.x
|
||||||
const BOOL = new Set(["must_change_password", "analytics_enabled", "is_builtin"]);
|
const BOOL = new Set(["must_change_password", "is_builtin"]);
|
||||||
// per-table columns whose values must be cast to jsonb in the INSERT
|
// per-table columns whose values must be cast to jsonb in the INSERT
|
||||||
const JSONB: Record<string, Set<string>> = {
|
const JSONB: Record<string, Set<string>> = {
|
||||||
jobs: new Set(["settings", "input_refs", "output_refs", "progress", "error"]),
|
jobs: new Set(["settings", "input_refs", "output_refs", "progress", "error"]),
|
||||||
|
|||||||
@@ -6440,49 +6440,6 @@ paths:
|
|||||||
instanceId:
|
instanceId:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
/api/v1/user/analytics:
|
|
||||||
put:
|
|
||||||
operationId: updateAnalyticsConsent
|
|
||||||
tags: [Analytics]
|
|
||||||
summary: Update user analytics consent
|
|
||||||
description: |
|
|
||||||
Set the authenticated user's analytics consent preference, or snooze
|
|
||||||
the consent prompt for 7 days with remindLater.
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
enabled:
|
|
||||||
type: boolean
|
|
||||||
description: Whether the user consents to analytics
|
|
||||||
remindLater:
|
|
||||||
type: boolean
|
|
||||||
description: Snooze the consent prompt for 7 days
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Consent updated
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
ok:
|
|
||||||
type: boolean
|
|
||||||
analyticsEnabled:
|
|
||||||
type: boolean
|
|
||||||
nullable: true
|
|
||||||
"401":
|
|
||||||
description: Authentication required
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/UnauthorizedError"
|
|
||||||
|
|
||||||
# ─── Features ────────────────────────────────────────────────────────────
|
# ─── Features ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/api/v1/features:
|
/api/v1/features:
|
||||||
|
|||||||
+4
-12
@@ -14,23 +14,15 @@ 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 login to complete and grab the token in one step
|
// Wait for login to complete (the token lands in localStorage)
|
||||||
const handle = await page.waitForFunction(() => localStorage.getItem("snapotter-token"), null, {
|
await page.waitForFunction(() => localStorage.getItem("snapotter-token"), null, {
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
});
|
});
|
||||||
const token = await handle.jsonValue();
|
|
||||||
|
|
||||||
// Dismiss analytics consent via API so it won't block any test
|
// Navigate to "/" and let any client-side redirect settle
|
||||||
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
|
|
||||||
// Use waitUntil: "domcontentloaded" to avoid racing with client-side redirects
|
// Use waitUntil: "domcontentloaded" to avoid racing with client-side redirects
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
// Wait for the URL to settle (app may redirect through consent/auth guards)
|
// Wait for the URL to settle (app may redirect through auth guards)
|
||||||
await page.waitForURL((url) => url.pathname === "/", { timeout: 30_000 }).catch(() => {});
|
await page.waitForURL((url) => url.pathname === "/", { timeout: 30_000 }).catch(() => {});
|
||||||
await page.waitForLoadState("load");
|
await page.waitForLoadState("load");
|
||||||
|
|
||||||
|
|||||||
@@ -107,19 +107,6 @@ async function createReadyUser(
|
|||||||
headers: authJson(loginData.token),
|
headers: authJson(loginData.token),
|
||||||
body: JSON.stringify({ currentPassword: password, newPassword: password }),
|
body: JSON.stringify({ currentPassword: password, newPassword: password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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, 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. */
|
||||||
|
|||||||
@@ -62,19 +62,6 @@ async function createReadyUser(
|
|||||||
headers: authJson(loginData.token),
|
headers: authJson(loginData.token),
|
||||||
body: JSON.stringify({ currentPassword: password, newPassword: password }),
|
body: JSON.stringify({ currentPassword: password, newPassword: password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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, 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. */
|
||||||
|
|||||||
@@ -13,18 +13,10 @@ setup("authenticate for QA", 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();
|
||||||
|
|
||||||
const handle = await page.waitForFunction(() => localStorage.getItem("snapotter-token"), null, {
|
// Wait for login to complete (the token lands in localStorage)
|
||||||
|
await page.waitForFunction(() => localStorage.getItem("snapotter-token"), null, {
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
});
|
});
|
||||||
const token = await handle.jsonValue();
|
|
||||||
|
|
||||||
// Dismiss analytics consent via API (use same baseURL)
|
|
||||||
await page.request
|
|
||||||
.put("/api/v1/user/analytics", {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
data: { enabled: false },
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
await page.waitForURL((url) => url.pathname === "/", { timeout: 15_000 }).catch(() => {});
|
await page.waitForURL((url) => url.pathname === "/", { timeout: 15_000 }).catch(() => {});
|
||||||
|
|||||||
@@ -86,19 +86,6 @@ 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. */
|
||||||
|
|||||||
@@ -69,19 +69,6 @@ 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. */
|
||||||
@@ -218,19 +205,6 @@ 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 () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import { buildTestApp, type TestApp } from "../test-server.js";
|
import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js";
|
||||||
|
|
||||||
let testApp: TestApp;
|
let testApp: TestApp;
|
||||||
|
|
||||||
@@ -64,9 +64,14 @@ describe("GET /api/v1/config/analytics", () => {
|
|||||||
|
|
||||||
describe("PUT /api/v1/user/analytics (removed)", () => {
|
describe("PUT /api/v1/user/analytics (removed)", () => {
|
||||||
it("returns 404 (endpoint no longer exists)", async () => {
|
it("returns 404 (endpoint no longer exists)", async () => {
|
||||||
|
// Authenticate first: the global auth preHandler answers unauthenticated
|
||||||
|
// requests with 401 before routing, so only an authenticated request can
|
||||||
|
// reach Fastify's not-found handler and prove the route is gone.
|
||||||
|
const token = await loginAsAdmin(testApp.app);
|
||||||
const res = await testApp.app.inject({
|
const res = await testApp.app.inject({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/api/v1/user/analytics",
|
url: "/api/v1/user/analytics",
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
payload: { enabled: true },
|
payload: { enabled: true },
|
||||||
});
|
});
|
||||||
expect(res.statusCode).toBe(404);
|
expect(res.statusCode).toBe(404);
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ function buildFixtureSqlite(path: string): void {
|
|||||||
CREATE TABLE users (id text PRIMARY KEY, username text NOT NULL, password_hash text,
|
CREATE TABLE users (id text PRIMARY KEY, username text NOT NULL, password_hash text,
|
||||||
role text NOT NULL DEFAULT 'user', team text NOT NULL DEFAULT 'Default',
|
role text NOT NULL DEFAULT 'user', team text NOT NULL DEFAULT 'Default',
|
||||||
must_change_password integer NOT NULL DEFAULT 1, auth_provider text NOT NULL DEFAULT 'local',
|
must_change_password integer NOT NULL DEFAULT 1, auth_provider text NOT NULL DEFAULT 'local',
|
||||||
external_id text, email text, created_at integer NOT NULL, updated_at integer NOT NULL,
|
external_id text, email text, created_at integer NOT NULL, updated_at integer NOT NULL);
|
||||||
analytics_enabled integer, analytics_consent_shown_at integer, analytics_consent_remind_at integer);
|
|
||||||
CREATE TABLE teams (id text PRIMARY KEY, name text NOT NULL, created_at integer NOT NULL);
|
CREATE TABLE teams (id text PRIMARY KEY, name text NOT NULL, created_at integer NOT NULL);
|
||||||
CREATE TABLE settings ("key" text PRIMARY KEY, value text NOT NULL, updated_at integer NOT NULL);
|
CREATE TABLE settings ("key" text PRIMARY KEY, value text NOT NULL, updated_at integer NOT NULL);
|
||||||
CREATE TABLE roles (id text PRIMARY KEY, name text NOT NULL, description text NOT NULL DEFAULT '',
|
CREATE TABLE roles (id text PRIMARY KEY, name text NOT NULL, description text NOT NULL DEFAULT '',
|
||||||
@@ -40,8 +39,8 @@ function buildFixtureSqlite(path: string): void {
|
|||||||
`);
|
`);
|
||||||
const now = 1750000000; // seconds epoch, as 1.x stored
|
const now = 1750000000; // seconds epoch, as 1.x stored
|
||||||
s.prepare(
|
s.prepare(
|
||||||
"INSERT INTO users (id, username, password_hash, must_change_password, created_at, updated_at, analytics_enabled, analytics_consent_shown_at) VALUES (?,?,?,?,?,?,?,?)",
|
"INSERT INTO users (id, username, password_hash, must_change_password, created_at, updated_at) VALUES (?,?,?,?,?,?)",
|
||||||
).run("u1", "alice", "hash", 0, now, now, 1, null);
|
).run("u1", "alice", "hash", 0, now, now);
|
||||||
s.prepare("INSERT INTO teams (id, name, created_at) VALUES (?,?,?)").run("t1", "Legal", now);
|
s.prepare("INSERT INTO teams (id, name, created_at) VALUES (?,?,?)").run("t1", "Legal", now);
|
||||||
s.prepare('INSERT INTO settings ("key", value, updated_at) VALUES (?,?,?)').run(
|
s.prepare('INSERT INTO settings ("key", value, updated_at) VALUES (?,?,?)').run(
|
||||||
"cookieSecret",
|
"cookieSecret",
|
||||||
@@ -98,8 +97,6 @@ describe("migrate-from-sqlite", () => {
|
|||||||
const [user] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u1'`)).rows;
|
const [user] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u1'`)).rows;
|
||||||
expect(user.username).toBe("alice");
|
expect(user.username).toBe("alice");
|
||||||
expect(user.must_change_password).toBe(false); // 0 became boolean false
|
expect(user.must_change_password).toBe(false); // 0 became boolean false
|
||||||
expect(user.analytics_enabled).toBe(true); // 1 became boolean true
|
|
||||||
expect(user.analytics_consent_shown_at).toBeNull(); // explicit NULL preserved
|
|
||||||
expect(new Date(user.created_at as string).getTime()).toBe(1750000000 * 1000); // seconds became timestamptz
|
expect(new Date(user.created_at as string).getTime()).toBe(1750000000 * 1000); // seconds became timestamptz
|
||||||
const [pipeline] = (await db.execute(sql`SELECT * FROM pipelines WHERE id = 'p1'`)).rows;
|
const [pipeline] = (await db.execute(sql`SELECT * FROM pipelines WHERE id = 'p1'`)).rows;
|
||||||
expect((pipeline.steps as Array<{ toolId: string }>)[0].toolId).toBe("compress"); // text JSON became jsonb
|
expect((pipeline.steps as Array<{ toolId: string }>)[0].toolId).toBe("compress"); // text JSON became jsonb
|
||||||
@@ -160,8 +157,7 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
CREATE TABLE users (id text PRIMARY KEY, username text NOT NULL, password_hash text,
|
CREATE TABLE users (id text PRIMARY KEY, username text NOT NULL, password_hash text,
|
||||||
role text NOT NULL DEFAULT 'user', team text NOT NULL DEFAULT 'Default',
|
role text NOT NULL DEFAULT 'user', team text NOT NULL DEFAULT 'Default',
|
||||||
must_change_password integer NOT NULL DEFAULT 1, auth_provider text NOT NULL DEFAULT 'local',
|
must_change_password integer NOT NULL DEFAULT 1, auth_provider text NOT NULL DEFAULT 'local',
|
||||||
external_id text, email text, created_at integer NOT NULL, updated_at integer NOT NULL,
|
external_id text, email text, created_at integer NOT NULL, updated_at integer NOT NULL);
|
||||||
analytics_enabled integer, analytics_consent_shown_at integer, analytics_consent_remind_at integer);
|
|
||||||
CREATE TABLE teams (id text PRIMARY KEY, name text NOT NULL, created_at integer NOT NULL);
|
CREATE TABLE teams (id text PRIMARY KEY, name text NOT NULL, created_at integer NOT NULL);
|
||||||
CREATE TABLE settings ("key" text PRIMARY KEY, value text NOT NULL, updated_at integer NOT NULL);
|
CREATE TABLE settings ("key" text PRIMARY KEY, value text NOT NULL, updated_at integer NOT NULL);
|
||||||
CREATE TABLE roles (id text PRIMARY KEY, name text NOT NULL, description text NOT NULL DEFAULT '',
|
CREATE TABLE roles (id text PRIMARY KEY, name text NOT NULL, description text NOT NULL DEFAULT '',
|
||||||
@@ -193,9 +189,8 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
// ── Users: multiple users with diverse boolean/null combos ──
|
// ── Users: multiple users with diverse boolean/null combos ──
|
||||||
const insU = s.prepare(
|
const insU = s.prepare(
|
||||||
`INSERT INTO users (id, username, password_hash, role, team, must_change_password,
|
`INSERT INTO users (id, username, password_hash, role, team, must_change_password,
|
||||||
auth_provider, external_id, email, created_at, updated_at,
|
auth_provider, external_id, email, created_at, updated_at)
|
||||||
analytics_enabled, analytics_consent_shown_at, analytics_consent_remind_at)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
||||||
);
|
);
|
||||||
insU.run(
|
insU.run(
|
||||||
"u-admin",
|
"u-admin",
|
||||||
@@ -209,9 +204,6 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
"admin@example.com",
|
"admin@example.com",
|
||||||
t1,
|
t1,
|
||||||
t1,
|
t1,
|
||||||
1,
|
|
||||||
t1,
|
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
insU.run(
|
insU.run(
|
||||||
"u-editor",
|
"u-editor",
|
||||||
@@ -225,9 +217,6 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
null,
|
null,
|
||||||
t2,
|
t2,
|
||||||
t2,
|
t2,
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
insU.run(
|
insU.run(
|
||||||
"u-oidc",
|
"u-oidc",
|
||||||
@@ -241,9 +230,6 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
"sso@corp.com",
|
"sso@corp.com",
|
||||||
t3,
|
t3,
|
||||||
t3,
|
t3,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
t3,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Teams ──
|
// ── Teams ──
|
||||||
@@ -478,29 +464,26 @@ describe("migrate-from-sqlite (representative 1.x database)", () => {
|
|||||||
expect(result.tables.user_files).toBe(4);
|
expect(result.tables.user_files).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("boolean conversions: 0 -> false, 1 -> true, NULL -> null", async () => {
|
it("boolean conversions: 0 -> false, 1 -> true", async () => {
|
||||||
const users = (await db.execute(sql`SELECT * FROM users ORDER BY id`)).rows;
|
const users = (await db.execute(sql`SELECT * FROM users ORDER BY id`)).rows;
|
||||||
// u-admin: must_change_password=0 -> false, analytics_enabled=1 -> true
|
// u-admin: must_change_password=0 -> false
|
||||||
const admin = users.find((u) => u.id === "u-admin");
|
const admin = users.find((u) => u.id === "u-admin");
|
||||||
expect(admin?.must_change_password).toBe(false);
|
expect(admin?.must_change_password).toBe(false);
|
||||||
expect(admin?.analytics_enabled).toBe(true);
|
// u-editor: must_change_password=1 -> true
|
||||||
// u-editor: must_change_password=1 -> true, analytics_enabled=0 -> false
|
|
||||||
const editor = users.find((u) => u.id === "u-editor");
|
const editor = users.find((u) => u.id === "u-editor");
|
||||||
expect(editor?.must_change_password).toBe(true);
|
expect(editor?.must_change_password).toBe(true);
|
||||||
expect(editor?.analytics_enabled).toBe(false);
|
// u-oidc: must_change_password=0 -> false
|
||||||
// u-oidc: analytics_enabled=NULL -> null
|
|
||||||
const oidc = users.find((u) => u.id === "u-oidc");
|
const oidc = users.find((u) => u.id === "u-oidc");
|
||||||
expect(oidc?.analytics_enabled).toBeNull();
|
expect(oidc?.must_change_password).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("timestamp conversions: epoch seconds -> timestamptz", async () => {
|
it("timestamp conversions: epoch seconds -> timestamptz", async () => {
|
||||||
const [admin] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u-admin'`)).rows;
|
const [admin] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u-admin'`)).rows;
|
||||||
expect(new Date(admin.created_at as string).getTime()).toBe(1748000000 * 1000);
|
expect(new Date(admin.created_at as string).getTime()).toBe(1748000000 * 1000);
|
||||||
// NULL timestamps stay null
|
expect(new Date(admin.updated_at as string).getTime()).toBe(1748000000 * 1000);
|
||||||
expect(admin.analytics_consent_remind_at).toBeNull();
|
// A different row's distinct timestamp also converts
|
||||||
// Non-null timestamp
|
|
||||||
const [oidc] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u-oidc'`)).rows;
|
const [oidc] = (await db.execute(sql`SELECT * FROM users WHERE id = 'u-oidc'`)).rows;
|
||||||
expect(new Date(oidc.analytics_consent_remind_at as string).getTime()).toBe(1748200000 * 1000);
|
expect(new Date(oidc.created_at as string).getTime()).toBe(1748200000 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("JSON column conversions: text -> jsonb", async () => {
|
it("JSON column conversions: text -> jsonb", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user