From 57cc676e8ae07b9f6fa3653e035e6f7d49673dd3 Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 31 Jul 2026 15:41:02 +0200 Subject: [PATCH] test(uploads): pin the uploads subdir list across Dockerfile and boot The set of uploads subdirectories is hand-mirrored between the image build and the boot-time mkdir in server/src/index.ts, and drift is invisible until a user whose bind-mounted uploads dir is not writable by node hits an EACCES on the first upload to the missing dir (#1762). Parse both lists and assert they match, with a guard case so the regexes cannot silently match nothing and pass vacuously. --- server/tests/unit/uploads-dirs.test.ts | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 server/tests/unit/uploads-dirs.test.ts diff --git a/server/tests/unit/uploads-dirs.test.ts b/server/tests/unit/uploads-dirs.test.ts new file mode 100644 index 000000000..691b36b2a --- /dev/null +++ b/server/tests/unit/uploads-dirs.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +// The set of uploads subdirectories is hand-mirrored between the image build and +// the boot-time mkdir in server/src/index.ts. Drift is invisible until a user on +// a host whose bind-mounted uploads dir isn't writable by node hits an EACCES on +// first upload to the missing dir (#1762), so pin the two copies together. +const repoRoot = path.resolve(__dirname, '../../..'); + +const readSubdirs = (file: string, pattern: RegExp): Set => { + const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); + const found = new Set(); + for (const m of source.matchAll(pattern)) found.add(m[1]); + return found; +}; + +describe('uploads subdirectory parity', () => { + const fromDockerfile = readSubdirs('Dockerfile', /\/app\/uploads\/([A-Za-z0-9_-]+)/g); + const fromBoot = readSubdirs('server/src/index.ts', /path\.join\(uploadsDir, '([A-Za-z0-9_-]+)'\)/g); + + it('finds both lists (guards against the regexes silently matching nothing)', () => { + expect(fromDockerfile.size).toBeGreaterThan(0); + expect(fromBoot.size).toBeGreaterThan(0); + }); + + it('creates the same uploads subdirs in the Dockerfile and at boot', () => { + expect([...fromBoot].sort()).toEqual([...fromDockerfile].sort()); + }); + + it('covers every uploads subdir the server writes to', () => { + // journey/ and places/ are written by journey.controller.ts and + // services/placeImage.ts; the rest are the long-standing upload stores. + expect([...fromBoot].sort()).toEqual([ + 'avatars', + 'covers', + 'files', + 'journey', + 'photos', + 'places', + ]); + }); +});