/** * AC verification for Passo 12 — server-side portion. * Tests that the sync.ts logic correctly: * - processes a queued item (with photo) against the real server * - handles duplicate clientRequestId (409) as success * - creates DB rows and MinIO objects as expected * * The browser-side IndexedDB + offline scenario is verified manually * using Chrome DevTools Network=Offline. */ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { config as loadEnv } from 'dotenv'; const here = path.dirname(fileURLToPath(import.meta.url)); loadEnv({ path: path.resolve(here, '../.env') }); import { prisma } from '../packages/db/src/index.js'; import { appRouter, createTRPCContext } from '../packages/api/src/index.js'; import { createCallerFactory } from '../packages/api/src/trpc.js'; async function makeCaller(email: string) { const user = await prisma.user.findFirst({ where: { email } }); if (!user) throw new Error(`${email} not found`); const ctx = await createTRPCContext({ user: { id: user.id, email: user.email, role: user.role as 'OPERATOR', tenantId: user.tenantId }, headers: new Headers(), }); return createCallerFactory(appRouter)(ctx); } async function main() { const op1 = await makeCaller('op1@demo.local'); const admin = await makeCaller('admin@demo.local'); const ws = (await op1.workstation.list())[0]; if (!ws) throw new Error('No workstations'); const fakePhoto = 'fake-photo-offline-test'; // Simulate what the sync loop does for 3 queued items with photos console.log('Simulating sync of 3 queued items (with photos)...'); const ids: string[] = []; for (let i = 1; i <= 3; i++) { const clientRequestId = crypto.randomUUID(); ids.push(clientRequestId); // Step a: signPhotoUpload + PUT (simulates what sync.ts does) const { uploadUrl, photoKey } = await op1.storage.signPhotoUpload({ contentType: 'image/jpeg', byteSize: fakePhoto.length, }); const putRes = await fetch(uploadUrl, { method: 'PUT', body: fakePhoto, headers: { 'Content-Type': 'image/jpeg' }, }); if (!putRes.ok) throw new Error(`Photo ${i} PUT failed: ${putRes.status}`); // Step b: maintenanceRequest.create await op1.maintenanceRequest.create({ workstationId: ws.id, description: `Pedido offline #${i} — passo 12`, photoKey, clientRequestId, }); console.log(` Item ${i}: id=${clientRequestId.slice(0, 8)}… photoKey set ✓`); } // Verify all 3 appear in admin queue const { items } = await admin.maintenanceRequest.queue({ statuses: ['OPEN'] }); const found = ids.filter((id) => items.some((item) => item.clientRequestId === id)); if (found.length !== 3) throw new Error(`Only ${found.length}/3 items in queue`); console.log(`\nAll 3 requests appear in admin queue ✓`); // Verify photos are in MinIO for (const item of items.filter((i) => ids.includes(i.clientRequestId))) { if (!item.photoKey) throw new Error(`Missing photoKey on ${item.clientRequestId}`); const { url } = await admin.storage.signPhotoDownload({ photoKey: item.photoKey }); const res = await fetch(url); if (!res.ok) throw new Error(`Photo download failed for ${item.clientRequestId}`); const content = await res.text(); if (content !== fakePhoto) throw new Error('Photo content mismatch'); } console.log('All 3 photos retrievable from MinIO ✓'); // Verify idempotency (duplicate clientRequestId → same row) console.log('\nTesting 409 idempotency...'); const dup = await op1.maintenanceRequest.create({ workstationId: ws.id, description: 'Duplicado', clientRequestId: ids[0]!, }); const original = await prisma.maintenanceRequest.findFirst({ where: { clientRequestId: ids[0] } }); if (dup.id !== original?.id) throw new Error('Idempotency failed'); console.log('Duplicate clientRequestId returns same row ✓'); await prisma.$disconnect(); console.log('\nPasso 12 server-side AC PASSED'); console.log('\nManual AC (browser): DevTools → Network=Offline → create 3 requests → Network=Online → ≤30s all appear in admin-web'); } main().catch(async (err) => { console.error('Passo 12 AC FAILED:', err); await prisma.$disconnect(); process.exit(1); });