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', + ]); + }); +});