115 lines
9.1 KiB
TypeScript
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);
|