feat: Sync to OSS repo

This commit is contained in:
Wanjohi
2026-08-06 22:13:51 +03:00
parent 46d2a56180
commit 3faac3008f
144 changed files with 27561 additions and 0 deletions

View File

@@ -0,0 +1,542 @@
import { describe, expect, test } from 'bun:test';
import { app } from '../app/index';
import { TEST_ADMIN_SECRET } from './setup';
import './setup';
function adminHeaders(): Record<string, string> {
return { 'x-nestri-admin-token': TEST_ADMIN_SECRET };
}
describe('Index', () => {
test('GET / returns hello world', async () => {
const res = await app.request('/');
expect(res.status).toBe(200);
expect(await res.text()).toBe('Hello World!');
});
});
describe('Auth middleware', () => {
test('public access to a protected route returns 401', async () => {
const res = await app.request('/games');
expect(res.status).toBe(401);
const body = (await res.json()) as any;
expect(body.type).toBe('authentication');
expect(body.code).toBe('unauthorized');
});
test('admin token gains access to protected routes', async () => {
const res = await app.request('/games', {
headers: adminHeaders()
});
expect(res.status).toBe(200);
});
test('wrong admin token is treated as public → 401', async () => {
const res = await app.request('/games', {
headers: { 'x-nestri-admin-token': 'wrong-secret' }
});
expect(res.status).toBe(401);
const body = (await res.json()) as any;
expect(body.type).toBe('authentication');
});
test('a bearer token that cannot be verified is unauthenticated, not a server error', async () => {
// A token nobody can verify makes the *caller* unauthenticated; it does
// not make the request a server fault. `verify` reports a malformed or
// expired token in `err`, but throws when it cannot reach the auth
// service at all, and that throw used to surface as a 500.
const res = await app.request('/games', {
headers: { authorization: 'Bearer not-a-real-token' }
});
expect(res.status).toBe(401);
const body = (await res.json()) as any;
expect(body.type).toBe('authentication');
});
test('missing authorization on admin-only route returns 401', async () => {
const res = await app.request('/games/sync', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({})
});
// notPublic runs before adminOnly → 401
expect(res.status).toBe(401);
const body = (await res.json()) as any;
expect(body.code).toBe('unauthorized');
});
});
describe('Validation', () => {
test('malformed JSON body returns 400', async () => {
const res = await app.request('/games/sync', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: '{not-json'
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.type).toBe('validation');
});
test('missing required fields returns 400 with code', async () => {
const res = await app.request('/games/download-state', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: JSON.stringify({ status: 'downloading' })
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.type).toBe('validation');
});
test('invalid status enum in download-state returns 400', async () => {
const res = await app.request('/games/download-state', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: JSON.stringify({
hostId: 'hst_test',
steamAppId: 440,
status: 'bogus_status'
})
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.type).toBe('validation');
});
test('non-existent game returns 404', async () => {
const res = await app.request('/games/gam_nonexistent', {
headers: adminHeaders()
});
expect(res.status).toBe(404);
const body = (await res.json()) as any;
expect(body.type).toBe('not_found');
});
test('missing content-type header returns 400', async () => {
const res = await app.request('/games/sync', {
method: 'POST',
headers: adminHeaders(),
body: JSON.stringify({})
});
expect(res.status).toBe(400);
});
});
describe('Error response shape', () => {
test('404 on unknown game has standard error shape', async () => {
const res = await app.request('/games/gam_nonexistent', {
headers: adminHeaders()
});
expect(res.status).toBe(404);
const body = (await res.json()) as any;
expect(body).toHaveProperty('type');
expect(body).toHaveProperty('code');
expect(body).toHaveProperty('message');
});
test('429 error responses have standard shape', async () => {
const res = await app.request('/games/gam_nonexistent', {
headers: adminHeaders()
});
expect(res.status).toBe(404);
const body = (await res.json()) as any;
expect(body.type).toBe('not_found');
expect(body.code).toBe('resource_not_found');
});
});
describe('OpenAPI doc', () => {
test('GET /doc returns 200 with JSON', async () => {
const res = await app.request('/doc');
expect(res.status).toBe(200);
const body = (await res.json()) as any;
expect(body).toHaveProperty('openapi');
expect(body.info.title).toBe('Nestri API');
});
test('GET /doc contains expected route paths', async () => {
const res = await app.request('/doc');
const body = (await res.json()) as any;
const paths = Object.keys(body.paths);
expect(paths).toContain('/games');
expect(paths).toContain('/games/sync');
expect(paths).toContain('/games/{id}');
expect(paths).toContain('/games/{id}/download-state');
expect(paths).toContain('/games/download-state');
expect(paths).toContain('/library');
expect(paths).toContain('/library/sync');
expect(paths).toContain('/steam/link');
expect(paths).toContain('/user');
});
test('doc has security schemes defined', async () => {
const res = await app.request('/doc');
const body = (await res.json()) as any;
expect(body.components.securitySchemes.Bearer).toMatchObject({
type: 'http',
scheme: 'bearer'
});
});
});
describe('CORS', () => {
test('CORS preflight returns headers', async () => {
const res = await app.request('/games', {
method: 'OPTIONS',
headers: {
origin: 'http://localhost:5173',
'access-control-request-method': 'GET'
}
});
expect(res.status).toBe(204);
expect(res.headers.get('access-control-allow-origin')).toBeTruthy();
});
test('response includes cache-control no-store', async () => {
const res = await app.request('/');
expect(res.headers.get('cache-control')).toBe('no-store');
});
});
describe('Download state route', () => {
test('POST /games/download-state requires hostId and steamAppId', async () => {
const res = await app.request('/games/download-state', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: JSON.stringify({
hostId: 'hst_test',
status: 'downloading'
// missing steamAppId
})
});
expect(res.status).toBe(400);
});
test('POST /games/download-state validates status enum', async () => {
const valid = ['pending', 'verifying', 'downloading', 'ready', 'failed'] as const;
for (const status of valid) {
//eslint-disable-next-line
const res = await app.request('/games/download-state', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: JSON.stringify({
hostId: 'hst_test',
steamAppId: 440,
status
})
});
// Validation should pass (200 or 404 if game not in DB)
expect(res.status).not.toBe(400);
}
});
test('an unauthenticated caller cannot report download state', async () => {
const res = await app.request('/games/download-state', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ hostId: 'mch_test', steamAppId: 440, status: 'ready' })
});
// The route group's `notPublic` runs first, so this is 401 rather than
// the 403 `machineOrAdmin` would give an authenticated non-host.
expect(res.status).toBe(401);
});
test('an admin caller must say which host it is reporting for', async () => {
// hostId is optional in the schema now because a machine supplies it
// from its own identity. Admin has no identity to take it from, so
// leaving it out has to fail rather than write under an empty host.
const res = await app.request('/games/download-state', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ steamAppId: 440, status: 'ready' })
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.code).toBe('missing_required_field');
expect(body.param).toBe('hostId');
});
});
describe('Access tokens', () => {
test('creating a token requires authentication', async () => {
const res = await app.request('/access-token', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'living-room-box' })
});
expect(res.status).toBe(401);
});
test('the admin token cannot mint a token for anyone', async () => {
// This is the boundary that makes admin safe to hand out for tooling:
// it reads and writes API data but cannot *become* a user. Minting a
// PAT on someone's behalf would erase exactly that.
const res = await app.request('/access-token', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ name: 'living-room-box' })
});
expect(res.status).toBe(403);
const body = (await res.json()) as any;
expect(body.message).toContain('user session');
});
test('a token needs a name', async () => {
const res = await app.request('/access-token', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ name: '' })
});
expect(res.status).toBe(400);
});
test('expiry is capped at a year', async () => {
const res = await app.request('/access-token', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ name: 'box', expiresInDays: 4000 })
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.type).toBe('validation');
});
test('teamId accepts null to force a token scoped to the user alone', async () => {
// Team scope is the default and is *broader* than user scope, so there
// has to be an explicit way to ask for the narrow one. Null is it;
// omitting the field means "take the default", which is not the same.
const res = await app.request('/access-token', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ name: 'box', teamId: null })
});
// Admin is refused at the handler, but only after validation — so a
// 403 here proves null passed the schema rather than being rejected.
expect(res.status).toBe(403);
});
test('revoking someone elses token requires authentication', async () => {
const res = await app.request('/access-token/pat_whatever', { method: 'DELETE' });
expect(res.status).toBe(401);
});
test('an unknown access token is unauthenticated, not a server error', async () => {
// A `pat_` prefix routes to the database rather than JWT verification.
// A miss there must read as "not signed in", the same as a bad JWT.
const res = await app.request('/games', {
headers: { authorization: 'Bearer pat_nosuchtokenvalue' }
});
expect(res.status).toBe(401);
const body = (await res.json()) as any;
expect(body.type).toBe('authentication');
});
});
describe('Box access', () => {
test('rescoping a machine requires authentication', async () => {
const res = await app.request('/machine/mch_whatever', {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ teamId: null })
});
expect(res.status).toBe(401);
});
test('the admin token cannot rescope a machine', async () => {
// Rescoping is an owner action and the query is scoped to a user id;
// admin has none, so it must be refused rather than 500 later.
const res = await app.request('/machine/mch_whatever', {
method: 'PATCH',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ teamId: null })
});
expect(res.status).toBe(403);
const body = (await res.json()) as any;
expect(body.message).toContain('user session');
});
test('teamId is required on the body, and may be null', async () => {
// Null is "make it mine alone" — a different thing from omitting the
// field, which would leave the scope ambiguous.
const missing = await app.request('/machine/mch_whatever', {
method: 'PATCH',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({})
});
expect(missing.status).toBe(400);
const explicitNull = await app.request('/machine/mch_whatever', {
method: 'PATCH',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ teamId: null })
});
// Past validation, refused at the handler for being admin.
expect(explicitNull.status).toBe(403);
});
test('entitlement requires machine credentials, not a user session', async () => {
// The machine is taken from its credentials, never the query, so a box
// cannot ask about another box.
const res = await app.request('/machine/entitlement?userId=usr_x', {
headers: adminHeaders()
});
expect(res.status).toBe(403);
const body = (await res.json()) as any;
expect(body.message).toContain('Machine credentials');
});
test('entitlement needs a userId to answer about', async () => {
const res = await app.request('/machine/entitlement');
// machineOnly refuses before validation; either way it does not answer.
expect([400, 403]).toContain(res.status);
});
});
describe('Machine registration', () => {
test('registering a machine requires authentication', async () => {
const res = await app.request('/machine/register', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ label: 'living-room-box' })
});
expect(res.status).toBe(401);
});
test('the admin token cannot register a machine', async () => {
// Registering is an act of ownership and the resulting row references a
// user. Admin is authenticated but owns nothing, so it must be refused
// here rather than fail later on a null owner.
const res = await app.request('/machine/register', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ label: 'living-room-box' })
});
expect(res.status).toBe(403);
const body = (await res.json()) as any;
expect(body.message).toContain('user session');
});
test('registering a machine requires a label', async () => {
const res = await app.request('/machine/register', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ label: '' })
});
expect(res.status).toBe(400);
const body = (await res.json()) as any;
expect(body.type).toBe('validation');
});
test('describing yourself requires machine credentials', async () => {
const res = await app.request('/machine/me', { headers: adminHeaders() });
expect(res.status).toBe(403);
const body = (await res.json()) as any;
expect(body.message).toContain('Machine credentials');
});
});
describe('Steam routes', () => {
test('POST /steam/link requires auth', async () => {
const res = await app.request('/steam/link', { method: 'POST' });
expect(res.status).toBe(401);
});
test('POST /steam/link validates steamId', async () => {
const res = await app.request('/steam/link', {
method: 'POST',
headers: {
...adminHeaders(),
'content-type': 'application/json'
},
body: JSON.stringify({})
});
expect(res.status).toBe(400);
});
});
describe('User routes', () => {
test('GET /user requires auth', async () => {
const res = await app.request('/user');
expect(res.status).toBe(401);
});
});
describe('Library routes', () => {
test('GET /library requires auth', async () => {
const res = await app.request('/library');
expect(res.status).toBe(401);
});
});
describe('Pairing code routes', () => {
test('POST /pairing-code requires auth', async () => {
// Generating a code says "this key is also me", so it can only be done
// from a session that already is that user.
const res = await app.request('/pairing-code', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({})
});
expect(res.status).toBe(401);
});
test('POST /pairing-code/claim rejects an unauthenticated caller', async () => {
// Claiming is done for a device with no identity yet, so it carries the
// admin token rather than a user session. Without it, no.
const res = await app.request('/pairing-code/claim', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ code: 'NESSH-7F2Q', fingerprint: 'aa:bb' })
});
expect([401, 403]).toContain(res.status);
});
test('POST /pairing-code/claim requires both a code and a fingerprint', async () => {
for (const body of [{}, { code: 'NESSH-7F2Q' }, { fingerprint: 'aa:bb' }]) {
// eslint-disable-next-line
const res = await app.request('/pairing-code/claim', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify(body)
});
expect(res.status).toBe(400);
}
});
test('POST /pairing-code/claim rejects an empty code', async () => {
// An empty string must not be treated as "any code".
const res = await app.request('/pairing-code/claim', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ code: '', fingerprint: 'aa:bb' })
});
expect(res.status).toBe(400);
});
test('POST /pairing-code caps how long a code stays valid', async () => {
// Short-lived by design; a long-lived code is a shared password.
const res = await app.request('/pairing-code', {
method: 'POST',
headers: { ...adminHeaders(), 'content-type': 'application/json' },
body: JSON.stringify({ ttlMinutes: 60 * 24 })
});
expect(res.status).toBe(400);
});
});

16
apps/api/test/setup.ts Normal file
View File

@@ -0,0 +1,16 @@
import { beforeEach } from 'bun:test';
import { Env } from '@nestri/core/env';
const TEST_ADMIN_SECRET = 'test-admin-secret-42';
const TEST_FRONTEND_URL = 'http://localhost:5173';
beforeEach(() => {
Env.init({
NODE_ENV: 'test',
ADMIN_SHARED_SECRET: TEST_ADMIN_SECRET,
FRONTEND_URL: TEST_FRONTEND_URL
});
});
export { TEST_ADMIN_SECRET, TEST_FRONTEND_URL };