145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||
|
|
import Fastify from "fastify";
|
||
|
|
import { registerBastionRoutes } from "../src/routes/bastions.js";
|
||
|
|
import { bastionRegistry } from "../src/services/bastion-registry.js";
|
||
|
|
import type { DbClient } from "../src/server.js";
|
||
|
|
import type { BastionState } from "@lab/shared";
|
||
|
|
|
||
|
|
function createMockDb(servers: unknown[] = []): DbClient {
|
||
|
|
return {
|
||
|
|
$queryRaw: vi.fn().mockResolvedValue([{ "?column?": 1 }]),
|
||
|
|
server: {
|
||
|
|
findMany: vi.fn().mockResolvedValue(servers),
|
||
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
||
|
|
upsert: vi.fn().mockResolvedValue({}),
|
||
|
|
},
|
||
|
|
joinToken: {
|
||
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
||
|
|
findMany: vi.fn().mockResolvedValue([]),
|
||
|
|
create: vi.fn().mockResolvedValue({ id: "t" }),
|
||
|
|
update: vi.fn().mockResolvedValue({}),
|
||
|
|
},
|
||
|
|
bastion: {
|
||
|
|
upsert: vi.fn().mockResolvedValue({}),
|
||
|
|
findMany: vi.fn().mockResolvedValue([]),
|
||
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
||
|
|
update: vi.fn().mockResolvedValue({}),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerFakeBastion(bastionId: string, state: BastionState): void {
|
||
|
|
bastionRegistry.register({
|
||
|
|
bastionId,
|
||
|
|
hostname: "fake",
|
||
|
|
network: "192.168.8.0/24",
|
||
|
|
serverIp: "192.168.8.11",
|
||
|
|
// socket is referenced only on commands, not during aggregation
|
||
|
|
socket: { on: () => undefined, off: () => undefined, send: () => undefined, close: () => undefined } as never,
|
||
|
|
connectedAt: new Date(),
|
||
|
|
lastHeartbeat: new Date(),
|
||
|
|
state,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("GET /api/machines aggregation", () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
for (const b of bastionRegistry.getAll()) bastionRegistry.unregister(b.bastionId);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("promotes a live-discovered MAC to installed when the DB has a real hostname+role for it", async () => {
|
||
|
|
// Simulates the worker0-k8s0 bug: bastion restarted, lost its installed map,
|
||
|
|
// rediscovered the machine via DHCP/PXE. DB still has hostname=worker0-k8s0,
|
||
|
|
// role=infra, ip=192.168.8.23. Without the fix, the CLI sees a "discovered"
|
||
|
|
// row with no hostname/role/IP. With the fix, the row is promoted to
|
||
|
|
// "installed" with full identity preserved.
|
||
|
|
const mac = "78:55:36:08:28:fb";
|
||
|
|
registerFakeBastion("b1", {
|
||
|
|
discovered: {
|
||
|
|
[mac]: {
|
||
|
|
mac, product: "SER", board: "SER", serial: "x", manufacturer: "AZW",
|
||
|
|
cpu_model: "AMD Ryzen 7 255", cpu_cores: 16, memory_gb: 58, arch: "x86_64",
|
||
|
|
disks: [], nics: [], first_seen: "", last_seen: "",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
install_queue: {},
|
||
|
|
installed: {},
|
||
|
|
debug: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
const app = Fastify({ logger: false });
|
||
|
|
const db = createMockDb([
|
||
|
|
{ mac, hostname: "worker0-k8s0", role: "infra", ip: "192.168.8.23", status: "discovered", labels: {} },
|
||
|
|
]);
|
||
|
|
registerBastionRoutes(app, db);
|
||
|
|
|
||
|
|
const res = await app.inject({ method: "GET", url: "/api/machines" });
|
||
|
|
expect(res.statusCode).toBe(200);
|
||
|
|
const body = JSON.parse(res.body);
|
||
|
|
|
||
|
|
expect(body.discovered[mac]).toBeUndefined();
|
||
|
|
expect(body.installed[mac]).toMatchObject({
|
||
|
|
hostname: "worker0-k8s0",
|
||
|
|
role: "infra",
|
||
|
|
ip: "192.168.8.23",
|
||
|
|
cpu_model: "AMD Ryzen 7 255",
|
||
|
|
cpu_cores: 16,
|
||
|
|
memory_gb: 58,
|
||
|
|
});
|
||
|
|
|
||
|
|
await app.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("leaves a fresh-discovery MAC in discovered when DB only has a discovery-shaped record", async () => {
|
||
|
|
const mac = "aa:bb:cc:dd:ee:ff";
|
||
|
|
registerFakeBastion("b1", {
|
||
|
|
discovered: {
|
||
|
|
[mac]: {
|
||
|
|
mac, product: "SER", board: "SER", serial: "x", manufacturer: "AZW",
|
||
|
|
cpu_model: "AMD Ryzen 7", cpu_cores: 8, memory_gb: 32, arch: "x86_64",
|
||
|
|
disks: [], nics: [], first_seen: "", last_seen: "",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
install_queue: {},
|
||
|
|
installed: {},
|
||
|
|
debug: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
const app = Fastify({ logger: false });
|
||
|
|
// Matches what labd writes on first discovery: hostname=product, role="unknown"
|
||
|
|
const db = createMockDb([
|
||
|
|
{ mac, hostname: "SER", role: "unknown", ip: null, status: "discovered", labels: {} },
|
||
|
|
]);
|
||
|
|
registerBastionRoutes(app, db);
|
||
|
|
|
||
|
|
const res = await app.inject({ method: "GET", url: "/api/machines" });
|
||
|
|
const body = JSON.parse(res.body);
|
||
|
|
|
||
|
|
expect(body.discovered[mac]).toBeDefined();
|
||
|
|
expect(body.installed[mac]).toBeUndefined();
|
||
|
|
|
||
|
|
await app.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("falls back to DB for MACs not in any live bucket", async () => {
|
||
|
|
const mac = "11:22:33:44:55:66";
|
||
|
|
// No bastions connected
|
||
|
|
const app = Fastify({ logger: false });
|
||
|
|
const db = createMockDb([
|
||
|
|
{ mac, hostname: "worker1-k8s0", role: "infra", ip: "192.168.8.13", status: "online", labels: {} },
|
||
|
|
]);
|
||
|
|
registerBastionRoutes(app, db);
|
||
|
|
|
||
|
|
const res = await app.inject({ method: "GET", url: "/api/machines" });
|
||
|
|
const body = JSON.parse(res.body);
|
||
|
|
|
||
|
|
expect(body.installed[mac]).toMatchObject({
|
||
|
|
hostname: "worker1-k8s0",
|
||
|
|
role: "infra",
|
||
|
|
ip: "192.168.8.13",
|
||
|
|
});
|
||
|
|
|
||
|
|
await app.close();
|
||
|
|
});
|
||
|
|
});
|