Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
Replace the customers/data_request, customers/redact, and shop/redact stubs with real logic in app/services/gdpr.server.ts: compile a customer's Booking history, anonymize customerEmail/customerPhone on redact, and purge every shopDomain-scoped row on shop uninstall (Booking before Location, relying on Location's cascade for SlotTemplate/SlotOverride/BlackoutDate/Zone/Rate). Covered by a new tests/integration/gdpr.test.ts against live Postgres to verify the FK deletion order actually works, not just typechecks. Still commented out in shopify.app.toml pending Protected customer data access approval in the Partner Dashboard — unrelated to code readiness. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
import db from "../../app/db.server";
|
|
import { compileCustomerData, purgeShopData, redactCustomerData } from "../../app/services/gdpr.server";
|
|
|
|
const shopDomain = "gdpr-integration-test.myshopify.com";
|
|
|
|
async function cleanup() {
|
|
await db.booking.deleteMany({ where: { shopDomain } });
|
|
await db.location.deleteMany({ where: { shopDomain } });
|
|
await db.shop.deleteMany({ where: { shopDomain } });
|
|
await db.session.deleteMany({ where: { shop: shopDomain } });
|
|
}
|
|
|
|
async function seed() {
|
|
await db.shop.create({ data: { shopDomain } });
|
|
const location = await db.location.create({
|
|
data: { shopDomain, name: "Test Location", address: "", timezone: "America/Toronto" },
|
|
});
|
|
const zone = await db.zone.create({
|
|
data: { shopDomain, locationId: location.id, name: "Local", type: "postal", postalCodes: ["M5V"] },
|
|
});
|
|
await db.rate.create({
|
|
data: { shopDomain, method: "LOCAL_DELIVERY", zoneId: zone.id, name: "Standard", priceCents: 500, keyedBy: "zone" },
|
|
});
|
|
await db.slotTemplate.create({
|
|
data: { shopDomain, locationId: location.id, method: "PICKUP", weekday: 1, startMin: 540, endMin: 600, capacity: 5 },
|
|
});
|
|
const booking = await db.booking.create({
|
|
data: {
|
|
shopDomain,
|
|
orderId: "gid://shopify/Order/gdpr-1",
|
|
locationId: location.id,
|
|
method: "PICKUP",
|
|
slotStart: new Date("2026-08-25T13:00:00.000Z"),
|
|
slotEnd: new Date("2026-08-25T13:30:00.000Z"),
|
|
customerEmail: "shopper@example.com",
|
|
customerPhone: "+15551234567",
|
|
},
|
|
});
|
|
return { location, zone, booking };
|
|
}
|
|
|
|
describe("GDPR handlers", () => {
|
|
beforeEach(cleanup);
|
|
afterAll(async () => {
|
|
await cleanup();
|
|
await db.$disconnect();
|
|
});
|
|
|
|
it("compileCustomerData finds Bookings matching the customer's email", async () => {
|
|
await seed();
|
|
const results = await compileCustomerData(shopDomain, { email: "shopper@example.com" });
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].orderId).toBe("gid://shopify/Order/gdpr-1");
|
|
});
|
|
|
|
it("compileCustomerData finds Bookings matching the customer's phone", async () => {
|
|
await seed();
|
|
const results = await compileCustomerData(shopDomain, { phone: "+15551234567" });
|
|
expect(results).toHaveLength(1);
|
|
});
|
|
|
|
it("compileCustomerData returns nothing for an unrelated customer", async () => {
|
|
await seed();
|
|
const results = await compileCustomerData(shopDomain, { email: "someone-else@example.com" });
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
|
|
it("redactCustomerData anonymizes matching Bookings but keeps the row", async () => {
|
|
const { booking } = await seed();
|
|
const { count } = await redactCustomerData(shopDomain, { email: "shopper@example.com" });
|
|
expect(count).toBe(1);
|
|
|
|
const after = await db.booking.findUnique({ where: { id: booking.id } });
|
|
expect(after).not.toBeNull();
|
|
expect(after?.customerEmail).toBeNull();
|
|
expect(after?.customerPhone).toBeNull();
|
|
expect(after?.orderId).toBe("gid://shopify/Order/gdpr-1"); // booking history preserved
|
|
});
|
|
|
|
it("purgeShopData deletes every shopDomain-scoped row, respecting FK order", async () => {
|
|
const { location, zone, booking } = await seed();
|
|
|
|
await purgeShopData(shopDomain);
|
|
|
|
expect(await db.booking.findUnique({ where: { id: booking.id } })).toBeNull();
|
|
expect(await db.location.findUnique({ where: { id: location.id } })).toBeNull();
|
|
expect(await db.zone.findUnique({ where: { id: zone.id } })).toBeNull(); // cascaded from Location
|
|
expect(await db.rate.findMany({ where: { shopDomain } })).toHaveLength(0); // cascaded from Zone
|
|
expect(await db.slotTemplate.findMany({ where: { shopDomain } })).toHaveLength(0); // cascaded from Location
|
|
expect(await db.shop.findUnique({ where: { shopDomain } })).toBeNull();
|
|
});
|
|
});
|