feat(api): complete phase 2.4 control plane

This commit is contained in:
chick
2026-07-17 00:37:11 +08:00
parent c2702b5fc6
commit 7b91dbbad1
40 changed files with 4526 additions and 74 deletions
+112
View File
@@ -0,0 +1,112 @@
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it } from 'vitest';
import { buildControlPlaneApp } from './control-plane.js';
import { migrateDatabase } from './infrastructure/database/migrations.js';
import { UpstreamError } from './infrastructure/transport/upstream-error.js';
import { SafeInstanceTransport } from './infrastructure/transport/safe-instance-transport.js';
import { createSafeControlPlaneUpstream } from './infrastructure/transport/safe-control-plane-upstream.js';
import type { SecretStore } from './infrastructure/secrets/secret-store.js';
const dbs: Database.Database[] = [];
afterEach(async () => {
for (const db of dbs.splice(0)) db.close();
});
class Store implements SecretStore {
async set() {
return '';
}
async get() {
return '[REDACTED]';
}
async delete() {
return false;
}
}
describe('buildControlPlaneApp', () => {
it('assembles instance CRUD, connection checks, login and logout without listening or reading external config', async () => {
const db = new Database(':memory:');
db.pragma('foreign_keys=ON');
migrateDatabase(db);
dbs.push(db);
const app = buildControlPlaneApp({
db,
store: new Store(),
upstream: createSafeControlPlaneUpstream(
new SafeInstanceTransport({
resolve: async () => [{ address: '192.168.1.10', family: 4 }],
request: async (request) => ({
status: request.url.endsWith('/api/health') ? 401 : 200,
headers: { 'set-cookie': 'simadmin_session=opaque' },
body: '',
}),
}),
),
now: () => new Date('2026-07-16T12:00:00.000Z'),
});
const created = await app.inject({
method: 'POST',
url: '/api/v1/instances',
payload: { name: 'A', origin: 'http://192.168.1.10:8080' },
});
expect(created.statusCode).toBe(201);
const tested = await app.inject({
method: 'POST',
url: '/api/v1/instances/' + created.json().id + '/test-connection',
});
expect(tested.json()).toMatchObject({ authenticated: false });
const insecureLogin = await app.inject({
method: 'POST',
url: '/api/v1/instances/' + created.json().id + '/login',
payload: { password: 'never-leak-this' },
});
expect(insecureLogin.statusCode).toBe(400);
expect(insecureLogin.json()).toMatchObject({
title: 'Bad Request',
status: 400,
code: 'UPSTREAM_INSECURE_AUTH',
});
expect(insecureLogin.body).not.toContain('never-leak-this');
expect(insecureLogin.body).not.toContain('192.168.1.10');
await app.close();
});
it('maps typed upstream failures to redacted gateway Problem Details', async () => {
const db = new Database(':memory:');
db.pragma('foreign_keys=ON');
migrateDatabase(db);
dbs.push(db);
const app = buildControlPlaneApp({
db,
store: new Store(),
upstream: {
get: async () => {
throw new UpstreamError('UPSTREAM_UNAVAILABLE', {
cause: new Error('ECONNREFUSED 192.168.1.99 cookie=simadmin_session=secret'),
});
},
request: async () => ({ status: 200, headers: {}, body: '' }),
},
});
const created = await app.inject({
method: 'POST',
url: '/api/v1/instances',
payload: { name: 'B', origin: 'http://192.168.1.99' },
});
const response = await app.inject({
method: 'POST',
url: '/api/v1/instances/' + created.json().id + '/test-connection',
});
expect(response.statusCode).toBe(502);
expect(response.headers['content-type']).toContain('application/problem+json');
expect(response.json()).toMatchObject({
title: 'Bad Gateway',
status: 502,
code: 'UPSTREAM_UNAVAILABLE',
});
expect(response.body).not.toContain('ECONNREFUSED');
expect(response.body).not.toContain('192.168.1.99');
expect(response.body).not.toContain('simadmin_session');
await app.close();
});
});