Files
peardock/test/compose.test.js
T
Raven Scott 67dffcadb8
Release rolling / release (push) Has been cancelled
Remove networks on compose down
2026-07-16 04:40:50 -04:00

189 lines
4.7 KiB
JavaScript

import test from 'brittle'
import {
parseComposeFile,
validateComposeFile,
removeComposeStack,
} from '../server/utils/composeManager.js'
/**
* Minimal dockerode-shaped mock for removeComposeStack.
* @param {{ containers?: object[], networks?: object[] }} seed
*/
function mockDocker(seed = {}) {
const containers = [...(seed.containers || [])]
const networks = [...(seed.networks || [])]
const removedNetworks = []
const removedContainers = []
return {
removedNetworks,
removedContainers,
listContainers: async () => containers,
listNetworks: async () => networks,
getContainer: (id) => ({
stop: async () => {},
remove: async () => {
removedContainers.push(id)
const i = containers.findIndex((c) => c.Id === id)
if (i >= 0) containers.splice(i, 1)
},
}),
getNetwork: (id) => ({
remove: async () => {
const n = networks.find((x) => x.Id === id)
if (!n) throw new Error(`network ${id} not found`)
removedNetworks.push({ id, name: n.Name })
const i = networks.findIndex((x) => x.Id === id)
if (i >= 0) networks.splice(i, 1)
},
}),
}
}
const SAMPLE = `
version: "3.8"
services:
web:
image: nginx:alpine
ports:
- "8080:80"
environment:
FOO: bar
BAZ: "1"
volumes:
- ./data:/data:ro
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
networks:
- backend
networks:
backend:
volumes:
pgdata:
`
test('parseComposeFile uses js-yaml and normalizes services', (t) => {
const parsed = parseComposeFile(SAMPLE)
t.is(parsed.version, '3.8')
t.ok(parsed.services.web)
t.ok(parsed.services.db)
t.is(parsed.services.web.image, 'nginx:alpine')
t.alike(parsed.services.web.ports, ['8080:80'])
t.ok(parsed.services.web.environment.includes('FOO=bar'))
t.ok(parsed.services.web.depends_on.includes('db'))
t.is(parsed.services.web.restart, 'unless-stopped')
t.ok(parsed.services.db.environment.some((e) => e.includes('POSTGRES_PASSWORD')))
t.ok(Object.prototype.hasOwnProperty.call(parsed.networks, 'backend'))
})
test('parseComposeFile rejects invalid YAML', (t) => {
try {
parseComposeFile('services: [')
t.fail('should throw')
} catch (err) {
t.ok(/Invalid compose YAML/i.test(err.message))
}
})
test('parseComposeFile requires services mapping', (t) => {
try {
parseComposeFile('version: "3"\n')
t.fail('should throw')
} catch (err) {
t.ok(/services/i.test(err.message))
}
})
test('validateComposeFile requires image or build', (t) => {
try {
validateComposeFile(`
services:
bad:
command: echo hi
`)
t.fail('should throw')
} catch (err) {
t.ok(/image or build/i.test(err.message))
}
})
test('validateComposeFile accepts build-only service', (t) => {
const parsed = validateComposeFile(`
services:
app:
build: .
`)
t.ok(parsed.services.app.build)
})
test('removeComposeStack removes project networks after containers', async (t) => {
const docker = mockDocker({
containers: [
{
Id: 'c1',
State: 'running',
Labels: { 'com.docker.compose.project': 'mystack' },
},
],
networks: [
{
Id: 'n1',
Name: 'mystack_default',
Labels: { 'com.docker.compose.project': 'mystack' },
},
{
Id: 'n2',
Name: 'other_default',
Labels: { 'com.docker.compose.project': 'other' },
},
{
Id: 'bridge',
Name: 'bridge',
Labels: {},
},
],
})
const result = await removeComposeStack(docker, 'mystack')
t.is(result.removed, 1)
t.is(result.networksRemoved, 1)
t.alike(docker.removedContainers, ['c1'])
t.is(docker.removedNetworks.length, 1)
t.is(docker.removedNetworks[0].name, 'mystack_default')
})
test('removeComposeStack cleans orphaned project networks with no containers', async (t) => {
const docker = mockDocker({
containers: [],
networks: [
{
Id: 'n1',
Name: 'orphan_backend',
Labels: { 'com.docker.compose.project': 'orphan' },
},
],
})
const result = await removeComposeStack(docker, 'orphan')
t.is(result.removed, 0)
t.is(result.networksRemoved, 1)
t.is(docker.removedNetworks[0].name, 'orphan_backend')
})
test('removeComposeStack reports already removed when nothing matches', async (t) => {
const docker = mockDocker({
containers: [],
networks: [{ Id: 'bridge', Name: 'bridge', Labels: {} }],
})
const result = await removeComposeStack(docker, 'missing')
t.is(result.removed, 0)
t.is(result.networksRemoved, 0)
t.ok(/not found|already removed/i.test(result.message))
})