mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-16 13:38:33 +00:00
381ed2a91f
* feat: add Admiral Hardened Build channel and business assurance surfaces Introduce Studio Saelix entitlement-backed Hardened Build switching, a single-flight image operation coordinator, Recovery Vault naming, Admiral Account settings, and typed Fleet update failures while preserving Community custom-repo and targetless pull-current updates. * fix: harden image-op paths and clear CI CodeQL/pilot flake Validate operation IDs before filesystem use, use hostname checks in Fleet fetch mocks, sanitize registry probe logs, and swallow expected TCP teardown errors in the pilot reverse-route post-handshake test. * fix: sanitize image-op docker config write and probe logs Allowlist-copy registry host keys and base64 auth before writing the temp DOCKER_CONFIG, and log registry probe failures with a fixed message so CodeQL no longer flags network-to-file and log-injection mediums. * fix: address Admiral Hardened Build audit blockers Expose imageChannel so hardened Fleet peers still POST for typed rejection, claim community updates before 202, terminalize helper failures, gate Hardened on paid, and align support/docs/e2e wording. * fix: terminalize image ops on helper survival and aborted claims * fix: prevent recreating persist from overwriting helper-exit failure * test: assert helper-exit failure lands before recreating persist * fix: keep current pointer when acknowledging a stale image operation
103 lines
5.2 KiB
TypeScript
103 lines
5.2 KiB
TypeScript
/**
|
|
* Unit coverage for the cross-node-rbac capability probe used to gate
|
|
* mixed-version cross-node operations. It probes the remote's live /api/meta on
|
|
* every call (no cross-request caching), must fail closed when the capability
|
|
* cannot be determined, and must re-verify each time so a downgraded remote is
|
|
* detected immediately rather than trusted from a stale verdict.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
|
|
import type { RemoteMeta } from '../services/CapabilityRegistry';
|
|
|
|
let remoteSupportsCrossNodeRbac: typeof import('../helpers/remoteCapabilities').remoteSupportsCrossNodeRbac;
|
|
let remoteAdvertisesCapability: typeof import('../helpers/remoteCapabilities').remoteAdvertisesCapability;
|
|
let NodeRegistry: typeof import('../services/NodeRegistry').NodeRegistry;
|
|
let tmpDir: string;
|
|
|
|
const NODE_ID = 4242;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ remoteSupportsCrossNodeRbac, remoteAdvertisesCapability } = await import('../helpers/remoteCapabilities'));
|
|
({ NodeRegistry } = await import('../services/NodeRegistry'));
|
|
});
|
|
|
|
afterAll(() => cleanupTestDb(tmpDir));
|
|
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
const ONLINE = { startedAt: null, updateError: null, online: true, imagePinKind: null, updateBlocked: false, imageChannel: null } as const;
|
|
const capable: RemoteMeta = { version: '0.93.0', capabilities: ['fleet', 'cross-node-rbac'], ...ONLINE };
|
|
const incapable: RemoteMeta = { version: '0.92.0', capabilities: ['fleet', 'labels'], ...ONLINE };
|
|
|
|
describe('remoteSupportsCrossNodeRbac', () => {
|
|
it('returns true when the remote advertises cross-node-rbac', async () => {
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode').mockResolvedValue(capable);
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(true);
|
|
});
|
|
|
|
it('returns false when the remote does not advertise it', async () => {
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode').mockResolvedValue(incapable);
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(false);
|
|
});
|
|
|
|
it('fails closed when the remote is offline (empty capabilities)', async () => {
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode')
|
|
.mockResolvedValue({ version: null, capabilities: [], startedAt: null, updateError: null, online: false, imagePinKind: null, updateBlocked: false, imageChannel: null });
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(false);
|
|
});
|
|
|
|
it('trusts a reachable remote that advertises the capability even with a non-semver version', async () => {
|
|
// A 0.0.0-dev image reports version null (non-semver) but is reachable and
|
|
// genuinely advertises the capability; it must not be wrongly denied.
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode')
|
|
.mockResolvedValue({ version: null, capabilities: ['fleet', 'cross-node-rbac'], startedAt: null, updateError: null, online: true, imagePinKind: null, updateBlocked: false, imageChannel: null });
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(true);
|
|
});
|
|
|
|
it('fails closed when the meta fetch throws (unreachable)', async () => {
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode').mockRejectedValue(new Error('unreachable'));
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(false);
|
|
});
|
|
|
|
it('re-probes on every call so a downgraded remote is detected immediately (no stale verdict)', async () => {
|
|
const spy = vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode')
|
|
.mockResolvedValueOnce(capable) // first probe: current remote
|
|
.mockResolvedValue(incapable); // after the remote is swapped for older code
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(true);
|
|
expect(await remoteSupportsCrossNodeRbac(NODE_ID)).toBe(false);
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('dedupes concurrent probes for the same node into a single fetch', async () => {
|
|
const spy = vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode').mockResolvedValue(capable);
|
|
const [a, b] = await Promise.all([
|
|
remoteSupportsCrossNodeRbac(NODE_ID),
|
|
remoteSupportsCrossNodeRbac(NODE_ID),
|
|
]);
|
|
expect(a).toBe(true);
|
|
expect(b).toBe(true);
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('remoteAdvertisesCapability', () => {
|
|
it('returns true when the remote advertises the requested capability', async () => {
|
|
vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode')
|
|
.mockResolvedValue({ version: '0.93.0', capabilities: ['stack-down-remove-volumes'], ...ONLINE });
|
|
expect(await remoteAdvertisesCapability(NODE_ID, 'stack-down-remove-volumes')).toBe(true);
|
|
});
|
|
|
|
it('dedupes concurrent probes per node+capability, not per node alone', async () => {
|
|
const spy = vi.spyOn(NodeRegistry.getInstance(), 'fetchMetaForNode')
|
|
.mockResolvedValue({ version: '0.93.0', capabilities: ['cross-node-rbac', 'stack-down-remove-volumes'], ...ONLINE });
|
|
const [rbac, volumes] = await Promise.all([
|
|
remoteAdvertisesCapability(NODE_ID, 'cross-node-rbac'),
|
|
remoteAdvertisesCapability(NODE_ID, 'stack-down-remove-volumes'),
|
|
]);
|
|
expect(rbac).toBe(true);
|
|
expect(volumes).toBe(true);
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|