Files
dashcaddy/dashcaddy-api/__tests__/routes/shipdeck-fleet.routes.test.js
T
DashCaddy Polish Loop 0dd8493f98 [grade=A] Shipdeck fleet module and manual deploy flow
Adds validated fleet install/lifecycle routes, bridge-backed Git and OCI workflows, persistent service cards, and the Local-tab Shipdeck deployment UI while preserving catalog and External flows.

Judge: urn:ump:if6udffdyelsf4qvskkr65ikhuprsjiajzij6h653fhf2zgyynzq
2026-09-15 11:13:29 -07:00

71 lines
4.1 KiB
JavaScript

const express = require('express');
function fetcher(fixtures) {
return jest.fn(async (url, opts = {}) => {
const key = `${opts.method || 'GET'} ${url.replace(/^https?:\/\/[^/]+/, '')}`;
const hit = fixtures[key] || { status: 404, body: { ok: false, error: 'missing fixture' } };
return { status: hit.status, json: async () => hit.body };
});
}
function appFor(fixtures = {}, initial = []) {
process.env.SHIPDECK_BRIDGE_URL = 'http://127.0.0.1:8977';
process.env.SHIPDECK_BRIDGE_TOKEN_FILE = '';
jest.resetModules();
const make = require('../../routes/shipdeck-fleet');
let services = initial.slice();
const router = make({
asyncHandler: (fn) => async (req, res, next) => { try { await fn(req, res, next); } catch (e) { next(e); } },
log: { info: jest.fn(), warn: jest.fn(), error: jest.fn() },
auditLogger: { log: jest.fn(async () => {}) },
fetchT: fetcher(fixtures),
servicesStateManager: { read: async () => services, update: async (fn) => { services = await fn(services); } },
});
const app = express(); app.use(express.json()); app.use('/api/v1/fleet', router);
app.use((err, req, res, next) => res.status(500).json({ success: false, error: err.message }));
return { app, services: () => services };
}
async function request(app, path, options) {
const server = app.listen(0); const port = server.address().port;
try { const response = await fetch(`http://127.0.0.1:${port}${path}`, options); return { response, body: await response.json() }; }
finally { server.close(); }
}
describe('Shipdeck fleet routes', () => {
test('from-git rejects privileged inputs before bridge', async () => {
const { app } = appFor();
const { response } = await request(app, '/api/v1/fleet/from-git', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ repo_url: 'https://github.com/a/b;id', name: '../bad', subdomain: 'bad', port: 80 }) });
expect(response.status).toBe(400);
});
test('from-git persists the card server-side while never returning or storing the token', async () => {
const fixtures = { 'POST /api/install': { status: 200, body: { ok: true, service: { logo: '', host: 'localhost', shipdeckfile: '/var/lib/shipdeck/services/demo/Shipdeckfile', journal_row_id: 'demo:1' } } } };
const { app, services } = appFor(fixtures);
const secret = 'ghp_private_secret';
const { response, body } = await request(app, '/api/v1/fleet/from-git', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ repo_url: 'https://github.com/acme/demo', name: 'demo', subdomain: 'demo', port: 8080, token: secret }) });
expect(response.status).toBe(200); expect(body.success).toBe(true);
expect(JSON.stringify(body)).not.toContain(secret); expect(JSON.stringify(services())).not.toContain(secret);
expect(services()[0].managedBy).toBe('shipdeck');
expect(services()[0].shipdeckfile).toBe('/var/lib/shipdeck/services/demo/Shipdeckfile');
});
test('from-image validates mounts and registry refs', async () => {
const { app } = appFor();
const { response } = await request(app, '/api/v1/fleet/from-image', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ image: 'alpine;id', name: 'demo', subdomain: 'demo', port: 8080, mounts: [{ source: '/tmp/../etc', target: '/data' }] }) });
expect(response.status).toBe(400);
});
test('lifecycle validates service and proxies argv-shaped action', async () => {
const { app } = appFor({ 'POST /api/restart': { status: 200, body: { ok: true, output: 'RESTART demo' } } });
const { response, body } = await request(app, '/api/v1/fleet/restart', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'demo' }) });
expect(response.status).toBe(200); expect(body.action).toBe('restart');
});
test('shipdeckfile requires registered canonical path', async () => {
const { app } = appFor({}, [{ id: 'demo', managedBy: 'shipdeck', shipdeckfile: '/tmp/evil' }]);
const { response } = await request(app, '/api/v1/fleet/shipdeckfile?id=demo');
expect(response.status).toBe(404);
});
});