Files
webmail/lib/__tests__/docker-data-dirs.test.ts
T

68 lines
2.8 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
// Everything the server keeps lives under /app/data. The compose file has to
// persist all of it, and the image has to create each dir for the app user,
// or a fresh volume on one of them comes up root-owned and unwritable.
const ROOT = path.resolve(__dirname, '../..');
function libFiles(dir = path.join(ROOT, 'lib'), out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
if (name === '__tests__') continue;
const full = path.join(dir, name);
if (statSync(full).isDirectory()) libFiles(full, out);
else if (/\.tsx?$/.test(name)) out.push(full);
}
return out;
}
/** Default data dirs named in the source: <cwd>/data/<dir>, and plugin-secrets next to settings. */
function defaultDataDirs(): string[] {
const dirs = new Set<string>();
for (const file of libFiles()) {
const text = readFileSync(file, 'utf8');
for (const m of text.matchAll(/cwd\(\),\s*'data',\s*'([a-z-]+)'/g)) dirs.add(m[1]);
for (const m of text.matchAll(/'\.\.',\s*'([a-z-]+)'\)/g)) dirs.add(m[1]);
}
return [...dirs].sort();
}
describe('docker data dirs', () => {
it('finds the data dirs in the source', () => {
expect(defaultDataDirs()).toEqual(
expect.arrayContaining(['admin', 'admin-state', 'plugin-secrets', 'settings', 'telemetry', 'version-check']),
);
});
it('creates every data dir in the image for the app user', () => {
const dockerfile = readFileSync(path.join(ROOT, 'Dockerfile'), 'utf8');
const run = dockerfile.match(/RUN mkdir -p ([^&]+)&&[\s\S]*?chown -R nextjs:nodejs[^\n]*\/app\/data/);
expect(run).not.toBeNull();
const created = run![1].split(/[\s\\]+/).filter(Boolean);
for (const dir of defaultDataDirs()) expect(created).toContain(`/app/data/${dir}`);
});
it('persists every data dir in docker-compose.yml, by its own volume or one on /app/data', () => {
const compose = readFileSync(path.join(ROOT, 'docker-compose.yml'), 'utf8');
const mounts = [...compose.matchAll(/^\s*-\s*[\w.-]+:(\/app\/data[^\s:]*)/gm)].map((m) => m[1].replace(/\/+$/, ''));
expect(mounts.length).toBeGreaterThan(0);
const unpersisted = defaultDataDirs().filter(
(dir) => !mounts.includes('/app/data') && !mounts.includes(`/app/data/${dir}`),
);
expect(unpersisted).toEqual([]);
});
});
describe('.env.example', () => {
it('shows a session secret sample long enough to be accepted', () => {
const example = readFileSync(path.join(ROOT, '.env.example'), 'utf8');
const sample = example.match(/^#\s*SESSION_SECRET=(\S+)/m);
expect(sample).not.toBeNull();
expect(sample![1].length).toBeGreaterThanOrEqual(32);
expect(example).toContain('openssl rand -hex 32');
});
});