Files
netris-nestri/apps/api/test/heartbeat.test.ts
Wanjohi c3682136f1 test(api): hold the heartbeat wire contract from the host's side
`lastSeen` and `intervalSeconds` are the two field names neslet's plane.rs
reads out of the reply, and a rename on either side yields a host that beats,
parses nothing and reports success. These tests are what make that a contract
rather than a coincidence.

Also asserts the property the middleware comment claims and nothing checked:
wrong machine credentials and no credentials produce *identical* responses,
because bad credentials fall through to `public` rather than erroring so that
probing cannot reveal which machine ids exist. Comparing the two bodies is the
only way that stays true.

Two of these tests started out asserting 401 and were wrong, not the code —
`machineOnly` sees a public actor either way and forbids.

138 tests, 0 fail.
2026-09-03 21:46:30 +03:00

108 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterAll, describe, expect, test } from 'bun:test';
import { Fixtures } from '@nestri/core/db/fixtures';
import { testDb } from '@nestri/core/db/test';
import { Identifier } from '@nestri/core/id';
import { Machine } from '@nestri/core/machine/index';
import { app } from '../app/index';
import './setup';
const sql = testDb();
const createdUserIds: string[] = [];
/**
* A registered host, with the secret kept — which registration returns exactly
* once, so a test that needs to authenticate as a machine has to hold onto it
* here rather than reading it back later.
*/
async function registeredHost(label: string) {
const owner = await Fixtures.owner(label);
createdUserIds.push(owner.userId);
const registered = await Machine.register({
id: Identifier.ascending('machine'),
ownerUserId: owner.userId,
teamId: owner.teamId,
label
});
return {
id: registered.id,
headers: {
'x-nestri-machine-id': registered.id,
'x-nestri-machine-secret': registered.secret
}
};
}
afterAll(async () => {
if (createdUserIds.length > 0) {
await sql`delete from "user" where id in ${sql(createdUserIds)}`;
createdUserIds.length = 0;
}
});
describe('POST /machine/heartbeat', () => {
test('a host beats and is told how often to beat again', async () => {
const host = await registeredHost('beat-ok');
const res = await app.request('/machine/heartbeat', {
method: 'POST',
headers: host.headers
});
expect(res.status).toBe(200);
const body = (await res.json()) as any;
// These two field names are what `neslet`'s plane.rs reads out of the
// reply. A rename on either side produces a host that beats, parses
// nothing, and reports success — so the names are the contract and this
// is the test that holds them.
expect(typeof body.data.lastSeen).toBe('string');
expect(body.data.intervalSeconds).toBe(Machine.HEARTBEAT_SECONDS);
expect(new Date(body.data.lastSeen).getTime()).not.toBeNaN();
});
test('the beat is what makes the host look online', async () => {
const host = await registeredHost('beat-online');
// Before any beat there is nothing to be online on the strength of.
expect(Machine.isOnline((await Machine.fromID(host.id))?.lastSeen ?? null)).toBe(false);
await app.request('/machine/heartbeat', { method: 'POST', headers: host.headers });
expect(Machine.isOnline((await Machine.fromID(host.id))?.lastSeen ?? null)).toBe(true);
});
test('wrong credentials are indistinguishable from none', async () => {
const host = await registeredHost('beat-wrongsecret');
const none = await app.request('/machine/heartbeat', { method: 'POST' });
const wrong = await app.request('/machine/heartbeat', {
method: 'POST',
headers: { ...host.headers, 'x-nestri-machine-secret': 'msk_wrong' }
});
// The middleware falls through to `public` on bad credentials rather
// than erroring, precisely so probing cannot tell an attacker which
// machine ids exist. Both therefore fail the same way, and asserting
// they are *identical* is the only way that property stays true.
expect(wrong.status).toBe(403);
expect(none.status).toBe(403);
expect(await wrong.json()).toEqual(await none.json());
// And the failed attempt left no trace of having been alive.
expect((await Machine.fromID(host.id))?.lastSeen).toBeNull();
});
test('a user session cannot beat on a hosts behalf', async () => {
// A box holds credentials but is not its owner, and the reverse holds
// too: `machineOnly` exists so a route written for a host cannot be
// driven by whoever owns it.
const res = await app.request('/machine/heartbeat', {
method: 'POST',
headers: { 'x-nestri-admin-token': 'test-admin-secret-42' }
});
expect(res.status).toBe(403);
});
});