Files
sencho/frontend/src/lib/networkExposureSummary.test.ts
T
Anso 2eafee3594 fix(networking): treat host-network services as host-exposed in summaries (#1430)
The exposure summaries derived a stack's exposure solely from the declared
published-port list, so a service running with network_mode: host (which
publishes every container port on the host but declares no ports:) was
under-reported as less exposed than it actually is.

Capture network_mode in the lightweight dependency parser, add an
isHostNetwork predicate, and treat a host-network service as exposed and
publishing across the Fleet networking summary, the Stack Dossier export, and
the Networking panel, matching how the Compose Doctor already flags host
networking.
2026-06-24 19:50:45 -04:00

74 lines
3.6 KiB
TypeScript

/**
* The redacted networking + exposure summary for the dossier export: it carries
* only names, intents, port numbers, and binding scope, never env or label
* values, and renders nothing when there is nothing to document.
*/
import { describe, it, expect } from 'vitest';
import { buildNetworkExposureSummary, networkExposureSection } from './networkExposureSummary';
const facts = (over: Record<string, unknown> = {}) => ({
renderable: true,
networks: [{ name: 'app_backend', external: false, internal: true }],
services: [{
name: 'web',
publishedPorts: [
{ startPort: 8080, endPort: 8080, protocol: 'tcp', allInterfaces: true, loopbackOnly: false },
{ startPort: 9000, endPort: 9000, protocol: 'tcp', allInterfaces: false, loopbackOnly: true },
],
}],
...over,
});
describe('buildNetworkExposureSummary', () => {
it('returns null when the model is not renderable', () => {
expect(buildNetworkExposureSummary({ renderable: false }, [])).toBeNull();
});
it('returns null when there is nothing worth documenting', () => {
expect(buildNetworkExposureSummary({ renderable: true, networks: [], services: [{ name: 'web', publishedPorts: [] }] }, [])).toBeNull();
});
it('summarizes networks, intents, and ports with their binding scope', () => {
const s = buildNetworkExposureSummary(facts(), [{ service: '', intent: 'internal' }, { service: 'web', intent: 'public' }]);
expect(s).toEqual({
stackIntent: 'internal',
networks: [{ name: 'app_backend', external: false, internal: true }],
services: [{ name: 'web', intent: 'public', ports: ['8080/tcp (all interfaces)', '9000/tcp (loopback)'], hostNetwork: false }],
});
});
it('flags a host-network service as host-exposed even with no published ports', () => {
const s = buildNetworkExposureSummary({ renderable: true, networks: [], services: [{ name: 'app', publishedPorts: [], networkMode: 'host' }] }, []);
expect(s).toEqual({
stackIntent: null,
networks: [],
services: [{ name: 'app', intent: null, ports: [], hostNetwork: true }],
});
});
});
describe('networkExposureSection', () => {
it('renders a section with the redacted facts', () => {
const md = networkExposureSection(buildNetworkExposureSummary(facts(), [{ service: '', intent: 'public' }]));
expect(md).toContain('## Network exposure');
expect(md).toContain('**Stack intent:** public');
expect(md).toContain('app_backend (internal)');
expect(md).toContain('8080/tcp (all interfaces)');
});
it('returns null for a null summary', () => {
expect(networkExposureSection(null)).toBeNull();
});
it('renders the host-network phrase for a host-mode service', () => {
const md = networkExposureSection(buildNetworkExposureSummary({ renderable: true, networks: [], services: [{ name: 'app', publishedPorts: [], networkMode: 'host' }] }, [])) ?? '';
expect(md).toContain('host network (all ports exposed on host)');
});
it('never includes a value that lives in an ignored field (no env or label leak)', () => {
// A secret planted in fields the builder does not read must not surface.
const leaky = facts({
services: [{ name: 'web', publishedPorts: [], env: { TOKEN: 'SECRET-9f3a' }, labels: { x: 'LABEL-SECRET' } }],
networks: [{ name: 'app_backend', external: false, internal: false, driver: 'SECRET-DRIVER' }],
});
const md = networkExposureSection(buildNetworkExposureSummary(leaky, [{ service: '', intent: 'internal' }])) ?? '';
expect(md).not.toContain('SECRET-9f3a');
expect(md).not.toContain('LABEL-SECRET');
expect(md).not.toContain('SECRET-DRIVER');
});
});