Initial commit: Infrastructure host tracking app
build-and-push / build-and-push (push) Successful in 1m26s
build-and-push / build-and-push (push) Successful in 1m26s
Fastify + node:sqlite single-process app with vanilla JS UI for looking up hosts by hardware ID, hostname, or asset ID. Includes per-host network interface tracking, sites/rooms/server-types CRUD, Docker packaging, and a Gitea Actions workflow that runs tests then builds and pushes to gitea.thewrightserver.net/josh/infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { test, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { newApp, seedFixtures, seedHost } from './helpers.js';
|
||||
|
||||
const validIface = (host_id, name = 'eth0') => ({
|
||||
host_id,
|
||||
name,
|
||||
mac_address: 'aa:bb:cc:dd:ee:ff',
|
||||
ip_address: '10.0.0.5',
|
||||
subnet: '10.0.0.0/24',
|
||||
link_speed: '1000/full',
|
||||
});
|
||||
|
||||
test('interfaces: create, list, get, update, delete', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const host = await seedHost(app, fx, 'A');
|
||||
|
||||
const create = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: validIface(host.id, 'eth0'),
|
||||
});
|
||||
assert.equal(create.statusCode, 201);
|
||||
const iface = JSON.parse(create.body);
|
||||
assert.equal(iface.name, 'eth0');
|
||||
assert.equal(iface.host_id, host.id);
|
||||
assert.equal(iface.ip_address, '10.0.0.5');
|
||||
|
||||
const list = await app.inject({
|
||||
method: 'GET', url: `/api/interfaces?host_id=${host.id}`,
|
||||
});
|
||||
assert.equal(list.statusCode, 200);
|
||||
assert.equal(JSON.parse(list.body).length, 1);
|
||||
|
||||
const get = await app.inject({ method: 'GET', url: `/api/interfaces/${iface.id}` });
|
||||
assert.equal(get.statusCode, 200);
|
||||
assert.equal(JSON.parse(get.body).name, 'eth0');
|
||||
|
||||
const upd = await app.inject({
|
||||
method: 'PUT', url: `/api/interfaces/${iface.id}`,
|
||||
payload: { ...validIface(host.id, 'eth0'), ip_address: '10.0.0.99' },
|
||||
});
|
||||
assert.equal(upd.statusCode, 200);
|
||||
assert.equal(JSON.parse(upd.body).ip_address, '10.0.0.99');
|
||||
|
||||
const del = await app.inject({ method: 'DELETE', url: `/api/interfaces/${iface.id}` });
|
||||
assert.equal(del.statusCode, 204);
|
||||
const after404 = await app.inject({ method: 'GET', url: `/api/interfaces/${iface.id}` });
|
||||
assert.equal(after404.statusCode, 404);
|
||||
});
|
||||
|
||||
test('interfaces: optional fields may be empty strings', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const host = await seedHost(app, fx, 'B');
|
||||
|
||||
const r = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth0' },
|
||||
});
|
||||
assert.equal(r.statusCode, 201);
|
||||
const iface = JSON.parse(r.body);
|
||||
assert.equal(iface.mac_address, '');
|
||||
assert.equal(iface.ip_address, '');
|
||||
assert.equal(iface.subnet, '');
|
||||
assert.equal(iface.link_speed, '');
|
||||
});
|
||||
|
||||
test('interfaces: duplicate (host_id, name) returns 409', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const host = await seedHost(app, fx, 'C');
|
||||
|
||||
const a = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth0' },
|
||||
});
|
||||
assert.equal(a.statusCode, 201);
|
||||
|
||||
const dup = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth0' },
|
||||
});
|
||||
assert.equal(dup.statusCode, 409);
|
||||
assert.match(JSON.parse(dup.body).error, /already exists/);
|
||||
});
|
||||
|
||||
test('interfaces: same name allowed on different hosts', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const h1 = await seedHost(app, fx, 'D1');
|
||||
const h2 = await seedHost(app, fx, 'D2');
|
||||
|
||||
const a = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: h1.id, name: 'eth0' },
|
||||
});
|
||||
const b = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: h2.id, name: 'eth0' },
|
||||
});
|
||||
assert.equal(a.statusCode, 201);
|
||||
assert.equal(b.statusCode, 201);
|
||||
});
|
||||
|
||||
test('interfaces: missing host FK returns 409', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
|
||||
const r = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: 9999, name: 'eth0' },
|
||||
});
|
||||
assert.equal(r.statusCode, 409);
|
||||
});
|
||||
|
||||
test('interfaces: invalid formats return 400 with details', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const host = await seedHost(app, fx, 'E');
|
||||
|
||||
const cases = [
|
||||
{ mac_address: 'zz:zz:zz:zz:zz:zz' },
|
||||
{ ip_address: '999.0.0.1' },
|
||||
{ subnet: '10.0.0.0/99' },
|
||||
{ link_speed: '1000/weird' },
|
||||
];
|
||||
for (const extra of cases) {
|
||||
const r = await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth0', ...extra },
|
||||
});
|
||||
assert.equal(r.statusCode, 400, `expected 400 for ${JSON.stringify(extra)}`);
|
||||
const body = JSON.parse(r.body);
|
||||
assert.equal(body.error, 'validation failed');
|
||||
assert.ok(Array.isArray(body.details) && body.details.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('interfaces: deleting the host cascades', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
const fx = await seedFixtures(app);
|
||||
const host = await seedHost(app, fx, 'F');
|
||||
|
||||
await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth0' },
|
||||
});
|
||||
await app.inject({
|
||||
method: 'POST', url: '/api/interfaces',
|
||||
payload: { host_id: host.id, name: 'eth1' },
|
||||
});
|
||||
|
||||
const del = await app.inject({ method: 'DELETE', url: `/api/hosts/${host.id}` });
|
||||
assert.equal(del.statusCode, 204);
|
||||
|
||||
const list = await app.inject({
|
||||
method: 'GET', url: `/api/interfaces?host_id=${host.id}`,
|
||||
});
|
||||
assert.equal(list.statusCode, 200);
|
||||
assert.equal(JSON.parse(list.body).length, 0);
|
||||
});
|
||||
|
||||
test('interfaces: list requires host_id', async () => {
|
||||
const app = await newApp();
|
||||
after(() => app.close());
|
||||
|
||||
const r = await app.inject({ method: 'GET', url: '/api/interfaces' });
|
||||
assert.equal(r.statusCode, 400);
|
||||
});
|
||||
Reference in New Issue
Block a user