mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
150 lines
6.2 KiB
TypeScript
150 lines
6.2 KiB
TypeScript
import * as assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import {
|
|
firstPublishedHostPort,
|
|
mergeContainerConfigPreservingHostPorts,
|
|
mergeUiLocationPreservingHostPort,
|
|
} from '../../app/utils/service_catalog_merge.js'
|
|
|
|
const config = (bindings: Record<string, string>, extra: Record<string, unknown> = {}) =>
|
|
JSON.stringify({
|
|
Image: 'ghcr.io/example/app:1.0.0',
|
|
...extra,
|
|
HostConfig: {
|
|
PortBindings: Object.fromEntries(
|
|
Object.entries(bindings).map(([containerPort, hostPort]) => [
|
|
containerPort,
|
|
[{ HostPort: hostPort }],
|
|
])
|
|
),
|
|
},
|
|
})
|
|
|
|
// ── container_config ────────────────────────────────────────────────────────
|
|
|
|
test('keeps the live host port when it diverges from the catalog default', () => {
|
|
const merged = mergeContainerConfigPreservingHostPorts(
|
|
config({ '8080/tcp': '8090' }),
|
|
config({ '8080/tcp': '9999' })
|
|
)
|
|
assert.equal(firstPublishedHostPort(merged), '9999')
|
|
})
|
|
|
|
test('applies other catalog changes while keeping the port', () => {
|
|
const merged = mergeContainerConfigPreservingHostPorts(
|
|
config({ '8080/tcp': '8090' }, { Image: 'ghcr.io/example/app:2.0.0' }),
|
|
config({ '8080/tcp': '9999' }, { Image: 'ghcr.io/example/app:1.0.0' })
|
|
)
|
|
assert.equal(JSON.parse(merged).Image, 'ghcr.io/example/app:2.0.0')
|
|
assert.equal(firstPublishedHostPort(merged), '9999')
|
|
})
|
|
|
|
test('returns the catalog value untouched when the port already matches', () => {
|
|
const catalog = config({ '8080/tcp': '8090' })
|
|
assert.equal(mergeContainerConfigPreservingHostPorts(catalog, config({ '8080/tcp': '8090' })), catalog)
|
|
})
|
|
|
|
test('a catalog change to the CONTAINER-side port wins', () => {
|
|
// The app now listens on 3000 inside the container. The live 8080 binding is a
|
|
// different key, so there is nothing to preserve and the catalog is authoritative.
|
|
const merged = mergeContainerConfigPreservingHostPorts(
|
|
config({ '3000/tcp': '8090' }),
|
|
config({ '8080/tcp': '9999' })
|
|
)
|
|
assert.equal(firstPublishedHostPort(merged), '8090')
|
|
})
|
|
|
|
test('preserves each port independently on a multi-port app', () => {
|
|
const merged = mergeContainerConfigPreservingHostPorts(
|
|
config({ '8080/tcp': '8310', '8081/tcp': '8311' }),
|
|
config({ '8080/tcp': '9310', '8081/tcp': '8311' })
|
|
)
|
|
const bindings = JSON.parse(merged).HostConfig.PortBindings
|
|
assert.equal(bindings['8080/tcp'][0].HostPort, '9310')
|
|
assert.equal(bindings['8081/tcp'][0].HostPort, '8311')
|
|
})
|
|
|
|
// `services.container_config` is a MySQL json column, so the live value arrives as
|
|
// an already-parsed OBJECT, not a string, despite the model typing it `string | null`.
|
|
// A string-only implementation passes every other test in this file and silently
|
|
// no-ops against a real database. Verified live before adding these.
|
|
test('accepts an already-parsed live config object (MySQL json column)', () => {
|
|
const merged = mergeContainerConfigPreservingHostPorts(
|
|
config({ '80/tcp': '8410' }),
|
|
JSON.parse(config({ '80/tcp': '9410' }))
|
|
)
|
|
assert.equal(firstPublishedHostPort(merged), '9410')
|
|
})
|
|
|
|
test('ui_location honours a parsed live config object too', () => {
|
|
assert.equal(
|
|
mergeUiLocationPreservingHostPort('8410', '9410', JSON.parse(config({ '80/tcp': '9410' }))),
|
|
'9410'
|
|
)
|
|
})
|
|
|
|
test('unparseable or portless live config falls back to the catalog', () => {
|
|
const catalog = config({ '8080/tcp': '8090' })
|
|
assert.equal(mergeContainerConfigPreservingHostPorts(catalog, 'not json'), catalog)
|
|
assert.equal(mergeContainerConfigPreservingHostPorts(catalog, null), catalog)
|
|
assert.equal(mergeContainerConfigPreservingHostPorts(catalog, JSON.stringify({})), catalog)
|
|
})
|
|
|
|
// ── ui_location ─────────────────────────────────────────────────────────────
|
|
|
|
test('rewrites a bare port to the live one', () => {
|
|
assert.equal(
|
|
mergeUiLocationPreservingHostPort('8090', '9999', config({ '8080/tcp': '9999' })),
|
|
'9999'
|
|
)
|
|
})
|
|
|
|
test('takes the catalog scheme and the live port', () => {
|
|
// Vaultwarden moved to https in the catalog; this install runs on 9480.
|
|
assert.equal(
|
|
mergeUiLocationPreservingHostPort('https:8480', '9480', config({ '80/tcp': '9480' })),
|
|
'https:9480'
|
|
)
|
|
})
|
|
|
|
test('a path ui_location is never rewritten', () => {
|
|
assert.equal(mergeUiLocationPreservingHostPort('/chat', '/chat', null), '/chat')
|
|
assert.equal(mergeUiLocationPreservingHostPort('/chat', '9999', config({ '80/tcp': '9999' })), '/chat')
|
|
})
|
|
|
|
// The third argument is the config the sync is about to WRITE, not the live row.
|
|
// When the catalog moves the container-side port, the port merge keeps nothing, so
|
|
// the link has to take the catalog port too. Feeding the live config here instead
|
|
// writes a row whose container binds 8090 while its Open button points at 9999.
|
|
test('follows the catalog when a container-side port change wins', () => {
|
|
const catalogConfig = config({ '3000/tcp': '8090' })
|
|
const liveConfig = config({ '8080/tcp': '9999' })
|
|
const mergedConfig = mergeContainerConfigPreservingHostPorts(catalogConfig, liveConfig)
|
|
|
|
assert.equal(firstPublishedHostPort(mergedConfig), '8090')
|
|
assert.equal(mergeUiLocationPreservingHostPort('8090', '9999', mergedConfig), '8090')
|
|
})
|
|
|
|
test('a preserved host port still reaches ui_location through the merged config', () => {
|
|
const mergedConfig = mergeContainerConfigPreservingHostPorts(
|
|
config({ '8080/tcp': '8090' }),
|
|
config({ '8080/tcp': '9999' })
|
|
)
|
|
assert.equal(mergeUiLocationPreservingHostPort('8090', '9999', mergedConfig), '9999')
|
|
})
|
|
|
|
test('ignores a live port the install does not actually publish', () => {
|
|
// Stale or hand-edited ui_location: pinning the link to it would point the
|
|
// Open button at a port with nothing behind it.
|
|
assert.equal(
|
|
mergeUiLocationPreservingHostPort('8090', '7777', config({ '8080/tcp': '8090' })),
|
|
'8090'
|
|
)
|
|
})
|
|
|
|
test('null or missing values fall back to the catalog', () => {
|
|
assert.equal(mergeUiLocationPreservingHostPort('8090', null, null), '8090')
|
|
assert.equal(mergeUiLocationPreservingHostPort(null, '9999', null), null)
|
|
})
|