Files

128 lines
6.1 KiB
TypeScript

/**
* Unit tests for resolveRestoreTargets - the single source of truth for where a restore lands.
*
* The load-bearing guarantee: for IN-PLACE, the resolved target MUST equal what the swap actually
* binds, i.e. resolveBindFromMetadata(meta, key).source. These tests assert that identity directly
* so the preview can never show a path the restore doesn't write. Clone targets are verbatim from
* volumeDestinations; stack files come from the passed stackDir.
*/
import { describe, expect, test } from 'bun:test';
import { resolveRestoreTargets } from '../../src/lib/server/backups/restore-targets';
import { resolveBindFromMetadata, type SnapshotMetadata } from '../../src/lib/server/backups/restore-core';
describe('resolveRestoreTargets - in-place (target === what the swap binds)', () => {
const meta: SnapshotMetadata = {
volumes: [
{ key: 'html', name: '/srv/app/html', source: '/srv/app/html', type: 'bind' },
{ key: 'pgdata', name: 'pgdata', source: 'pgdata', type: 'volume' },
],
};
test('bind volume target equals resolveBindFromMetadata source (identity)', () => {
const r = resolveRestoreTargets({ job: { mode: 'in-place' }, volumes: ['html'], metadata: meta, stackDir: null });
expect(r.volumes[0]).toMatchObject({ key: 'html', type: 'bind', target: '/srv/app/html', origin: 'in-place-metadata' });
// the exact identity the preview relies on - target AND the full bind equal the leaf fn:
expect(r.volumes[0].target).toBe(resolveBindFromMetadata(meta, 'html').source);
expect(r.volumes[0].bind).toBe(resolveBindFromMetadata(meta, 'html').bind);
});
test('named volume target equals the volume name', () => {
const r = resolveRestoreTargets({ job: { mode: 'in-place' }, volumes: ['pgdata'], metadata: meta, stackDir: null });
expect(r.volumes[0]).toMatchObject({ key: 'pgdata', type: 'volume', target: 'pgdata', origin: 'in-place-metadata', bind: 'pgdata:/volumes/pgdata:rw' });
expect(r.volumes[0].target).toBe(resolveBindFromMetadata(meta, 'pgdata').source);
});
test('a bind whose source cannot be recovered goes to unresolved, does NOT throw', () => {
// legacy string list: 'data_config' slugs from a bind path but no absolute source recorded.
const bad: SnapshotMetadata = { volumes: ['/data/config'] };
const r = resolveRestoreTargets({ job: { mode: 'in-place' }, volumes: ['data_config'], metadata: bad, stackDir: null });
expect(r.volumes).toEqual([]);
expect(r.unresolved).toHaveLength(1);
expect(r.unresolved[0].key).toBe('data_config');
});
});
describe('resolveRestoreTargets - clone (verbatim from volumeDestinations)', () => {
const meta: SnapshotMetadata = {
volumes: [{ key: 'html', name: '/orig/html', source: '/orig/html', type: 'bind' }],
};
test('a path destination is used verbatim as a bind host path', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', volumeDestinations: [{ volume: 'html', kind: 'path', target: '/new/custom/html' }] },
volumes: ['html'], metadata: meta, stackDir: null,
});
expect(r.volumes[0]).toMatchObject({ key: 'html', type: 'bind', target: '/new/custom/html', origin: 'clone', include: '/volumes/html', bind: '/new/custom/html:/volumes/html:rw' });
});
test('a volume destination is a named volume target', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', volumeDestinations: [{ volume: 'html', kind: 'volume', target: 'html_clone' }] },
volumes: ['html'], metadata: meta, stackDir: null,
});
expect(r.volumes[0]).toMatchObject({ key: 'html', type: 'volume', target: 'html_clone', origin: 'clone', bind: 'html_clone:/volumes/html:rw' });
});
test('an unmapped volume falls back to loose-files under targetPath', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', targetPath: '/restore/here', volumeDestinations: [] },
volumes: ['html'], metadata: meta, stackDir: null,
});
expect(r.volumes[0]).toMatchObject({ key: 'html', type: 'bind', target: '/restore/here', origin: 'loose-files', bind: '/restore/here:/restore/here' });
});
test('an unmapped volume with no targetPath is unresolved (fail-closed, no throw)', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', volumeDestinations: [] },
volumes: ['html'], metadata: meta, stackDir: null,
});
expect(r.volumes).toEqual([]);
expect(r.unresolved[0].key).toBe('html');
});
});
describe('resolveRestoreTargets - stack files', () => {
test('writes the managed stack dir for a stack restore', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', targetType: 'stack', targetName: 'blog' },
volumes: [], metadata: null, stackDir: '/app/data/stacks/anton/blog',
});
expect(r.stackFiles).toEqual({ targetDir: '/app/data/stacks/anton/blog', willWrite: true, overwrite: true });
});
test('skipStackFiles -> willWrite false (data-only restore)', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', targetType: 'stack', targetName: 'blog', skipStackFiles: true },
volumes: [], metadata: null, stackDir: '/app/data/stacks/anton/blog',
});
expect(r.stackFiles?.willWrite).toBe(false);
});
test('mergeStackFiles -> overwrite false', () => {
const r = resolveRestoreTargets({
job: { mode: 'new-location', targetType: 'stack', targetName: 'blog', mergeStackFiles: true },
volumes: [], metadata: null, stackDir: '/app/data/stacks/anton/blog',
});
expect(r.stackFiles?.overwrite).toBe(false);
});
test('a container restore has no stackFiles', () => {
const r = resolveRestoreTargets({
job: { mode: 'in-place', targetType: 'container', targetName: 'nginx' },
volumes: [], metadata: null, stackDir: null,
});
expect(r.stackFiles).toBeNull();
});
});
describe('resolveRestoreTargets - the stack-dir synthetic volume is not a user volume', () => {
test('the reserved __dockhand_stackdir__ key is ignored in volumes (handled via stackFiles)', () => {
const r = resolveRestoreTargets({
job: { mode: 'in-place', targetType: 'stack', targetName: 'blog' },
volumes: ['__dockhand_stackdir__'], metadata: { volumes: [] }, stackDir: '/app/data/stacks/anton/blog',
});
expect(r.volumes).toEqual([]);
expect(r.unresolved).toEqual([]);
});
});