Files
qinglong/packages/ql3-cluster-admin/test/automationManagementProcess.test.cjs

212 lines
7.0 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFile, writeFile, mkdtemp, rm } = require('node:fs/promises');
const { tmpdir } = require('node:os');
const { join, resolve } = require('node:path');
const { test } = require('node:test');
const {
ClusterAutomationManagementProcessConfigError,
loadClusterAutomationManagementProcessConfig,
startClusterAutomationManagementProcess,
} = require('@qinglong/cluster-admin/automation-management-process');
const FIXTURES = resolve(
__dirname,
'../../ql3-cluster-control/test/fixtures/mtls',
);
const NOW_MS = Date.UTC(2030, 0, 1);
function enabledEnvironment(paths, overrides = {}) {
return {
QL3_AUTOMATION_MANAGEMENT_ENABLED: 'true',
QL3_PROFILE: 'cluster-admin',
QL3_AUTOMATION_MANAGEMENT_HOST: '127.0.0.1',
QL3_AUTOMATION_MANAGEMENT_PORT: '8445',
QL3_AUTOMATION_MANAGEMENT_TLS_CERT_FILE: paths.certificateFile,
QL3_AUTOMATION_MANAGEMENT_TLS_KEY_FILE: paths.privateKeyFile,
QL3_AUTOMATION_MANAGEMENT_CLIENT_CA_FILE:
paths.clientCertificateAuthorityFile,
QL3_AUTOMATION_MANAGEMENT_CLIENT_CRL_FILE:
paths.clientCertificateRevocationListFile,
QL3_AUTOMATION_MANAGEMENT_IDENTITY_KEYSET_FILE: paths.identityKeysetFile,
QL3_POSTGRES_AUTOMATION_MANAGER_URL:
'postgresql://ql3_automation_manager:secret@postgres.example.test/ql3',
QL3_POSTGRES_AUTOMATION_MANAGER_TLS_MODE: 'disable',
QL3_POSTGRES_AUTOMATION_MANAGER_ALLOW_INSECURE: 'true',
...overrides,
};
}
async function tlsFixture(run) {
const directory = await mkdtemp(join(tmpdir(), 'ql3-automation-manager-'));
const paths = {
certificateFile: join(directory, 'tls.crt'),
privateKeyFile: join(directory, 'tls.key'),
clientCertificateAuthorityFile: join(directory, 'client-ca.crt'),
clientCertificateRevocationListFile: join(directory, 'client.crl'),
identityKeysetFile: join(directory, 'keyset.json'),
};
try {
await writeFile(
paths.certificateFile,
await readFile(join(FIXTURES, 'server-cert.pem')),
{ mode: 0o644 },
);
await writeFile(
paths.privateKeyFile,
await readFile(join(FIXTURES, 'server-key.pem')),
{ mode: 0o640 },
);
await writeFile(
paths.clientCertificateAuthorityFile,
await readFile(join(FIXTURES, 'ca-cert.pem')),
{ mode: 0o644 },
);
await writeFile(
paths.clientCertificateRevocationListFile,
await readFile(join(FIXTURES, 'empty-crl.pem')),
{ mode: 0o644 },
);
await writeFile(paths.identityKeysetFile, '{}\n', { mode: 0o644 });
return await run(paths);
} finally {
await rm(directory, { recursive: true, force: true });
}
}
test('disabled automation manager opens no PostgreSQL or file authority', async () => {
let opened = 0;
const runtime = await startClusterAutomationManagementProcess({
environment: { QL3_AUTOMATION_MANAGEMENT_ENABLED: 'false' },
async openDatabase() {
opened += 1;
throw new Error('must not open');
},
});
assert.equal(runtime.status, 'disabled');
assert.equal(opened, 0);
await runtime.close();
});
test('loads bounded low-footprint automation-only HTTPS and PostgreSQL configuration', async () => {
await tlsFixture(async (paths) => {
const config = loadClusterAutomationManagementProcessConfig(
enabledEnvironment(paths),
);
assert.equal(config.enabled, true);
assert.equal(config.port, 8445);
assert.equal(config.database.pool.maxConnections, 2);
assert.equal(
config.database.pool.applicationName,
'qinglong3-automation-manager',
);
assert.equal(config.http.maxConnections, 32);
assert.equal(config.http.maxConcurrentRequests, 16);
assert.match(
config.database.connection.connectionString,
/^postgresql:\/\/ql3_automation_manager:/,
);
assert.equal(config.database.connection.tls.mode, 'disable');
});
assert.throws(
() =>
loadClusterAutomationManagementProcessConfig({
QL3_AUTOMATION_MANAGEMENT_ENABLED: 'true',
QL3_PROFILE: 'cluster-admin',
QL3_POSTGRES_AUTOMATION_MANAGER_TLS_MODE: 'disable',
}),
ClusterAutomationManagementProcessConfigError,
);
});
test('starts only after automation readiness and identity validation then closes in order', async () => {
await tlsFixture(async (paths) => {
const order = [];
let privateKey;
let transport;
let httpClosed = 0;
let databaseClosed = 0;
const pool = {
async query() {
throw new Error('repositories must remain lazy during composition');
},
async connect() {
throw new Error('repositories must remain lazy during composition');
},
};
const runtime = await startClusterAutomationManagementProcess({
environment: enabledEnvironment(paths),
now: () => NOW_MS,
async openDatabase() {
order.push('open');
return {
pool,
async close() {
order.push('database-close');
databaseClosed += 1;
},
};
},
async assertReady(candidate) {
order.push('ready');
assert.equal(candidate, pool);
return {
ready: true,
writablePrimary: true,
serverVersionNum: 180004,
serverMajor: 18,
currentUser: 'ql3_automation_manager',
contractName: 'control-core',
contractVersion: 53,
migrationIds: [
'pg-0052-automation-management-identity-keyset-ledger',
],
};
},
identities: {
async reload() {
order.push('identity');
return {
schemaVersion: 1,
generation: 3,
digest: 'identity-digest',
issuer: 'https://identity.example.test/',
audience: 'qinglong3-automation-management',
activeKeyIds: ['key-3'],
revokedKeyIds: ['key-2'],
};
},
bind() {
throw new Error('HTTP stub does not authenticate');
},
},
async startHttp(options) {
order.push('http');
privateKey = options.tls.privateKey;
transport = options.transport;
assert.ok(options.tls.clientCertificateAuthority);
assert.ok(options.tls.clientCertificateRevocationList);
return {
status: 'active',
address: { host: options.host, port: options.port },
availabilityStatus: () => 'ready',
withdraw() {},
async close() {
order.push('http-close');
httpClosed += 1;
},
};
},
});
assert.equal(runtime.status, 'active');
assert.deepEqual(order, ['open', 'ready', 'identity', 'http']);
assert.equal(typeof transport.execute, 'function');
assert.equal(privateKey.every((byte) => byte === 0), true);
assert.equal(runtime.database.contractVersion, 53);
await Promise.all([runtime.close(), runtime.close()]);
assert.equal(httpClosed, 1);
assert.equal(databaseClosed, 1);
assert.deepEqual(order.slice(-2), ['http-close', 'database-close']);
});
});