Files
manyangles/apps/web/src/server/publishing.integration.test.ts
T

115 lines
9.1 KiB
TypeScript

import { expect, test } from "bun:test";
import { and, eq } from "drizzle-orm";
import { events, getDb, groups, guests, photos, photoJobs } from "@album/database";
import { deletePrefix, getObjectBuffer, photoObjectPrefix, headObject } from "@album/storage";
import { photosRouter } from "./api/routers/photos";
import { eventRouter } from "./api/routers/event";
import { guestRouter } from "./api/routers/guest";
import type { TrpcContext } from "./api/trpc";
// Opt in against a migrated development database; creates and removes its own event.
test.skipIf(process.env.PUBLISHING_INTEGRATION !== "1")("standalone notes, approval reset, hidden stats, and gallery policies", async () => {
const db = getDb();
if (!["localhost", "127.0.0.1"].includes(new URL(process.env.DATABASE_URL!).hostname)) throw new Error("Local database required");
const [group] = await db.select({ id: groups.id }).from(groups).limit(1);
if (!group) throw new Error("Seed a development group first");
const [event] = await db.insert(events).values({ groupId: group.id, title: "Publishing test", slug: `publishing-test-${crypto.randomUUID()}`, status: "published", uploadEnabled: false }).returning();
if (!event) throw new Error("Could not create test event");
let token: string | null = null;
const ctx: TrpcContext = { session: null, requestOrigin: "http://localhost:3000", clientIdentifier: "publishing-test", cookies: new Map(), activeGroupId: null,
guestTokenForEvent: () => token, setCookies: [], appendSetCookie: (value) => { token = decodeURIComponent(value.split(";")[0]!.split("=").slice(1).join("=")); } };
const guest = guestRouter.createCaller(ctx);
const publicEvent = eventRouter.createCaller(ctx);
let uploadedPhotoId: string | null = null;
try {
const result = await guest.ensure({ eventSlug: event.slug, note: "Private note without a photo" });
expect((await publicEvent.community(event.slug))).toEqual({ notes: [], stats: { photos: null, submitters: null, notes: null } });
await db.update(events).set({ notesPolicy: "automatic", showPhotoStats: true, showSubmitterStats: true, showNoteStats: true }).where(eq(events.id, event.id));
expect((await publicEvent.community(event.slug)).notes).toHaveLength(0);
await guest.ensure({ eventSlug: event.slug, note: "New automatic note" });
const automatic = await publicEvent.community(event.slug);
expect(automatic.notes).toHaveLength(1);
expect(automatic.stats).toEqual({ photos: 0, submitters: 1, notes: 1 });
expect(Object.keys(automatic.notes[0]!)).toEqual(["id", "displayName", "note"]);
await db.update(events).set({ notesPolicy: "approved" }).where(eq(events.id, event.id));
await guest.ensure({ eventSlug: event.slug, note: "Edited note requires approval" });
expect((await publicEvent.community(event.slug)).notes).toHaveLength(0);
await db.update(guests).set({ noteApproved: true }).where(and(eq(guests.eventId, event.id), eq(guests.id, result.guestId)));
expect((await publicEvent.community(event.slug)).notes).toHaveLength(1);
const future = new Date(Date.now() + 3600000);
const past = new Date(Date.now() - 3600000);
await db.update(events).set({ notesVisibleAt: future, galleryVisibleAt: future, publishAt: future }).where(eq(events.id, event.id));
await expect(publicEvent.community(event.slug)).rejects.toThrow();
await db.update(events).set({ publishAt: past }).where(eq(events.id, event.id));
expect((await publicEvent.community(event.slug)).notes).toHaveLength(0);
expect(await publicEvent.gallery(event.slug)).toEqual([]);
await db.update(events).set({ notesVisibleAt: past, galleryVisibleAt: past, submissionsOpenAt: future }).where(eq(events.id, event.id));
expect((await publicEvent.community(event.slug)).notes).toHaveLength(1);
await expect(guest.ensure({ eventSlug: event.slug, note: "Too early" })).rejects.toThrow();
await db.update(events).set({ submissionsOpenAt: past }).where(eq(events.id, event.id));
const emailOnly = guestRouter.createCaller({ ...ctx, guestTokenForEvent: () => null, appendSetCookie: () => {} });
const emailGuest = await emailOnly.ensure({ eventSlug: event.slug, email: "email-only@manyangles.test", notifyWhenReady: true });
const [saved] = await db.select({ note: guests.note, optIn: guests.notifyWhenReady }).from(guests).where(and(eq(guests.eventId, event.id), eq(guests.id, emailGuest.guestId)));
expect(saved).toEqual({ note: null, optIn: true });
await db.update(events).set({ galleryVisibleAt: null }).where(eq(events.id, event.id));
await db.update(events).set({ galleryPolicy: "automatic", uploadEnabled: true }).where(eq(events.id, event.id));
const submission = await guest.startSubmission({ eventSlug: event.slug });
const photoCaller = photosRouter.createCaller(ctx);
let original: Buffer | null = null;
if (process.env.REAL_UPLOAD_INTEGRATION === "1") {
const [demo] = await db.select({ id: events.id }).from(events).where(eq(events.slug, "demo"));
const [source] = await db.select({ key: photos.originalKey }).from(photos).where(and(eq(photos.eventId, demo!.id), eq(photos.processingStatus, "ready"))).limit(1);
original = await getObjectBuffer(source!.key);
}
const created = await photoCaller.create({ eventSlug: event.slug, submissionId: submission.submissionId, contentType: "image/jpeg", fileName: "test.jpg", byteSize: original?.length ?? 100 });
expect((await photoCaller.retryUpload({ eventId: event.id, photoId: created.photoId })).uploadUrl).toBeTruthy();
await expect(photosRouter.createCaller({ ...ctx, guestTokenForEvent: () => null }).retryUpload({ eventId: event.id, photoId: created.photoId })).rejects.toThrow("Guest session");
await expect(photoCaller.retryUpload({ eventId: crypto.randomUUID(), photoId: created.photoId })).rejects.toThrow();
await expect(photosRouter.createCaller({ ...ctx, guestTokenForEvent: () => null }).complete({ photoId: created.photoId })).rejects.toThrow("Guest session");
expect(await publicEvent.gallery(event.slug)).toEqual([]);
if (original) {
uploadedPhotoId = created.photoId;
expect((await fetch(created.uploadUrl, { method: "PUT", headers: { "Content-Type": "image/jpeg" }, body: new Uint8Array(original) })).ok).toBe(true);
await Promise.all([photoCaller.complete({ photoId: created.photoId }), photoCaller.complete({ photoId: created.photoId })]);
expect((await photoCaller.retryUpload({ eventId: event.id, photoId: created.photoId })).uploadUrl).toBeNull();
expect(await db.select({ id: photoJobs.id }).from(photoJobs).where(eq(photoJobs.photoId, created.photoId))).toHaveLength(1);
let ready = false;
for (let attempt = 0; attempt < 45; attempt++) {
const [photo] = await db.select().from(photos).where(and(eq(photos.eventId, event.id), eq(photos.id, created.photoId)));
if (photo?.processingStatus === "failed") throw new Error("Image processing failed");
if (photo?.processingStatus === "ready") {
expect(photo.displayKey).toBeTruthy();
expect(photo.thumbKey).toBeTruthy();
expect(Number((await headObject(photo.displayKey!))?.ContentLength)).toBeLessThan(original.length);
expect(Number((await headObject(photo.thumbKey!))?.ContentLength)).toBeLessThan(original.length);
ready = true;
break;
}
await Bun.sleep(500);
}
expect(ready).toBe(true);
} else {
// Fast policy-only test; opt in above for real storage and worker processing.
await db.update(photos).set({ processingStatus: "ready" }).where(and(eq(photos.eventId, event.id), eq(photos.id, created.photoId)));
}
expect(await publicEvent.gallery(event.slug)).toHaveLength(1);
await db.update(photos).set({ visibility: "pending" }).where(and(eq(photos.eventId, event.id), eq(photos.id, created.photoId)));
expect(await publicEvent.gallery(event.slug)).toEqual([]);
await db.update(photos).set({ visibility: "public" }).where(and(eq(photos.eventId, event.id), eq(photos.id, created.photoId)));
await db.update(events).set({ galleryPolicy: "approved" }).where(eq(events.id, event.id));
expect(await publicEvent.gallery(event.slug)).toEqual([]);
await db.update(events).set({ galleryReleasedAt: new Date() }).where(eq(events.id, event.id));
expect(await publicEvent.gallery(event.slug)).toHaveLength(1);
await db.update(events).set({ notesPolicy: "never", galleryPolicy: "never", galleryReleasedAt: new Date() }).where(eq(events.id, event.id));
expect((await publicEvent.community(event.slug)).notes).toHaveLength(0);
expect(await publicEvent.gallery(event.slug)).toEqual([]);
await db.update(events).set({ status: "closed" }).where(eq(events.id, event.id));
await expect(guest.ensure({ eventSlug: event.slug, note: "Closed" })).rejects.toThrow("Notes are closed");
await db.update(events).set({ status: "draft", publishAt: null }).where(eq(events.id, event.id));
await expect(publicEvent.community(event.slug)).rejects.toThrow();
} finally {
if (uploadedPhotoId) await deletePrefix(photoObjectPrefix(event.id, uploadedPhotoId));
await db.delete(events).where(eq(events.id, event.id));
}
}, 30000);