mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-25 22:10:49 +00:00
03efbde268
Make guard-sqlite-auth-split self-contained via _guard_file_exists so orphan auth.db detection works when the script is sourced alone (unit tests) while still preferring file_exists_as_betterdesk under entrypoints. Align bootstrap credential tests with #385 (generate/export shared password; publish ADMIN_PASSWORD to .admin_credentials). Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
187 lines
6.8 KiB
JavaScript
187 lines
6.8 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawn, spawnSync } = require('child_process');
|
|
|
|
const bootstrapScript = path.resolve(__dirname, '..', '..', 'docker', 'bootstrap-admin-credentials.sh');
|
|
const hasPosixShell = spawnSync('sh', ['-c', 'exit 0'], { stdio: 'ignore' }).status === 0;
|
|
|
|
function sourceBootstrap(env) {
|
|
const scriptPath = bootstrapScript.replace(/\\/g, '/');
|
|
return spawnSync('sh', ['-c', `. "${scriptPath}"`], {
|
|
env: { ...process.env, ...env },
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
function sourceBootstrapExports(env) {
|
|
const scriptPath = bootstrapScript.replace(/\\/g, '/');
|
|
return spawnSync(
|
|
'sh',
|
|
[
|
|
'-c',
|
|
`. "${scriptPath}" >/dev/null 2>&1; printf "%s|%s|%s|%s\\n" "$INIT_ADMIN_USER" "$DEFAULT_ADMIN_USERNAME" "$INIT_ADMIN_PASS" "$DEFAULT_ADMIN_PASSWORD"`,
|
|
],
|
|
{
|
|
env: { ...process.env, ...env },
|
|
encoding: 'utf8',
|
|
}
|
|
);
|
|
}
|
|
|
|
(hasPosixShell ? describe : describe.skip)('Docker bootstrap-admin-credentials', () => {
|
|
let tempDir;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'betterdesk-bootstrap-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('publishes the configured password on a fresh primary store', () => {
|
|
const result = sourceBootstrap({
|
|
RUSTDESK_PATH: tempDir,
|
|
ADMIN_PASSWORD: 'configured-password-123',
|
|
INIT_ADMIN_PASS: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
const credentials = fs.readFileSync(path.join(tempDir, '.admin_credentials'), 'utf8');
|
|
expect(credentials).toContain('Admin Username: admin');
|
|
expect(credentials).toContain('Admin Password: configured-password-123');
|
|
expect(credentials).toContain('Generated by: BetterDesk Docker bootstrap (ADMIN_PASSWORD)');
|
|
});
|
|
|
|
test('refreshes stale credentials when the primary store is still empty', () => {
|
|
fs.writeFileSync(
|
|
path.join(tempDir, '.admin_credentials'),
|
|
'Admin Username: admin\nAdmin Password: stale-password\n',
|
|
{ mode: 0o600 }
|
|
);
|
|
|
|
const result = sourceBootstrap({
|
|
RUSTDESK_PATH: tempDir,
|
|
ADMIN_PASSWORD: 'replacement-password-123',
|
|
INIT_ADMIN_PASS: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
const credentials = fs.readFileSync(path.join(tempDir, '.admin_credentials'), 'utf8');
|
|
expect(credentials).toContain('Admin Password: replacement-password-123');
|
|
});
|
|
|
|
test('exports supervisord admin variables when no password is available', () => {
|
|
const result = sourceBootstrapExports({
|
|
RUSTDESK_PATH: tempDir,
|
|
DB_PATH: path.join(tempDir, 'missing.sqlite'),
|
|
INIT_ADMIN_USER: '',
|
|
INIT_ADMIN_PASS: '',
|
|
DEFAULT_ADMIN_USERNAME: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
ADMIN_USERNAME: '',
|
|
ADMIN_PASSWORD: '',
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
// Fresh volume with no ADMIN_PASSWORD: generate one shared secret and
|
|
// export it for both Go (INIT_*) and Node (DEFAULT_*) supervisord vars.
|
|
const parts = result.stdout.trim().split('|');
|
|
expect(parts).toHaveLength(4);
|
|
expect(parts[0]).toBe('admin');
|
|
expect(parts[1]).toBe('admin');
|
|
expect(parts[2].length).toBeGreaterThan(0);
|
|
expect(parts[3]).toBe(parts[2]);
|
|
expect(fs.existsSync(path.join(tempDir, '.admin_credentials'))).toBe(true);
|
|
});
|
|
|
|
test('keeps configured admin values synchronized across services', () => {
|
|
const result = sourceBootstrapExports({
|
|
RUSTDESK_PATH: tempDir,
|
|
INIT_ADMIN_USER: 'operator',
|
|
INIT_ADMIN_PASS: 'configured-password-123',
|
|
DEFAULT_ADMIN_USERNAME: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
ADMIN_USERNAME: '',
|
|
ADMIN_PASSWORD: '',
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout.trim()).toBe(
|
|
'operator|operator|configured-password-123|configured-password-123'
|
|
);
|
|
});
|
|
});
|
|
|
|
function runBootstrap(env) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn('sh', ['-c', '. "$BOOTSTRAP_SCRIPT"; printf "%s\\n" "$INIT_ADMIN_PASS" > "$RESULT_FILE"'], {
|
|
env: {
|
|
...process.env,
|
|
...env,
|
|
BOOTSTRAP_SCRIPT: bootstrapScript,
|
|
},
|
|
stdio: 'ignore',
|
|
});
|
|
child.on('error', reject);
|
|
child.on('close', code => resolve(code));
|
|
});
|
|
}
|
|
|
|
(hasPosixShell ? describe : describe.skip)('Docker bootstrap admin credentials', () => {
|
|
let tempDir;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'betterdesk-bootstrap-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('concurrent split entrypoints elect one shared password', async () => {
|
|
const jobs = Array.from({ length: 8 }, (_, index) => runBootstrap({
|
|
RUSTDESK_PATH: tempDir,
|
|
RESULT_FILE: path.join(tempDir, `result-${index}`),
|
|
BOOTSTRAP_CREDENTIALS_WAIT_SECONDS: '10',
|
|
INIT_ADMIN_PASS: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
ADMIN_PASSWORD: '',
|
|
}));
|
|
|
|
const codes = await Promise.all(jobs);
|
|
expect(codes).toEqual(Array(8).fill(0));
|
|
|
|
const passwords = codes.map((_, index) =>
|
|
fs.readFileSync(path.join(tempDir, `result-${index}`), 'utf8').trim());
|
|
expect(new Set(passwords).size).toBe(1);
|
|
|
|
const credentials = fs.readFileSync(path.join(tempDir, '.admin_credentials'), 'utf8');
|
|
expect(credentials).toContain(`Admin Password: ${passwords[0]}`);
|
|
expect(fs.existsSync(path.join(tempDir, '.admin_credentials.lock'))).toBe(false);
|
|
});
|
|
|
|
test('reuses an existing credentials file without replacing it', async () => {
|
|
const credentialsPath = path.join(tempDir, '.admin_credentials');
|
|
const original = 'Admin Username: admin\nAdmin Password: existing-password\n';
|
|
fs.writeFileSync(credentialsPath, original, { mode: 0o600 });
|
|
|
|
const code = await runBootstrap({
|
|
RUSTDESK_PATH: tempDir,
|
|
RESULT_FILE: path.join(tempDir, 'result'),
|
|
INIT_ADMIN_PASS: '',
|
|
DEFAULT_ADMIN_PASSWORD: '',
|
|
ADMIN_PASSWORD: '',
|
|
});
|
|
|
|
expect(code).toBe(0);
|
|
expect(fs.readFileSync(path.join(tempDir, 'result'), 'utf8').trim()).toBe('existing-password');
|
|
expect(fs.readFileSync(credentialsPath, 'utf8')).toBe(original);
|
|
});
|
|
});
|