Add bulk gallery management and direct organizer uploads
This commit is contained in:
@@ -4,6 +4,7 @@ import { getDb, user, groups, guests, submissions, photos, events, eventMembersh
|
||||
import { groupRouter } from "./api/routers/group";
|
||||
import { managerRouter } from "./api/routers/manager";
|
||||
import type { TrpcContext } from "./api/trpc";
|
||||
import { deletePrefix, photoObjectPrefix, headObject, originalObjectKey } from "@album/storage";
|
||||
|
||||
test.skipIf(process.env.POLISH_INTEGRATION !== "1")("group ownership and event-scoped submission approval", async () => {
|
||||
if (!["localhost", "127.0.0.1"].includes(new URL(process.env.DATABASE_URL!).hostname)) throw new Error("Local database required");
|
||||
@@ -12,6 +13,7 @@ test.skipIf(process.env.POLISH_INTEGRATION !== "1")("group ownership and event-s
|
||||
const people = await db.insert(user).values([0, 1].map(n => ({ id: `${id}-${n}`, name: "Workflow test", email: `${id}-${n}@manyangles.test`, emailVerified: true }))).returning();
|
||||
const ctx = (n: number): TrpcContext => ({ session: { user: people[n]!, session: {} } as TrpcContext["session"], cookies: new Map(), activeGroupId: null, requestOrigin: "http://localhost:3000", clientIdentifier: id, guestTokenForEvent: () => null, setCookies: [], appendSetCookie: () => {} });
|
||||
let groupId: string | undefined;
|
||||
const cleanupUploads: { eventId: string; photoId: string }[] = [];
|
||||
try {
|
||||
const owner = groupRouter.createCaller(ctx(0));
|
||||
const group = await owner.create({ name: "Workflow test" });
|
||||
@@ -41,7 +43,40 @@ test.skipIf(process.env.POLISH_INTEGRATION !== "1")("group ownership and event-s
|
||||
const [preserved] = await db.select().from(photos).where(and(eq(photos.eventId, event!.id), eq(photos.id, privatePhoto!.id)));
|
||||
expect(preserved!.visibility).toBe("private");
|
||||
await expect(moderator.moderateSubmission({ ...input, visibility: "private" })).rejects.toThrow();
|
||||
const bulk = { eventId: event!.id, photoIds: [...result.map(photo => photo.id), privatePhoto!.id], action: "public" as const };
|
||||
const preview = await moderator.previewBulkPhotos(bulk);
|
||||
expect(preview.affected).toBe(2);
|
||||
expect(preview.results.find(row => row.photoId === privatePhoto!.id)?.status).toBe("skipped");
|
||||
await expect(moderator.bulkPhotos({ ...bulk, confirm: false as true })).rejects.toThrow();
|
||||
expect((await moderator.bulkPhotos({ ...bulk, confirm: true })).affected).toBe(2);
|
||||
expect((await moderator.bulkPhotos({ ...bulk, confirm: true })).affected).toBe(0);
|
||||
await expect(moderator.bulkPhotos({ ...bulk, action: "private", confirm: true })).rejects.toThrow();
|
||||
const [otherEvent] = await db.insert(events).values({ groupId, title: "Other event", slug: `${id}-other` }).returning();
|
||||
await db.insert(eventMemberships).values({ eventId: otherEvent!.id, userId: people[0]!.id, role: "owner" });
|
||||
const crossEvent = await manager.previewBulkPhotos({ ...bulk, eventId: otherEvent!.id });
|
||||
expect(crossEvent.affected).toBe(0);
|
||||
const deletion = await manager.previewBulkPhotos({ ...bulk, action: "delete" });
|
||||
expect(deletion.results.find(row => row.photoId === result.find(photo => photo.processingStatus === "processing")!.id)?.status).toBe("skipped");
|
||||
await expect(manager.bulkPhotos({ ...bulk, photoIds: [result[0]!.id, result[0]!.id], confirm: true })).rejects.toThrow();
|
||||
await expect(moderator.createGalleryPhotos({ eventId: event!.id, files: [{ fileName: "test.png", contentType: "image/png", byteSize: 5 }] })).rejects.toThrow();
|
||||
expect((await moderator.photos({ eventId: event!.id, limit: 1, visibility: "private" })).length).toBe(0);
|
||||
expect((await manager.photos({ eventId: event!.id, limit: 1 })).length).toBe(1);
|
||||
if (process.env.GALLERY_STORAGE_INTEGRATION === "1") {
|
||||
if (!["localhost", "127.0.0.1"].includes(new URL(process.env.S3_ENDPOINT!).hostname)) throw new Error("Local object storage required");
|
||||
const bytes = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aRZkAAAAASUVORK5CYII=", "base64");
|
||||
const uploads = await manager.createGalleryPhotos({ eventId: event!.id, files: [{ fileName: "test.png", contentType: "image/png", byteSize: bytes.length }] });
|
||||
for (const upload of uploads) cleanupUploads.push({ eventId: event!.id, photoId: upload.photoId });
|
||||
expect((await fetch(uploads[0]!.uploadUrl, { method: "PUT", headers: { "Content-Type": "image/png" }, body: bytes })).ok).toBe(true);
|
||||
const batch = { eventId: event!.id, photoIds: uploads.map(upload => upload.photoId) };
|
||||
expect((await manager.completeGalleryPhotos(batch))[0]!.status).toBe("processing");
|
||||
expect((await manager.previewBulkPhotos({ ...batch, action: "delete" })).affected).toBe(0);
|
||||
// Simulate the worker's terminal state; never modify a real upload.
|
||||
await db.update(photos).set({ processingStatus: "ready" }).where(and(eq(photos.eventId, event!.id), eq(photos.id, uploads[0]!.photoId)));
|
||||
expect((await manager.bulkPhotos({ ...batch, action: "delete", confirm: true })).affected).toBe(1);
|
||||
expect(await headObject(originalObjectKey(event!.id, uploads[0]!.photoId))).toBeNull();
|
||||
}
|
||||
} finally {
|
||||
for (const upload of cleanupUploads) await deletePrefix(photoObjectPrefix(upload.eventId, upload.photoId));
|
||||
if (groupId) await db.delete(groups).where(eq(groups.id, groupId));
|
||||
await db.delete(user).where(inArray(user.id, people.map(person => person.id)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user