165 lines
7.4 KiB
TypeScript
165 lines
7.4 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import {
|
|
campaignOutcome,
|
|
campaignTitle,
|
|
type CampaignSummary
|
|
} from '../../src/lib/ui/campaigns.js';
|
|
|
|
let fixture: Fixture;
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([]);
|
|
});
|
|
afterEach(async () => {
|
|
await fixture.cleanup();
|
|
});
|
|
|
|
async function setup() {
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const handle = db();
|
|
const { GET } = await import('../../src/routes/api/campaigns/+server.js');
|
|
const get = async (
|
|
query = ''
|
|
): Promise<{ deployments: (CampaignSummary & { id: string })[]; total: number }> =>
|
|
(await GET({ url: new URL(`http://localhost/api/campaigns?${query}`) })).json();
|
|
const add = (id: string, states: string[], action = 'promote', deleted = false) => {
|
|
handle
|
|
.prepare('INSERT INTO deployments(id, action, started_at) VALUES(?,?,?)')
|
|
.run(id, action, '2026-10-06T12:00:00Z');
|
|
for (const [index, state] of states.entries()) {
|
|
handle
|
|
.prepare(
|
|
'INSERT INTO deployment_servers(deployment_id, server, files_changed, restart_state, server_deleted_at) VALUES(?,?,1,?,?)'
|
|
)
|
|
.run(
|
|
id,
|
|
`t5_zombie_theater_${index + 1}`,
|
|
state,
|
|
deleted ? '2026-10-06T13:00:00Z' : null
|
|
);
|
|
}
|
|
};
|
|
return { handle, get, add };
|
|
}
|
|
|
|
it('searches script names, campaign IDs and friendly server names across all retained history before pagination', async () => {
|
|
const { handle, get, add } = await setup();
|
|
add('old-multi-script', ['done']);
|
|
add('old-legacy-script', ['done']);
|
|
handle.exec(`INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'Double Tap','double.gsc'),(2,'100% power','power.gsc');
|
|
INSERT INTO script_versions(id,script_id,version,sha256,size) VALUES(1,1,2,'hash',0);
|
|
INSERT INTO deployment_scripts(deployment_id,script_id,script_version_id) VALUES('old-multi-script',1,1);
|
|
UPDATE deployments SET script_id=2 WHERE id='old-legacy-script';`);
|
|
for (let i = 0; i < 30; i++) add(`newer-${i}`, ['done']);
|
|
expect((await get('q=DOUBLE%20TAP&limit=1')).deployments.map((row) => row.id)).toEqual([
|
|
'old-multi-script'
|
|
]);
|
|
expect((await get('q=old-legacy&limit=1')).deployments.map((row) => row.id)).toEqual([
|
|
'old-legacy-script'
|
|
]);
|
|
expect((await get('q=%25')).deployments.map((row) => row.id)).toEqual(['old-legacy-script']);
|
|
const friendly = await get('q=Kino%20Der%20Toten&limit=1');
|
|
expect(friendly.total).toBe(32);
|
|
expect(friendly.deployments).toHaveLength(1);
|
|
expect((await get('q=not-found')).total).toBe(0);
|
|
});
|
|
|
|
it('exposes release versions for campaign titles and both campaign detail endpoints', async () => {
|
|
const { handle, get, add } = await setup();
|
|
add('aggregate-script', ['done']);
|
|
add('legacy-script', ['done']);
|
|
add('config', ['done']);
|
|
handle.exec(`
|
|
INSERT INTO scripts(id,name,target_rel_path,kind)
|
|
VALUES(1,'Double Tap','double.gsc','script'),(2,'Server settings','settings.cfg','config');
|
|
INSERT INTO script_versions(id,script_id,version,release_version,kind,sha256,size)
|
|
VALUES(1,1,2,'1.3.0','script','old',0),(2,1,11,'2.4.1','script','new',0),
|
|
(3,2,4,NULL,'config','settings',0);
|
|
INSERT INTO deployment_scripts(deployment_id,script_id,script_version_id,prior_version_id,kind)
|
|
VALUES('aggregate-script',1,2,1,'script'),('config',2,3,NULL,'config');
|
|
UPDATE deployments SET script_id=1,script_version_id=2,prior_version_id=1 WHERE id='legacy-script';
|
|
`);
|
|
const { deployments } = await get();
|
|
const versions = {
|
|
script_kind: 'script',
|
|
script_version: 11,
|
|
script_release_version: '2.4.1',
|
|
prior_version: 2,
|
|
prior_release_version: '1.3.0'
|
|
};
|
|
const aggregate = deployments.find((row) => row.id === 'aggregate-script')!;
|
|
const legacy = deployments.find((row) => row.id === 'legacy-script')!;
|
|
expect(aggregate.scripts).toEqual([expect.objectContaining(versions)]);
|
|
expect(legacy).toMatchObject(versions);
|
|
expect(campaignTitle(aggregate)).toBe('Promote · Double Tap 2.4.1 (upload 11)');
|
|
expect(campaignTitle(legacy)).toBe('Promote · Double Tap 2.4.1 (upload 11)');
|
|
expect(campaignTitle(deployments.find((row) => row.id === 'config')!)).toBe(
|
|
'Promote · Server settings v4'
|
|
);
|
|
|
|
const { GET: campaignDetail } = await import('../../src/routes/api/campaigns/[id]/+server.js');
|
|
const { GET: deploymentDetail } =
|
|
await import('../../src/routes/api/deployments/[id]/+server.js');
|
|
for (const detail of [campaignDetail, deploymentDetail]) {
|
|
const response = await (await detail({ params: { id: 'aggregate-script' } })).json();
|
|
expect(response.scripts).toEqual([expect.objectContaining(versions)]);
|
|
}
|
|
});
|
|
|
|
it('keeps API status filters and active/history partitions consistent with displayed outcomes', async () => {
|
|
const { handle, get, add } = await setup();
|
|
add('complete', ['done']);
|
|
add('deleted-complete', ['done'], 'promote', true);
|
|
add('pending', ['done', 'pending']);
|
|
add('held', ['held']);
|
|
add('manual', ['manual']);
|
|
add('failed', ['failed', 'pending']);
|
|
add('deleted-failed', ['failed'], 'promote', true);
|
|
add('withdrawn', ['skipped', 'cancelled']);
|
|
add('empty', []);
|
|
add('rollback', ['done'], 'rollback');
|
|
add('reversed', ['done']);
|
|
handle.prepare("UPDATE deployments SET rolled_back_by='rollback' WHERE id='reversed'").run();
|
|
const all = (await get('limit=100')).deployments;
|
|
for (const status of ['attention', 'progress', 'complete', 'rolled-back', 'inactive']) {
|
|
const expected = all
|
|
.filter((row) => campaignOutcome(row).status === status)
|
|
.map((row) => row.id);
|
|
const filtered = await get(`status=${status}&limit=100`);
|
|
expect(filtered.total).toBe(expected.length);
|
|
expect(filtered.deployments.map((row) => row.id)).toEqual(expected);
|
|
}
|
|
const active = await get('active=true');
|
|
expect(active.deployments.map((row) => row.id).sort()).toEqual([
|
|
'failed',
|
|
'held',
|
|
'manual',
|
|
'pending'
|
|
]);
|
|
const history = await get('view=history');
|
|
expect(history.total + active.total).toBe(all.length);
|
|
expect(
|
|
history.deployments.some((row) =>
|
|
active.deployments.some((candidate) => candidate.id === row.id)
|
|
)
|
|
).toBe(false);
|
|
});
|
|
|
|
it('combines search, type and status filters before counting and paginating history', async () => {
|
|
const { get, add } = await setup();
|
|
for (let i = 0; i < 30; i++) add(`repair-${i}`, ['done'], 'drift');
|
|
add('promote', ['done']);
|
|
add('incomplete-repair', ['pending'], 'drift');
|
|
const query = 'q=maintenance%20repair&action=drift&status=complete&view=history&limit=25';
|
|
const first = await get(query);
|
|
const second = await get(`${query}&offset=25`);
|
|
expect(first.total).toBe(30);
|
|
expect(first.deployments).toHaveLength(25);
|
|
expect(second.deployments).toHaveLength(5);
|
|
expect(new Set([...first.deployments, ...second.deployments].map((row) => row.id)).size).toBe(
|
|
30
|
|
);
|
|
expect((await get('action=rollback')).total).toBe(0);
|
|
});
|