Initial import of verae-middleware from zapier monorepo
This commit is contained in:
commit
4d496bf29e
66 changed files with 6590 additions and 0 deletions
49
test/unit/app.test.js
Normal file
49
test/unit/app.test.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* GATE 2 (partial) — HTTP shell health endpoint.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApp } from '../../src/app.js';
|
||||
|
||||
describe('createApp', () => {
|
||||
/** @type {import('http').Server} */
|
||||
let server;
|
||||
/** @type {number} */
|
||||
let port;
|
||||
|
||||
before(async () => {
|
||||
const app = createApp();
|
||||
await new Promise((resolve) => {
|
||||
server = app.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
port = server.address().port;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
|
||||
it('GET /health returns ok', async () => {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.status, 'ok');
|
||||
assert.equal(body.service, 'verae-zapier-middleware');
|
||||
assert.equal(typeof body.mockVerae, 'boolean');
|
||||
assert.equal(typeof body.natsEnabled, 'boolean');
|
||||
assert.equal(typeof body.natsConnected, 'boolean');
|
||||
assert.ok(res.headers.get('x-trace-id'));
|
||||
});
|
||||
|
||||
it('protected /zapier path requires auth', async () => {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/zapier/v1/timestamp`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
assert.equal(res.status, 401);
|
||||
const body = await res.json();
|
||||
assert.equal(body.code, 'UNAUTHORIZED');
|
||||
});
|
||||
});
|
||||
34
test/unit/config.test.js
Normal file
34
test/unit/config.test.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* GATE 2 (partial) — config exports.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { config, PLAN_LIMITS } from '../../src/config.js';
|
||||
|
||||
describe('config', () => {
|
||||
it('exposes required keys for HTTP and NATS', () => {
|
||||
for (const key of [
|
||||
'port',
|
||||
'host',
|
||||
'veraeApiBaseUrl',
|
||||
'mockVerae',
|
||||
'natsEnabled',
|
||||
'natsUrl',
|
||||
'tokenSecret',
|
||||
'jobPollIntervalMs',
|
||||
'jobPollMaxAttempts',
|
||||
'waitTimeoutMs',
|
||||
'storePath',
|
||||
]) {
|
||||
assert.notEqual(config[key], undefined, `missing config.${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('defines plan limits for free through enterprise', () => {
|
||||
for (const plan of ['free', 'starter', 'pro', 'enterprise']) {
|
||||
assert.ok(PLAN_LIMITS[plan], plan);
|
||||
assert.equal(typeof PLAN_LIMITS[plan].requestsPerMinute, 'number');
|
||||
}
|
||||
});
|
||||
});
|
||||
165
test/unit/debug.test.js
Normal file
165
test/unit/debug.test.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* GATE 1 — Debug facility unit tests.
|
||||
* Must pass before Phase 2.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
parseDebugVeraeEnv,
|
||||
loadDebugConfig,
|
||||
shouldLog,
|
||||
createDebugger,
|
||||
setDebugTestSink,
|
||||
redact,
|
||||
withTrace,
|
||||
getTraceId,
|
||||
generateTraceId,
|
||||
} from '../../src/debug/index.js';
|
||||
|
||||
describe('parseDebugVeraeEnv', () => {
|
||||
it('disables when unset or empty', () => {
|
||||
assert.equal(parseDebugVeraeEnv(undefined).enabled, false);
|
||||
assert.equal(parseDebugVeraeEnv('').enabled, false);
|
||||
assert.equal(parseDebugVeraeEnv('off').enabled, false);
|
||||
});
|
||||
|
||||
it('enables all namespaces for 1 or *', () => {
|
||||
const a = parseDebugVeraeEnv('1');
|
||||
assert.equal(a.enabled, true);
|
||||
assert.equal(a.namespaces, null);
|
||||
|
||||
const b = parseDebugVeraeEnv('*');
|
||||
assert.equal(b.enabled, true);
|
||||
assert.equal(b.namespaces, null);
|
||||
});
|
||||
|
||||
it('parses comma-separated namespaces', () => {
|
||||
const { enabled, namespaces } = parseDebugVeraeEnv('auth, NATS, jobs');
|
||||
assert.equal(enabled, true);
|
||||
assert.ok(namespaces.has('auth'));
|
||||
assert.ok(namespaces.has('nats'));
|
||||
assert.ok(namespaces.has('jobs'));
|
||||
assert.equal(namespaces.has('webhooks'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldLog', () => {
|
||||
it('respects level thresholds', () => {
|
||||
const config = {
|
||||
enabled: true,
|
||||
namespaces: null,
|
||||
level: 'warn',
|
||||
filePath: null,
|
||||
};
|
||||
assert.equal(shouldLog(config, 'app', 'debug'), false);
|
||||
assert.equal(shouldLog(config, 'app', 'warn'), true);
|
||||
assert.equal(shouldLog(config, 'app', 'error'), true);
|
||||
});
|
||||
|
||||
it('filters by namespace', () => {
|
||||
const config = {
|
||||
enabled: true,
|
||||
namespaces: new Set(['auth']),
|
||||
level: 'debug',
|
||||
filePath: null,
|
||||
};
|
||||
assert.equal(shouldLog(config, 'auth', 'debug'), true);
|
||||
assert.equal(shouldLog(config, 'nats', 'debug'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redact', () => {
|
||||
it('redacts sensitive keys and token-like strings', () => {
|
||||
const out = redact({
|
||||
password: 'secret',
|
||||
apiKey: 'zmw_abc123def456ghi789jkl',
|
||||
jobId: 'keep-me',
|
||||
authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.aaa.bbb',
|
||||
nested: { veraeToken: 'zmt_payload.sig' },
|
||||
});
|
||||
|
||||
assert.equal(out.password, '[REDACTED]');
|
||||
assert.equal(out.apiKey, '[REDACTED]');
|
||||
assert.equal(out.jobId, 'keep-me');
|
||||
assert.equal(out.authorization, '[REDACTED]');
|
||||
assert.equal(out.nested.veraeToken, '[REDACTED]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDebugger', () => {
|
||||
/** @type {string[]} */
|
||||
let lines;
|
||||
const prev = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
lines = [];
|
||||
setDebugTestSink((line) => lines.push(line));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setDebugTestSink(null);
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (!(key in prev)) delete process.env[key];
|
||||
}
|
||||
Object.assign(process.env, prev);
|
||||
});
|
||||
|
||||
it('is silent when DEBUG_VERAE is unset', () => {
|
||||
delete process.env.DEBUG_VERAE;
|
||||
const log = createDebugger('auth');
|
||||
log.debug('should not appear', { x: 1 });
|
||||
assert.equal(lines.length, 0);
|
||||
});
|
||||
|
||||
it('emits only selected namespaces', () => {
|
||||
process.env.DEBUG_VERAE = 'auth';
|
||||
createDebugger('auth').debug('auth-line');
|
||||
createDebugger('nats').debug('nats-line');
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0], /auth-line/);
|
||||
assert.doesNotMatch(lines[0], /nats-line/);
|
||||
});
|
||||
|
||||
it('redacts secrets in meta', () => {
|
||||
process.env.DEBUG_VERAE = 'auth';
|
||||
createDebugger('auth').debug('login', {
|
||||
password: 'hunter2',
|
||||
token: 'zmw_supersecretvaluehere12',
|
||||
});
|
||||
assert.equal(lines.length, 1);
|
||||
assert.doesNotMatch(lines[0], /hunter2/);
|
||||
assert.doesNotMatch(lines[0], /zmw_supersecret/);
|
||||
assert.match(lines[0], /REDACTED/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withTrace', () => {
|
||||
it('propagates traceId to nested async work', async () => {
|
||||
const outerId = generateTraceId();
|
||||
let innerId = null;
|
||||
|
||||
await withTrace({ traceId: outerId, span: 'outer' }, async () => {
|
||||
assert.equal(getTraceId(), outerId);
|
||||
await withTrace({ span: 'inner' }, async () => {
|
||||
innerId = getTraceId();
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(innerId, outerId);
|
||||
assert.equal(getTraceId(), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadDebugConfig', () => {
|
||||
it('reads level and file from env', () => {
|
||||
process.env.DEBUG_VERAE = '1';
|
||||
process.env.DEBUG_VERAE_LEVEL = 'error';
|
||||
process.env.DEBUG_VERAE_FILE = '/tmp/verae-debug.log';
|
||||
const cfg = loadDebugConfig(process.env);
|
||||
assert.equal(cfg.enabled, true);
|
||||
assert.equal(cfg.level, 'error');
|
||||
assert.equal(cfg.filePath, '/tmp/verae-debug.log');
|
||||
});
|
||||
});
|
||||
105
test/unit/store.test.js
Normal file
105
test/unit/store.test.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* GATE 3 — Store unit tests
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
useTempStore,
|
||||
seedFreeTenant,
|
||||
seedEnterpriseTenant,
|
||||
} from '../helpers.js';
|
||||
import { getTenantByApiKey, resolveLimits, getTenant } from '../../src/store/tenants.js';
|
||||
import {
|
||||
createWebhook,
|
||||
getActiveWebhooks,
|
||||
listWebhooksForTenant,
|
||||
deleteWebhook,
|
||||
} from '../../src/store/webhooks.js';
|
||||
import { getUsage, incrementUsage } from '../../src/store/usage.js';
|
||||
import { reloadStore, getStore } from '../../src/store/db.js';
|
||||
|
||||
describe('stores', () => {
|
||||
/** @type {ReturnType<typeof useTempStore>} */
|
||||
let ctx;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = useTempStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ctx.cleanup();
|
||||
});
|
||||
|
||||
it('create tenant → API key resolves to same tenant', () => {
|
||||
const { tenant, apiKey } = seedFreeTenant();
|
||||
const found = getTenantByApiKey(apiKey);
|
||||
assert.ok(found);
|
||||
assert.equal(found.id, tenant.id);
|
||||
assert.equal(found.plan, 'free');
|
||||
});
|
||||
|
||||
it('free plan limits applied; enterprise contract overrides', () => {
|
||||
const { tenant: free } = seedFreeTenant();
|
||||
const freeLimits = resolveLimits(free);
|
||||
assert.equal(freeLimits.timestamps, 50);
|
||||
assert.equal(freeLimits.batch, false);
|
||||
|
||||
const { tenant: ent } = seedEnterpriseTenant();
|
||||
const entLimits = resolveLimits(ent);
|
||||
assert.equal(entLimits.timestamps, 10);
|
||||
assert.equal(entLimits.batch, true);
|
||||
assert.equal(entLimits.batchMaxItems, 5);
|
||||
});
|
||||
|
||||
it('webhook isolation per tenant', () => {
|
||||
seedFreeTenant({ id: 'a' });
|
||||
seedFreeTenant({ id: 'b', veraeUsername: 'u2' });
|
||||
|
||||
createWebhook({
|
||||
tenantId: 'a',
|
||||
targetUrl: 'https://hooks.example/a',
|
||||
event: 'timestamp.completed',
|
||||
});
|
||||
createWebhook({
|
||||
tenantId: 'b',
|
||||
targetUrl: 'https://hooks.example/b',
|
||||
event: 'timestamp.completed',
|
||||
});
|
||||
|
||||
const aHooks = listWebhooksForTenant('a');
|
||||
const bHooks = getActiveWebhooks('b', 'timestamp.completed');
|
||||
assert.equal(aHooks.length, 1);
|
||||
assert.equal(aHooks[0].targetUrl, 'https://hooks.example/a');
|
||||
assert.equal(bHooks.length, 1);
|
||||
assert.equal(bHooks[0].tenantId, 'b');
|
||||
});
|
||||
|
||||
it('persist + reload round-trip preserves data', () => {
|
||||
const { tenant, apiKey } = seedFreeTenant({ id: 'persist-me' });
|
||||
incrementUsage(tenant.id, 'timestamps', 3);
|
||||
createWebhook({
|
||||
tenantId: tenant.id,
|
||||
targetUrl: 'https://hooks.example/x',
|
||||
event: 'timestamp.completed',
|
||||
});
|
||||
|
||||
reloadStore(ctx.storePath);
|
||||
|
||||
assert.equal(getTenant('persist-me')?.name, tenant.name);
|
||||
assert.equal(getTenantByApiKey(apiKey)?.id, 'persist-me');
|
||||
assert.equal(getUsage('persist-me').timestamps, 3);
|
||||
assert.equal(getStore().webhooks.length, 1);
|
||||
});
|
||||
|
||||
it('deleteWebhook removes by id', () => {
|
||||
seedFreeTenant({ id: 'w' });
|
||||
const hook = createWebhook({
|
||||
tenantId: 'w',
|
||||
targetUrl: 'https://hooks.example/w',
|
||||
event: 'timestamp.completed',
|
||||
});
|
||||
assert.equal(deleteWebhook({ tenantId: 'w', hookId: hook.id }), true);
|
||||
assert.equal(listWebhooksForTenant('w').length, 0);
|
||||
});
|
||||
});
|
||||
55
test/unit/tokens.test.js
Normal file
55
test/unit/tokens.test.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* GATE 4 — Token tests
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
issueSessionToken,
|
||||
parseSessionToken,
|
||||
generateApiKey,
|
||||
isApiKey,
|
||||
extractBearerToken,
|
||||
} from '../../src/lib/tokens.js';
|
||||
import { config } from '../../src/config.js';
|
||||
|
||||
describe('tokens', () => {
|
||||
it('issues and parses session tokens', () => {
|
||||
const token = issueSessionToken({
|
||||
tenantId: 't1',
|
||||
veraeToken: 'mock-jwt-user',
|
||||
expiresAt: '2099-01-01T00:00:00Z',
|
||||
});
|
||||
assert.ok(token.startsWith('zmt_'));
|
||||
const parsed = parseSessionToken(token);
|
||||
assert.equal(parsed.tenantId, 't1');
|
||||
assert.equal(parsed.veraeToken, 'mock-jwt-user');
|
||||
});
|
||||
|
||||
it('rejects forged session tokens', () => {
|
||||
const token = issueSessionToken({
|
||||
tenantId: 't1',
|
||||
veraeToken: 'secret',
|
||||
});
|
||||
const forged = token.slice(0, -4) + 'xxxx';
|
||||
assert.equal(parseSessionToken(forged), null);
|
||||
});
|
||||
|
||||
it('rejects tokens signed with wrong secret', () => {
|
||||
const token = issueSessionToken({
|
||||
tenantId: 't1',
|
||||
veraeToken: 'secret',
|
||||
});
|
||||
const original = config.tokenSecret;
|
||||
config.tokenSecret = 'other-secret';
|
||||
assert.equal(parseSessionToken(token), null);
|
||||
config.tokenSecret = original;
|
||||
});
|
||||
|
||||
it('generates api keys and extracts bearer', () => {
|
||||
const key = generateApiKey();
|
||||
assert.ok(isApiKey(key));
|
||||
assert.equal(extractBearerToken(`Bearer ${key}`), key);
|
||||
assert.equal(extractBearerToken(undefined), null);
|
||||
});
|
||||
});
|
||||
64
test/unit/veraeClient.test.js
Normal file
64
test/unit/veraeClient.test.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* GATE 4 — Mock Verae client lifecycle
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { veraeClient, clearMockJobs } from '../../src/clients/veraeClient.js';
|
||||
import { config } from '../../src/config.js';
|
||||
import { setDebugTestSink, createDebugger } from '../../src/debug/index.js';
|
||||
|
||||
describe('veraeClient mock', () => {
|
||||
beforeEach(() => {
|
||||
clearMockJobs();
|
||||
// ensure mock mode (set at process start via env in gate script)
|
||||
assert.equal(config.mockVerae, true, 'MOCK_VERAE must be true for unit tests');
|
||||
});
|
||||
|
||||
it('create → wait → completed', async () => {
|
||||
const login = await veraeClient.login({ username: 'u', password: 'p' });
|
||||
assert.ok(login.token.startsWith('mock-jwt-'));
|
||||
|
||||
const { jobId } = await veraeClient.createTimestamp(login.token, {
|
||||
data: 'hello',
|
||||
hashAlg: 'SHA256',
|
||||
});
|
||||
assert.ok(jobId);
|
||||
|
||||
const status = await veraeClient.waitForJob(login.token, jobId, {
|
||||
maxAttempts: 40,
|
||||
intervalMs: 20,
|
||||
});
|
||||
assert.equal(status.status, 'completed');
|
||||
assert.ok(status.result.startsWith('mock-cert-'));
|
||||
});
|
||||
|
||||
it('verify accepts mock certificates', async () => {
|
||||
const login = await veraeClient.login({ username: 'u', password: 'p' });
|
||||
const { jobId } = await veraeClient.createTimestamp(login.token, { data: 'x' });
|
||||
const status = await veraeClient.waitForJob(login.token, jobId, {
|
||||
maxAttempts: 40,
|
||||
intervalMs: 20,
|
||||
});
|
||||
const result = await veraeClient.verify(login.token, { certificate: status.result });
|
||||
assert.equal(result.valid, true);
|
||||
});
|
||||
|
||||
it('debug http logs do not include Authorization values', () => {
|
||||
const lines = [];
|
||||
setDebugTestSink((line) => lines.push(line));
|
||||
process.env.DEBUG_VERAE = 'http';
|
||||
const log = createDebugger('http');
|
||||
log.debug('verae request', {
|
||||
method: 'POST',
|
||||
path: '/api/timestamp',
|
||||
authorization: 'Bearer super-secret-token-value',
|
||||
hasToken: true,
|
||||
});
|
||||
setDebugTestSink(null);
|
||||
delete process.env.DEBUG_VERAE;
|
||||
assert.equal(lines.length, 1);
|
||||
assert.doesNotMatch(lines[0], /super-secret-token-value/);
|
||||
assert.match(lines[0], /REDACTED|hasToken/);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue