mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 06:07:58 +00:00
79b86ddcd4
* fix(security): harden auth and outbound targets * fix(security): prevent login lockout and honor trusted schemes
140 lines
4.4 KiB
TypeScript
140 lines
4.4 KiB
TypeScript
/**
|
|
* F9 regression guard for NodeRegistry.fetchMetaForNode:
|
|
*
|
|
* - Resolves getProxyTarget for the node and delegates to fetchRemoteMeta.
|
|
* - Pilot-agent with active tunnel resolves to a loopback URL with empty
|
|
* token; the request must reach fetchRemoteMeta with that exact shape.
|
|
* - Null target (proxy-mode missing api_url/api_token, or pilot-agent
|
|
* tunnel disconnected) returns OFFLINE_META without touching the network.
|
|
*/
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import axios from 'axios';
|
|
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
|
|
|
|
let tmpDir: string;
|
|
let NodeRegistry: typeof import('../services/NodeRegistry').NodeRegistry;
|
|
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ NodeRegistry } = await import('../services/NodeRegistry'));
|
|
({ DatabaseService } = await import('../services/DatabaseService'));
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('NodeRegistry.fetchMetaForNode', () => {
|
|
it('returns OFFLINE_META when getProxyTarget is null', async () => {
|
|
const reg = NodeRegistry.getInstance();
|
|
const db = DatabaseService.getInstance();
|
|
const nodeId = db.addNode({
|
|
name: 'meta-pilot-down',
|
|
type: 'remote',
|
|
mode: 'pilot_agent',
|
|
compose_dir: '/tmp',
|
|
is_default: false,
|
|
api_url: '',
|
|
api_token: '',
|
|
});
|
|
|
|
vi.spyOn(reg, 'getProxyTarget').mockReturnValue(null);
|
|
const axiosSpy = vi.spyOn(axios, 'get');
|
|
|
|
const meta = await reg.fetchMetaForNode(nodeId);
|
|
|
|
expect(meta).toEqual({
|
|
version: null,
|
|
capabilities: [],
|
|
startedAt: null,
|
|
updateError: null,
|
|
online: false,
|
|
imagePinKind: null,
|
|
updateBlocked: false, imageChannel: null,
|
|
});
|
|
expect(axiosSpy).not.toHaveBeenCalled();
|
|
db.deleteNode(nodeId);
|
|
});
|
|
|
|
it('delegates to fetchRemoteMeta against the loopback URL for pilot-agent', async () => {
|
|
const reg = NodeRegistry.getInstance();
|
|
const db = DatabaseService.getInstance();
|
|
const nodeId = db.addNode({
|
|
name: 'meta-pilot-up',
|
|
type: 'remote',
|
|
mode: 'pilot_agent',
|
|
compose_dir: '/tmp',
|
|
is_default: false,
|
|
api_url: '',
|
|
api_token: '',
|
|
});
|
|
|
|
vi.spyOn(reg, 'getProxyTarget').mockReturnValue({
|
|
apiUrl: 'http://127.0.0.1:54321',
|
|
apiToken: '',
|
|
trustedLoopback: true,
|
|
});
|
|
const axiosSpy = vi.spyOn(axios, 'get').mockResolvedValue({
|
|
data: {
|
|
version: '0.76.7',
|
|
capabilities: ['stacks', 'containers'],
|
|
startedAt: 1234,
|
|
updateError: null,
|
|
},
|
|
});
|
|
|
|
const meta = await reg.fetchMetaForNode(nodeId);
|
|
|
|
expect(meta.version).toBe('0.76.7');
|
|
expect(meta.capabilities).toEqual(['stacks', 'containers']);
|
|
expect(meta.online).toBe(true);
|
|
|
|
expect(axiosSpy).toHaveBeenCalledTimes(1);
|
|
const url = axiosSpy.mock.calls[0][0];
|
|
expect(url).toBe('http://127.0.0.1:54321/api/meta');
|
|
const init = axiosSpy.mock.calls[0][1] as { headers: Record<string, string>; httpAgent?: unknown; httpsAgent?: unknown };
|
|
expect(init.headers).toEqual({});
|
|
expect(init.httpAgent).toBeUndefined();
|
|
expect(init.httpsAgent).toBeUndefined();
|
|
|
|
db.deleteNode(nodeId);
|
|
});
|
|
|
|
it('forwards Authorization for proxy-mode targets with non-empty tokens', async () => {
|
|
const reg = NodeRegistry.getInstance();
|
|
const db = DatabaseService.getInstance();
|
|
const nodeId = db.addNode({
|
|
name: 'meta-proxy',
|
|
type: 'remote',
|
|
mode: 'proxy',
|
|
compose_dir: '/tmp',
|
|
is_default: false,
|
|
api_url: 'https://remote.example.com:1852',
|
|
api_token: 'real-token',
|
|
});
|
|
|
|
vi.spyOn(reg, 'getProxyTarget').mockReturnValue({
|
|
apiUrl: 'https://remote.example.com:1852',
|
|
apiToken: 'real-token',
|
|
trustedLoopback: false,
|
|
});
|
|
const axiosSpy = vi.spyOn(axios, 'get').mockResolvedValue({
|
|
data: { version: '0.76.7', capabilities: [], startedAt: 1, updateError: null },
|
|
});
|
|
|
|
await reg.fetchMetaForNode(nodeId);
|
|
|
|
const init = axiosSpy.mock.calls[0][1] as { headers: Record<string, string>; httpAgent?: unknown; httpsAgent?: unknown };
|
|
expect(init.headers).toEqual({ Authorization: 'Bearer real-token' });
|
|
expect(init.httpAgent).toBeDefined();
|
|
expect(init.httpsAgent).toBeDefined();
|
|
|
|
db.deleteNode(nodeId);
|
|
});
|
|
});
|