feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Phase 3b post-review — admin mutations drop per-key Prometheus
|
||||
* gauge labels so the registry doesn't grow unbounded over the key
|
||||
* lifecycle (issue → revoke → issue → revoke … leaves a permanent
|
||||
* `budgetUsedRatio{team, key_prefix}` series for every dead key
|
||||
* without this fix).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import express, { type Request, type RequestHandler } from 'express';
|
||||
import request from 'supertest';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { Registry } from 'prom-client';
|
||||
import { Repository } from '../db/repository.js';
|
||||
import { createAdminGatewayApi } from './admin-gateway-api.js';
|
||||
import { createGatewayMetrics, type GatewayMetrics } from '../metrics/gateway-metrics.js';
|
||||
|
||||
function buildApp(repo: Repository, metrics: GatewayMetrics): express.Application {
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '4kb' }));
|
||||
const guard: RequestHandler = (req, _res, next) => {
|
||||
(req as Request & { user?: unknown }).user = { id: 'admin-1', role: 'admin', status: 'active' };
|
||||
next();
|
||||
};
|
||||
const router = createAdminGatewayApi({
|
||||
repo,
|
||||
requireAdmin: guard,
|
||||
getUserId: (req) => {
|
||||
const u = (req as Request & { user?: { id?: string } }).user;
|
||||
return u?.id ?? null;
|
||||
},
|
||||
gatewayMetrics: metrics,
|
||||
});
|
||||
app.use('/api/admin/gateway/keys', router);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('admin-gateway-api metric label removal (Phase 3b post-review)', () => {
|
||||
let tmpDir: string;
|
||||
let repo: Repository;
|
||||
let reg: Registry;
|
||||
let metrics: GatewayMetrics;
|
||||
let app: express.Application;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'admin-gw-metrics-'));
|
||||
repo = new Repository(join(tmpDir, 'test.db'));
|
||||
reg = new Registry();
|
||||
metrics = createGatewayMetrics(reg, 'aao_gateway_ml');
|
||||
app = buildApp(repo, metrics);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
repo.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('revoke removes the budgetUsedRatio label for the revoked key', async () => {
|
||||
const create = await request(app).post('/api/admin/gateway/keys').send({ team: 'alpha' });
|
||||
expect(create.status).toBe(201);
|
||||
const id = create.body.id as string;
|
||||
const prefix = id.slice(0, 8);
|
||||
// Simulate the gauge being set as it would by the bootstrap recordUsage callback.
|
||||
metrics.budgetUsedRatio.labels({ team: 'alpha', key_prefix: prefix }).set(0.42);
|
||||
let dump = await reg.metrics();
|
||||
expect(dump).toMatch(
|
||||
new RegExp(`aao_gateway_ml_virtual_key_budget_used_ratio\\{team="alpha",key_prefix="${prefix}"\\} 0\\.42`),
|
||||
);
|
||||
// Now revoke.
|
||||
const rev = await request(app).post(`/api/admin/gateway/keys/${id}/revoke`);
|
||||
expect(rev.status).toBe(200);
|
||||
// After remove() the label series should no longer appear.
|
||||
dump = await reg.metrics();
|
||||
expect(dump).not.toMatch(new RegExp(`key_prefix="${prefix}"`));
|
||||
});
|
||||
|
||||
it('rotate removes the OLD key prefix label (new key creates its own on next usage)', async () => {
|
||||
const create = await request(app).post('/api/admin/gateway/keys').send({ team: 'beta' });
|
||||
const oldId = create.body.id as string;
|
||||
const oldPrefix = oldId.slice(0, 8);
|
||||
metrics.budgetUsedRatio.labels({ team: 'beta', key_prefix: oldPrefix }).set(0.55);
|
||||
let dump = await reg.metrics();
|
||||
expect(dump).toMatch(new RegExp(`key_prefix="${oldPrefix}"`));
|
||||
const rot = await request(app).post(`/api/admin/gateway/keys/${oldId}/rotate`);
|
||||
expect(rot.status).toBe(201);
|
||||
dump = await reg.metrics();
|
||||
expect(dump).not.toMatch(new RegExp(`key_prefix="${oldPrefix}"`));
|
||||
});
|
||||
|
||||
it('delete removes the label too', async () => {
|
||||
const create = await request(app).post('/api/admin/gateway/keys').send({ team: 'gamma' });
|
||||
const id = create.body.id as string;
|
||||
const prefix = id.slice(0, 8);
|
||||
metrics.budgetUsedRatio.labels({ team: 'gamma', key_prefix: prefix }).set(0.7);
|
||||
const del = await request(app).delete(`/api/admin/gateway/keys/${id}`);
|
||||
expect(del.status).toBe(204);
|
||||
const dump = await reg.metrics();
|
||||
expect(dump).not.toMatch(new RegExp(`key_prefix="${prefix}"`));
|
||||
});
|
||||
|
||||
it('revoke without an existing gauge label is a safe no-op', async () => {
|
||||
const create = await request(app).post('/api/admin/gateway/keys').send({ team: 'delta' });
|
||||
const id = create.body.id as string;
|
||||
// Don't pre-set the gauge — remove() of an unknown label is a noop.
|
||||
const rev = await request(app).post(`/api/admin/gateway/keys/${id}/revoke`);
|
||||
expect(rev.status).toBe(200);
|
||||
});
|
||||
|
||||
it('missing gatewayMetrics handle (cross-process deploy) does not block mutations', async () => {
|
||||
// Build a separate app without a metrics handle to verify the
|
||||
// admin API stays functional when the gateway runs in a different
|
||||
// process.
|
||||
const app2 = express();
|
||||
app2.use(express.json({ limit: '4kb' }));
|
||||
const guard: RequestHandler = (req, _res, next) => {
|
||||
(req as Request & { user?: unknown }).user = { id: 'admin-1', role: 'admin', status: 'active' };
|
||||
next();
|
||||
};
|
||||
const router = createAdminGatewayApi({
|
||||
repo,
|
||||
requireAdmin: guard,
|
||||
getUserId: (req) => {
|
||||
const u = (req as Request & { user?: { id?: string } }).user;
|
||||
return u?.id ?? null;
|
||||
},
|
||||
// No gatewayMetrics. dropKeyMetricLabels is a no-op.
|
||||
});
|
||||
app2.use('/api/admin/gateway/keys', router);
|
||||
const create = await request(app2).post('/api/admin/gateway/keys').send({ team: 'eps' });
|
||||
expect(create.status).toBe(201);
|
||||
const id = create.body.id as string;
|
||||
const rev = await request(app2).post(`/api/admin/gateway/keys/${id}/revoke`);
|
||||
expect(rev.status).toBe(200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user