118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
/**
|
||
* GDPR helpers for product analytics – integration tests.
|
||
* Assumes DATABASE_URL points to a writable database.
|
||
*/
|
||
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
||
import { eq } from "drizzle-orm";
|
||
import {
|
||
createDatabase,
|
||
closeDatabase,
|
||
schema,
|
||
isAnalyticsOptedIn,
|
||
deleteUserAnalyticsEvents,
|
||
} from "@app/database";
|
||
|
||
const TEST_USER_EMAIL = "analytics-gdpr-test@example.invalid";
|
||
|
||
describe("analytics GDPR helpers", () => {
|
||
const { db, pool } = createDatabase();
|
||
let userId: string;
|
||
|
||
beforeAll(async () => {
|
||
// Clean up any stale test user.
|
||
await db.delete(schema.users).where(eq(schema.users.email, TEST_USER_EMAIL));
|
||
const [user] = await db
|
||
.insert(schema.users)
|
||
.values({
|
||
email: TEST_USER_EMAIL,
|
||
displayName: "Analytics Test",
|
||
locale: "sv-SE",
|
||
})
|
||
.returning({ id: schema.users.id });
|
||
userId = user!.id;
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId));
|
||
await db.delete(schema.users).where(eq(schema.users.id, userId));
|
||
await closeDatabase();
|
||
});
|
||
|
||
it("missing consent row → opted in (legitimate interest, opt-out)", async () => {
|
||
await db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId));
|
||
expect(await isAnalyticsOptedIn(db, userId)).toBe(true);
|
||
});
|
||
|
||
it("explicit granted → opted in", async () => {
|
||
await db
|
||
.insert(schema.userConsents)
|
||
.values({
|
||
userId,
|
||
kind: "product_analytics",
|
||
status: "granted",
|
||
grantedAt: new Date(),
|
||
updatedAt: new Date(),
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: [schema.userConsents.userId, schema.userConsents.kind],
|
||
set: { status: "granted", revokedAt: null, updatedAt: new Date() },
|
||
});
|
||
expect(await isAnalyticsOptedIn(db, userId)).toBe(true);
|
||
});
|
||
|
||
it("explicit revoked → opted out", async () => {
|
||
const now = new Date();
|
||
await db
|
||
.insert(schema.userConsents)
|
||
.values({
|
||
userId,
|
||
kind: "product_analytics",
|
||
status: "revoked",
|
||
revokedAt: now,
|
||
updatedAt: now,
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: [schema.userConsents.userId, schema.userConsents.kind],
|
||
set: { status: "revoked", revokedAt: now, updatedAt: now },
|
||
});
|
||
expect(await isAnalyticsOptedIn(db, userId)).toBe(false);
|
||
});
|
||
|
||
it("deleteUserAnalyticsEvents removes only the target user's events", async () => {
|
||
// Insert a dummy event for the test user and another user.
|
||
const [otherUser] = await db
|
||
.insert(schema.users)
|
||
.values({
|
||
email: "other-analytics-test@example.invalid",
|
||
displayName: "Other",
|
||
locale: "sv-SE",
|
||
})
|
||
.returning({ id: schema.users.id });
|
||
|
||
await db.insert(schema.productAnalyticsEvents).values({
|
||
userId,
|
||
eventName: "app_first_open",
|
||
anonymousId: "anon-test",
|
||
});
|
||
await db.insert(schema.productAnalyticsEvents).values({
|
||
userId: otherUser!.id,
|
||
eventName: "app_first_open",
|
||
anonymousId: "anon-other",
|
||
});
|
||
|
||
const deleted = await deleteUserAnalyticsEvents(db, userId);
|
||
expect(deleted).toBe(1);
|
||
|
||
const remaining = await db
|
||
.select({ id: schema.productAnalyticsEvents.id })
|
||
.from(schema.productAnalyticsEvents)
|
||
.where(eq(schema.productAnalyticsEvents.userId, otherUser!.id));
|
||
expect(remaining).toHaveLength(1);
|
||
|
||
await db
|
||
.delete(schema.productAnalyticsEvents)
|
||
.where(eq(schema.productAnalyticsEvents.userId, otherUser!.id));
|
||
await db.delete(schema.users).where(eq(schema.users.id, otherUser!.id));
|
||
});
|
||
});
|