Files
sencho/backend/src/__tests__/distributed-license.test.ts
T
Anso ea1267b32f feat: give Community a 14-day in-app audit log (#1337)
* feat: give Community a 14-day in-app audit log

The audit-log list view is now reachable on the Community tier, scoped to a
rolling 14-day recent-activity window, with the existing filters (actor,
method, search, date range). Full retention, CSV/JSON export, and per-row
anomaly annotation remain on the paid tier.

Backend clamps the list window for unpaid tiers and forces anomaly annotation
off; a non-numeric from query value is normalized so it cannot lift the clamp.
The stats and export endpoints keep their paid gate. The frontend hides the
export control and the anomaly stat tiles for Community and omits the anomaly
request. Audit entries are still captured for every tier, so only read access
changes.

* test: repoint distributed-license stand-in to a still-paid audit route

The distributed-license trust-chain test borrowed GET /api/audit-log as a
paid-gated, DB-reading stand-in. That route is now Community-accessible, so its
six 403 PAID_REQUIRED assertions flipped to 200. Point the stand-in at
GET /api/audit-log/stats, which keeps requirePaid plus the system:audit
permission gate and is satisfied by both token types the test uses.
2026-06-08 08:58:49 -04:00

163 lines
6.1 KiB
TypeScript

/**
* Tests for Distributed License Enforcement: the trust chain where the main
* instance asserts its license tier to remote nodes via proxy headers.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import jwt from 'jsonwebtoken';
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_JWT_SECRET } from './helpers/setupTestDb';
let tmpDir: string;
let app: import('express').Express;
beforeAll(async () => {
tmpDir = await setupTestDb();
({ app } = await import('../index'));
});
afterAll(() => {
cleanupTestDb(tmpDir);
});
/** Helper: sign a token with the test JWT secret. */
const signToken = (payload: Record<string, unknown>, expiresIn: string | number = '1m') =>
jwt.sign(payload, TEST_JWT_SECRET, { expiresIn: expiresIn as jwt.SignOptions['expiresIn'] });
// We need a Paid-gated route that doesn't depend on Docker or remote nodes.
// The bare /api/audit-log list is Community (recent-activity window), but
// /api/audit-log/stats keeps requirePaid and reads from the DB, so it is a
// stable stand-in for exercising the distributed-license trust chain. The
// node_proxy and admin session tokens used below both satisfy its
// system:audit permission gate.
const PAID_ROUTE = '/api/audit-log/stats';
// ─── authMiddleware: proxyTier propagation ──────────────────────────────────
describe('authMiddleware - distributed license headers', () => {
it('sets proxyTier for node_proxy tokens with a valid tier header', async () => {
const token = signToken({ scope: 'node_proxy' });
// Hit a Paid-gated route with tier assertion - should be allowed
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
// Should NOT get 403 PAID_REQUIRED; the proxy tier assertion grants access
expect(res.status).not.toBe(403);
});
it('ignores tier headers for user session tokens', async () => {
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
// Even with a tier header set, a user session should use the local license (community)
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
// Local license is community in test env → should get 403
expect(res.status).toBe(403);
expect(res.body.code).toBe('PAID_REQUIRED');
});
it('ignores tier headers for malformed values on node_proxy tokens', async () => {
const token = signToken({ scope: 'node_proxy' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'enterprise'); // invalid value
// Invalid tier header → proxyTier not set → falls back to local (community) → 403
expect(res.status).toBe(403);
expect(res.body.code).toBe('PAID_REQUIRED');
});
it('falls back to local tier when no tier headers on node_proxy token', async () => {
const token = signToken({ scope: 'node_proxy' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`);
// No tier headers → falls back to local (community) → 403
expect(res.status).toBe(403);
expect(res.body.code).toBe('PAID_REQUIRED');
});
});
// ─── requirePaid guard ───────────────────────────────────────────────────────
describe('requirePaid - distributed license', () => {
it('allows access when proxy asserts paid tier', async () => {
const token = signToken({ scope: 'node_proxy' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
expect(res.status).not.toBe(403);
});
it('blocks access when proxy asserts community tier', async () => {
const token = signToken({ scope: 'node_proxy' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'community');
expect(res.status).toBe(403);
expect(res.body.code).toBe('PAID_REQUIRED');
});
it('blocks access for direct user when local tier is community', async () => {
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(403);
expect(res.body.code).toBe('PAID_REQUIRED');
});
});
// ─── Security: header injection prevention ──────────────────────────────────
describe('Security - tier header injection', () => {
it('cannot elevate access via tier headers on a user session', async () => {
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
// User session → tier headers ignored → local community tier → 403
expect(res.status).toBe(403);
});
it('cannot elevate access via tier headers without any auth', async () => {
const res = await request(app)
.get(PAID_ROUTE)
.set('x-sencho-tier', 'paid');
expect(res.status).toBe(401);
});
it('cannot elevate access with expired node_proxy token', async () => {
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '-1s' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
expect(res.status).toBe(401);
});
it('cannot elevate access with token signed by wrong secret', async () => {
const token = jwt.sign({ scope: 'node_proxy' }, 'wrong-secret', { expiresIn: '1m' });
const res = await request(app)
.get(PAID_ROUTE)
.set('Authorization', `Bearer ${token}`)
.set('x-sencho-tier', 'paid');
expect(res.status).toBe(401);
});
});