From fed99ac03dfde03cf4c55fcb4b1419fea7564156 Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:52:35 +0200 Subject: [PATCH] fix(archives): write a real timestamp on restored photos (#1257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings this branch in line with main, which fixed it in passing. The archive restore inserted photos with a bare Date for uploaded_at. Inside jest the sqlite3 binding's type dispatch misses sandbox-created Dates and stores the literal string "[object Object]", so every restored photo got a garbage timestamp. Verified on this branch rather than assumed: bare Date -> "[object Object]" toISOString -> "2026-09-01T06:48:41.915Z" Production writes Dates as ms-numbers and is unaffected, which is exactly why it survives unnoticed — it only corrupts what tests read back, so a future test asserting on a restored photo's date would have believed it. The regression test fails against the previous line. Not touched: the category insert a few lines up has the same shape, but it is identical on main, so fixing it here alone would re-open the divergence this commit closes. Worth one small PR against both branches. Co-authored-by: Paul Nothaft --- .../adminArchives.restoreCategories.test.js | 21 +++++++++++++++++++ backend/src/routes/adminArchives.js | 8 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/integration/adminArchives.restoreCategories.test.js b/backend/__tests__/integration/adminArchives.restoreCategories.test.js index 723efc9f..cac9982a 100644 --- a/backend/__tests__/integration/adminArchives.restoreCategories.test.js +++ b/backend/__tests__/integration/adminArchives.restoreCategories.test.js @@ -141,6 +141,27 @@ describe('archive restore restores categories (flat archives included)', () => { expect(await categoryOf('b.jpg')).toBe('Ceremony'); }); + it('stores a real timestamp on restored photos, not "[object Object]"', async () => { + // The jest+sqlite landmine: a Date handed to knex inside jest stores as + // the literal string "[object Object]". Production writes ms-numbers and + // is unaffected, so this only ever corrupts what tests read back — which + // is how it survives unnoticed. + const archiveRelPath = await writeArchive('timestamp.zip', { + 'individual/STAMPED.jpg': PIXEL, + 'photos_manifest.json': Buffer.from(JSON.stringify([ + { filename: 'STAMPED.jpg', original_filename: 'STAMPED.jpg', category_name: 'Ceremony' }, + ]), 'utf8'), + }); + const eventId = await seedArchivedEvent(archiveRelPath, 'timestamp-event'); + + const res = await request(app).post(`/admin/archives/${eventId}/restore`).send({}); + expect(res.status).toBe(200); + + const photo = await db('photos').where({ event_id: eventId, filename: 'STAMPED.jpg' }).first(); + expect(String(photo.uploaded_at)).not.toBe('[object Object]'); + expect(Number.isNaN(new Date(photo.uploaded_at).getTime())).toBe(false); + }); + it('reuses an existing category row instead of creating a duplicate', async () => { const archiveRelPath = await writeArchive('reuse.zip', { 'c.jpg': PIXEL, diff --git a/backend/src/routes/adminArchives.js b/backend/src/routes/adminArchives.js index ebdf81c0..4b92668b 100644 --- a/backend/src/routes/adminArchives.js +++ b/backend/src/routes/adminArchives.js @@ -464,7 +464,13 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re type: path.extname(filename).substring(1).toLowerCase(), size_bytes: stats.size, category_id: categoryId, - uploaded_at: new Date() + // .toISOString(), not a Date: inside jest the sqlite3 binding's + // type dispatch misses sandbox-created Dates and stores the + // literal string "[object Object]", so every restored photo + // gets a garbage timestamp that any test reading it would + // believe. Production stores Dates as ms-numbers and is + // unaffected — which is exactly why this survives unnoticed. + uploaded_at: new Date().toISOString() }); } } catch (statError) {