129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
/**
|
|
* Unit tests for the "Append to existing" merge helpers (#489): merging a generated
|
|
* single-service compose into an existing stack compose, and computing the highlight range
|
|
* of the added service. No browser needed - pure js-yaml logic.
|
|
*
|
|
* Run with: bun test tests/compose-merge.test.ts
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import yaml from 'js-yaml';
|
|
import { mergeServiceIntoCompose, computeAddedRange } from '../src/lib/utils/compose-merge';
|
|
|
|
const BASE = `services:
|
|
web:
|
|
image: nginx:latest
|
|
ports:
|
|
- '8080:80'
|
|
`;
|
|
|
|
describe('mergeServiceIntoCompose', () => {
|
|
test('adds a new service under services without touching the existing one', () => {
|
|
const service = `services:
|
|
redis:
|
|
image: redis:7
|
|
`;
|
|
const { merged, key, renamed } = mergeServiceIntoCompose(BASE, service);
|
|
expect(key).toBe('redis');
|
|
expect(renamed).toBe(false);
|
|
const doc = yaml.load(merged) as any;
|
|
expect(Object.keys(doc.services).sort()).toEqual(['redis', 'web']);
|
|
expect(doc.services.web.image).toBe('nginx:latest');
|
|
expect(doc.services.redis.image).toBe('redis:7');
|
|
});
|
|
|
|
test('auto-suffixes on a name clash instead of overwriting', () => {
|
|
const service = `services:
|
|
web:
|
|
image: caddy:2
|
|
`;
|
|
const { merged, key, renamed } = mergeServiceIntoCompose(BASE, service);
|
|
expect(key).toBe('web-2');
|
|
expect(renamed).toBe(true);
|
|
const doc = yaml.load(merged) as any;
|
|
// original web is untouched, the added one is web-2
|
|
expect(doc.services.web.image).toBe('nginx:latest');
|
|
expect(doc.services['web-2'].image).toBe('caddy:2');
|
|
});
|
|
|
|
test('picks the lowest free suffix when -2 is also taken', () => {
|
|
const base = `services:
|
|
web:
|
|
image: nginx:latest
|
|
web-2:
|
|
image: nginx:1.25
|
|
`;
|
|
const service = `services:
|
|
web:
|
|
image: caddy:2
|
|
`;
|
|
const { key } = mergeServiceIntoCompose(base, service);
|
|
expect(key).toBe('web-3');
|
|
});
|
|
|
|
test('carries over top-level networks and volumes the service references', () => {
|
|
const service = `services:
|
|
db:
|
|
image: postgres:16
|
|
networks:
|
|
- backend
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
networks:
|
|
backend:
|
|
external: true
|
|
volumes:
|
|
pgdata:
|
|
external: true
|
|
`;
|
|
const { merged } = mergeServiceIntoCompose(BASE, service);
|
|
const doc = yaml.load(merged) as any;
|
|
expect(doc.networks.backend).toEqual({ external: true });
|
|
expect(doc.volumes.pgdata).toEqual({ external: true });
|
|
});
|
|
|
|
test('handles an empty base compose (creates services)', () => {
|
|
const service = `services:
|
|
app:
|
|
image: alpine
|
|
`;
|
|
const { merged, key } = mergeServiceIntoCompose('', service);
|
|
expect(key).toBe('app');
|
|
expect((yaml.load(merged) as any).services.app.image).toBe('alpine');
|
|
});
|
|
});
|
|
|
|
describe('computeAddedRange', () => {
|
|
test('returns the exact block of the added service', () => {
|
|
const service = `services:
|
|
redis:
|
|
image: redis:7
|
|
`;
|
|
const { merged, key } = mergeServiceIntoCompose(BASE, service);
|
|
const [range] = computeAddedRange(merged, key);
|
|
const lines = merged.split('\n');
|
|
// the highlighted range must start at the ` redis:` line
|
|
expect(lines[range.line - 1]).toMatch(/^\s+redis:/);
|
|
// and every highlighted line must be inside the redis block (indent > 2) or the header
|
|
for (let l = range.line; l <= range.endLine; l++) {
|
|
expect(lines[l - 1].startsWith(' ')).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('does NOT over-highlight the trailing blank line when the service is last', () => {
|
|
// merged doc ends with the added service and a trailing newline from yaml.dump.
|
|
const service = `services:
|
|
last:
|
|
image: busybox
|
|
`;
|
|
const { merged, key } = mergeServiceIntoCompose(BASE, service);
|
|
const [range] = computeAddedRange(merged, key);
|
|
const lines = merged.split('\n');
|
|
// endLine must be a real content line, not the trailing empty one.
|
|
expect(lines[range.endLine - 1].trim()).not.toBe('');
|
|
});
|
|
|
|
test('returns empty when the key is absent', () => {
|
|
expect(computeAddedRange(BASE, 'nope')).toEqual([]);
|
|
});
|
|
});
|