import { describe, expect, it } from 'vitest'; import { extractUdpPort, extractContainerState } from '../../src/lib/server/integrations/komodo.js'; describe('container readiness', () => { it.each([ ['Up 35 seconds (health: starting)', 'starting'], ['Up about a minute (healthy)', 'running'], ['Up 5 minutes (unhealthy)', 'unhealthy'], ['Up 2 hours', 'running'], ['Exited (1) 5 seconds ago', 'exited'] ])('reads nested Komodo container status: %s', (status, expected) => { expect(extractContainerState({ state: 'Running', container: { state: 'running', status } })).toBe(expected); }); it('reads lowercase inspect health and does not trust missing containers or configuration', () => { expect(extractContainerState({ state: { status: 'running', running: true, health: { status: 'starting' } } })).toBe('starting'); expect(extractContainerState({ state: 'Running', container: null })).toBe('unknown'); expect(extractContainerState({ config: { state: 'running' }, status: 'ok' })).toBeNull(); expect(extractContainerState({ state: 'Exited', container: { status: 'Up 1 minute (healthy)' } })).toBe('exited'); }); it.each(['starting', 'unhealthy'])('does not accept a running container whose health is %s', (health) => { expect(extractContainerState({ State: { Status: 'running', Running: true, Health: { Status: health } } })).toBe(health); expect(extractContainerState({ state: 'running', health })).toBe(health); expect(extractContainerState({ state: 'running', health: { status: health } })).toBe(health); }); it('accepts healthy running containers and preserves stopped states', () => { expect(extractContainerState({ State: { Status: 'running', Running: true, Health: { Status: 'healthy' } } })).toBe('running'); expect(extractContainerState({ State: { Status: 'exited', Running: false, Health: { Status: 'healthy' } } })).toBe('exited'); }); }); describe('extractUdpPort', () => { it('returns the published port from docker-compose-style object entries', () => { const payload = { state: { container: { ports: [ { protocol: 'tcp', published: 3000, target: 3000 }, { protocol: 'udp', published: 4976, target: 28960 } ] } } }; expect(extractUdpPort(payload)).toBe(4976); }); it('parses docker-run-style "host:container/proto" strings', () => { const payload = { ports: ['3074:3074/tcp', '4977:28960/udp'] }; expect(extractUdpPort(payload)).toBe(4977); }); it('picks the UDP entry even when TCP appears first', () => { const payload = { deeply: { nested: { ports: [ { Protocol: 'tcp', PublishedPort: 80 }, { Protocol: 'udp', PublishedPort: 5555 } ] } } }; expect(extractUdpPort(payload)).toBe(5555); }); it('returns null when no UDP mapping exists', () => { const payload = { ports: [{ protocol: 'tcp', published: 3000 }] }; expect(extractUdpPort(payload)).toBeNull(); }); it('survives cyclic references', () => { const cyc: Record = {}; cyc.self = cyc; expect(extractUdpPort(cyc)).toBeNull(); }); it('recognizes HostPort / hostPort / host_port on object entries', () => { expect(extractUdpPort({ ports: [{ protocol: 'udp', HostPort: 28962 }] })).toBe(28962); expect(extractUdpPort({ ports: [{ protocol: 'udp', hostPort: 28963 }] })).toBe(28963); expect(extractUdpPort({ ports: [{ protocol: 'udp', host_port: 28964 }] })).toBe(28964); }); it('recognizes `type` alongside `protocol` as the protocol field', () => { expect(extractUdpPort({ ports: [{ type: 'UDP', published: 28965 }] })).toBe(28965); }); it('parses host-network "container/udp" strings', () => { expect(extractUdpPort({ ports: ['28966/udp'] })).toBe(28966); }); it('handles docker-inspect container-keyed PortBindings shape', () => { const payload = { NetworkSettings: { Ports: { '28960/udp': [{ HostIp: '0.0.0.0', HostPort: '28967' }], '3074/tcp': [{ HostIp: '0.0.0.0', HostPort: '3074' }] } } }; expect(extractUdpPort(payload)).toBe(28967); }); it('prefers `ports`-keyed arrays over unrelated arrays discovered deeper', () => { // The unrelated array happens to contain a matching entry — the walker // must still prefer the real `ports` list. const payload = { labels: [{ protocol: 'udp', published: 9999 }], state: { container: { ports: [{ protocol: 'udp', published: 28968 }] } } }; expect(extractUdpPort(payload)).toBe(28968); }); it('splits Komodo`s newline-delimited string form of `config.ports`', () => { // Verbatim shape from a real Komodo GetDeployment response — the // container publishes UDP 28963 and TCP 28963 as a single string. const payload = { name: 't5-nacht-1', config: { image: { type: 'Image', params: { image: 'ghcr.io/ayymoss/plutainer:latest' } }, network: 'komodo_default', ports: '28963:28963/udp\n28963:28963/tcp\n' } }; expect(extractUdpPort(payload)).toBe(28963); }); it('handles a single-line string-form ports value', () => { expect(extractUdpPort({ config: { ports: '28969:28960/udp' } })).toBe(28969); }); it('handles a host-network string-form ports value (no host prefix)', () => { expect(extractUdpPort({ config: { ports: '28970/udp\n' } })).toBe(28970); }); it('ignores TCP-only string-form ports entries', () => { expect(extractUdpPort({ config: { ports: '80:80/tcp\n443:443/tcp' } })).toBeNull(); }); }); describe('komodoResolvePortDetailed', () => { it('is importable and has the discriminated-union shape at the type level', async () => { // Runtime import shim just to prove it's exported. Behavior is // covered by the fixture-based extractUdpPort tests above plus manual // end-to-end verification against a live Komodo. const mod = await import('../../src/lib/server/integrations/komodo.js'); expect(typeof mod.komodoResolvePortDetailed).toBe('function'); expect(typeof mod.komodoResolveGamePort).toBe('function'); }); });