Files
BetterDesk/web-nodejs/tests/dockerBuildContract.test.js
T
UNITRONIX 602c4e798b fix(docker): preserve secrets on hardened bind mounts
Refs #424

Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
2026-09-26 03:02:17 +02:00

143 lines
5.8 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..', '..');
const dockerfiles = ['Dockerfile', 'Dockerfile.console', 'Dockerfile.server'];
function copySources(dockerfile) {
return fs.readFileSync(path.join(repoRoot, dockerfile), 'utf8')
.split(/\r?\n/)
.filter(line => /^\s*COPY\s/.test(line) && !line.includes('--from='))
.flatMap(line => {
const tokens = line.trim().split(/\s+/).slice(1);
return tokens.slice(0, -1);
});
}
function sourceExists(source) {
if (!/[?*[\]{}]/.test(source)) {
return fs.existsSync(path.join(repoRoot, source));
}
const wildcardIndex = source.search(/[?*[\]{}]/);
const directory = path.dirname(source.slice(0, wildcardIndex));
const prefix = path.basename(source.slice(0, wildcardIndex));
const absoluteDirectory = path.join(repoRoot, directory);
return fs.existsSync(absoluteDirectory)
&& fs.readdirSync(absoluteDirectory).some(entry => entry.startsWith(prefix));
}
describe('Docker build context contract', () => {
test.each(dockerfiles)('%s only copies sources present in the repository', dockerfile => {
const missing = copySources(dockerfile).filter(source => !sourceExists(source));
expect(missing).toEqual([]);
});
test.each(dockerfiles)('%s does not require withdrawn agent sources', dockerfile => {
const contents = fs.readFileSync(path.join(repoRoot, dockerfile), 'utf8');
expect(contents).not.toMatch(/betterdesk-(?:support-)?agent|agent-source/);
});
});
describe('Docker admin bootstrap contract', () => {
const composeFiles = [
'docker-compose.yml',
'docker-compose.single.yml',
'docker-compose.quick.yml',
'docker-compose.quick.single.yml',
'docker-compose.quick.macvlan.yml',
'docker-compose.quick.single.macvlan.yml',
];
test.each(composeFiles)('%s forwards all admin password aliases', composeFile => {
const contents = fs.readFileSync(path.join(repoRoot, composeFile), 'utf8');
expect(contents).toContain('ADMIN_PASSWORD=${ADMIN_PASSWORD:-}');
expect(contents).toContain('INIT_ADMIN_PASS=${ADMIN_PASSWORD:-}');
expect(contents).toContain('DEFAULT_ADMIN_PASSWORD=${ADMIN_PASSWORD:-}');
});
test('split Compose variants use the shared primary SQLite path', () => {
for (const composeFile of ['docker-compose.quick.yml', 'docker-compose.quick.macvlan.yml']) {
const contents = fs.readFileSync(path.join(repoRoot, composeFile), 'utf8');
expect(contents).toContain('DB_URL=/opt/rustdesk/db_v2.sqlite3');
expect(contents).toContain('DB_PATH=/opt/rustdesk/db_v2.sqlite3');
}
});
test('split server entrypoint normalizes the Go database path', () => {
const contents = fs.readFileSync(
path.join(repoRoot, 'docker', 'server-entrypoint.sh'),
'utf8'
);
expect(contents).toContain('export DB_URL="${DB_PATH:-${RUSTDESK_PATH:-$DATA_DIR}/db_v2.sqlite3}"');
expect(contents).toContain('export DB_URL="${DATABASE_URL:-${DB_PATH:-${RUSTDESK_PATH:-$DATA_DIR}/db_v2.sqlite3}}"');
});
test('hardened Docker checks app-owned paths as betterdesk', () => {
const bootstrap = fs.readFileSync(
path.join(repoRoot, 'docker', 'bootstrap-admin-credentials.sh'),
'utf8'
);
const entrypoint = fs.readFileSync(
path.join(repoRoot, 'docker', 'entrypoint.sh'),
'utf8'
);
const guard = fs.readFileSync(
path.join(repoRoot, 'docker', 'guard-sqlite-auth-split.sh'),
'utf8'
);
expect(bootstrap).toContain('file_exists_as_betterdesk()');
expect(bootstrap).toContain('path_exists_as_betterdesk()');
expect(bootstrap).toContain('file_exists_as_betterdesk "$_file"');
expect(bootstrap).toContain('file_exists_as_betterdesk "$_primary_db"');
expect(bootstrap).toContain('path_exists_as_betterdesk "$CREDS_FILE"');
expect(entrypoint).toContain('file_exists_as_betterdesk "$API_KEY_FILE"');
expect(entrypoint).toContain('file_exists_as_betterdesk "$ENROLLMENT_SENTINEL"');
expect(guard).toContain('file_exists_as_betterdesk "$_auth_db"');
expect(guard).toContain('file_exists_as_betterdesk "$_primary_db"');
});
test('AIO supervisord admin variables are always exported by bootstrap', () => {
const bootstrap = fs.readFileSync(
path.join(repoRoot, 'docker', 'bootstrap-admin-credentials.sh'),
'utf8'
);
const supervisor = fs.readFileSync(
path.join(repoRoot, 'docker', 'supervisord.conf'),
'utf8'
);
expect(bootstrap).toContain('ensure_admin_exports()');
expect(bootstrap).toContain('export INIT_ADMIN_USER=');
expect(bootstrap).toContain('export INIT_ADMIN_PASS=');
expect(bootstrap).toContain('export DEFAULT_ADMIN_USERNAME=');
expect(bootstrap).toContain('export DEFAULT_ADMIN_PASSWORD=');
expect(supervisor).toContain('%(ENV_DEFAULT_ADMIN_USERNAME)s');
expect(supervisor).toContain('%(ENV_DEFAULT_ADMIN_PASSWORD)s');
expect(supervisor).toContain('%(ENV_INIT_ADMIN_USER)s');
expect(supervisor).toContain('%(ENV_INIT_ADMIN_PASS)s');
});
test.each([
'docker-compose.yml',
'docker-compose.single.yml',
'docker-compose.quick.yml',
'docker-compose.quick.single.yml',
'docker-compose.quick.macvlan.yml',
'docker-compose.quick.single.macvlan.yml',
])('%s exposes optional TRUST_PROXY configuration', composeFile => {
const contents = fs.readFileSync(path.join(repoRoot, composeFile), 'utf8');
expect(contents).toContain('TRUST_PROXY=${TRUST_PROXY:-}');
});
});