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
71 lines
4.1 KiB
JavaScript
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);
|
|
});
|
|
});
|