Fas 1a: product analytics schema + GDPR controls
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user