komodoResolveGamePort silently returned null for six distinct failure
modes (unconfigured, no-name, unreachable, http-error, unparseable,
no-udp-in-response), which made "no game_port — Komodo did not return
one" indistinguishable from an auth failure or an unrecognized response
shape. Adds komodoResolvePortDetailed returning a discriminated union
with the failure reason, HTTP status, and a truncated response snippet.
extractUdpPort broadens to cover four more shapes seen in the wild:
extra host-port field names (HostPort/hostPort/host_port/PublicPort),
`type` as a protocol alias, host-network strings ("28962/udp"), and the
docker-inspect container-keyed form ({ "28960/udp": [{ HostPort }] }).
Arrays reached via a ports-flavored key now win over accidental matches
elsewhere in the tree.
New POST /api/servers/[name]/probe-komodo triggers a fresh lookup,
caches the port on success, and returns the structured result. The RCON
diagnostic panel gains a Probe Komodo button (shown when endpoint is
unresolved and Komodo is configured) that renders the snippet inline —
so unrecognized shapes surface directly instead of vanishing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { extractUdpPort } from '../../src/lib/server/integrations/komodo.js';
|
|
|
|
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<string, unknown> = {};
|
|
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);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|