mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-23 16:39:18 +00:00
5cc4566eb1
Adds a reusable five-role persona fixture and extends RBAC test matrix to cover proxy, WebSocket, Pilot-agent, and mixed-version transport surfaces.
106 lines
4.1 KiB
TypeScript
106 lines
4.1 KiB
TypeScript
/**
|
|
* Pilot-agent-mode header parity: the createRemoteProxyMiddleware code path
|
|
* for pilot_agent remotes is identical to proxy mode (the only difference is
|
|
* an empty apiToken from NodeRegistry.getProxyTarget). Stub the singleton so
|
|
* a pilot_agent node routes through a loopback capture server and assert the
|
|
* same PROXY_ROLE_HEADER is forwarded.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import http from 'http';
|
|
import request from 'supertest';
|
|
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
|
|
import { PROXY_ROLE_HEADER } from '../services/license-headers';
|
|
import { seedPersonas } from './fixtures/personas';
|
|
import { DatabaseService } from '../services/DatabaseService';
|
|
|
|
describe('pilot-agent-mode proxy role header parity', () => {
|
|
let tmpDir: string;
|
|
let app: import('express').Express;
|
|
let server: http.Server;
|
|
let captured: http.IncomingHttpHeaders | null = null;
|
|
let pilotNodeId: number;
|
|
let personas: ReturnType<typeof seedPersonas>;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ app } = await import('../index'));
|
|
personas = seedPersonas(DatabaseService.getInstance());
|
|
|
|
// Loopback capture server that advertises cross-node-rbac.
|
|
server = http.createServer((req, res) => {
|
|
if (req.url?.startsWith('/api/meta')) {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ version: '0.96.0', capabilities: ['cross-node-rbac'] }));
|
|
return;
|
|
}
|
|
captured = req.headers;
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end('[]');
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
const port = (server.address() as import('net').AddressInfo).port;
|
|
|
|
pilotNodeId = DatabaseService.getInstance().addNode({
|
|
name: 'pilot-header-test',
|
|
type: 'remote',
|
|
mode: 'pilot_agent',
|
|
compose_dir: '/tmp',
|
|
is_default: false,
|
|
api_url: '',
|
|
api_token: '',
|
|
});
|
|
|
|
// getProxyTarget returns null for pilot_agent without a live bridge.
|
|
// Stub it to return the capture-server URL with an empty apiToken — the
|
|
// exact shape a real PilotTunnelBridge produces at runtime.
|
|
const { NodeRegistry } = await import('../services/NodeRegistry');
|
|
const registry = NodeRegistry.getInstance();
|
|
const orig = registry.getProxyTarget.bind(registry);
|
|
vi.spyOn(registry, 'getProxyTarget').mockImplementation((nid: number) => {
|
|
if (nid === pilotNodeId) return { apiUrl: `http://127.0.0.1:${port}`, apiToken: '' };
|
|
return orig(nid);
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
vi.restoreAllMocks();
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
it('forwards the deployer session role through the pilot-agent proxy path', async () => {
|
|
captured = null;
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Authorization', personas.deployer.bearer)
|
|
.set('x-node-id', String(pilotNodeId));
|
|
expect(res.status).toBe(200);
|
|
// The capture server must have received the request.
|
|
expect(captured).not.toBeNull();
|
|
expect(captured?.[PROXY_ROLE_HEADER]).toBe('deployer');
|
|
});
|
|
|
|
it('overwrites a smuggled admin header with the real deployer role on pilot path', async () => {
|
|
captured = null;
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Authorization', personas.deployer.bearer)
|
|
.set('x-node-id', String(pilotNodeId))
|
|
.set(PROXY_ROLE_HEADER, 'admin'); // smuggled
|
|
expect(res.status).toBe(200);
|
|
expect(captured).not.toBeNull();
|
|
expect(captured?.[PROXY_ROLE_HEADER]).toBe('deployer');
|
|
});
|
|
|
|
it('forwards the admin session role through the pilot-agent proxy path', async () => {
|
|
captured = null;
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Authorization', personas.admin.bearer)
|
|
.set('x-node-id', String(pilotNodeId));
|
|
expect(res.status).toBe(200);
|
|
expect(captured).not.toBeNull();
|
|
expect(captured?.[PROXY_ROLE_HEADER]).toBe('admin');
|
|
});
|
|
});
|