Milestone 3: NATS wait (gate 9), hash lookup, zappier timestamp, NS1 tunnel
Wait-via-NATS returns completed or pending+jobId (GATE 9). Mock SHA256
idempotent register + GET /hashes/{sha256}. Zappier commercial edge has
POST /v1/timestamp and hash-lookup. GATE 12 smoke (signup + wait) passes.
NS1 NATS is 127.0.0.1:4222 on 70.88.205.138; SSH tunnel :14222. Local
nats-server -js used for isolated tests. Activate app adds Echo Text.
Learned: JetStream on NS1 is loopback-only; do not bind 4222 public.
This commit is contained in:
parent
10c663cc0c
commit
51ae79b75f
75 changed files with 1114 additions and 123 deletions
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Hash idempotent register + lookup (mock Verae)
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import { createApp } from '../../src/app.js';
|
||||
import { useTempStore, seedProTenant } from '../helpers.js';
|
||||
import { clearMockJobs, sha256Hex } from '../../src/clients/veraeClient.js';
|
||||
import { config } from '../../src/config.js';
|
||||
|
||||
describe('hash register + lookup', () => {
|
||||
let ctx;
|
||||
/** @type {import('http').Server} */
|
||||
let server;
|
||||
let port;
|
||||
let apiKey;
|
||||
|
||||
before(async () => {
|
||||
config.mockVerae = true;
|
||||
config.natsEnabled = false;
|
||||
clearMockJobs();
|
||||
ctx = useTempStore();
|
||||
const seeded = seedProTenant();
|
||||
apiKey = seeded.apiKey;
|
||||
const app = createApp();
|
||||
await new Promise((resolve) => {
|
||||
server = app.listen(0, '127.0.0.1', () => {
|
||||
port = server.address().port;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await new Promise((r) => server.close(r));
|
||||
ctx.cleanup();
|
||||
});
|
||||
|
||||
it('registers sha256, second create returns original jobId', async () => {
|
||||
const data = 'hello-hash';
|
||||
const sha = sha256Hex(data);
|
||||
const r1 = await json(port, 'POST', '/zapier/v1/timestamp', apiKey, { data });
|
||||
assert.equal(r1.status, 202);
|
||||
const r2 = await json(port, 'POST', '/zapier/v1/timestamp', apiKey, { sha256: sha });
|
||||
assert.equal(r2.status, 202);
|
||||
assert.equal(r2.body.jobId, r1.body.jobId);
|
||||
assert.equal(r2.body.existing, true);
|
||||
|
||||
const look = await json(port, 'GET', `/zapier/v1/hashes/${sha}`, apiKey);
|
||||
assert.equal(look.status, 200);
|
||||
assert.equal(look.body.exists, true);
|
||||
assert.equal(look.body.jobId, r1.body.jobId);
|
||||
});
|
||||
|
||||
it('lookup miss is 404', async () => {
|
||||
const miss = 'a'.repeat(64);
|
||||
const look = await json(port, 'GET', `/zapier/v1/hashes/${miss}`, apiKey);
|
||||
assert.equal(look.status, 404);
|
||||
});
|
||||
});
|
||||
|
||||
function json(port, method, path, apiKey, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: '127.0.0.1',
|
||||
port,
|
||||
path,
|
||||
method,
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (c) => {
|
||||
data += c;
|
||||
});
|
||||
res.on('end', () => {
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = data ? JSON.parse(data) : {};
|
||||
} catch {
|
||||
parsed = { raw: data };
|
||||
}
|
||||
resolve({ status: res.statusCode, body: parsed });
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* GATE 9 — /timestamp/wait via NATS events (timeout returns pending + jobId)
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { config } from '../../src/config.js';
|
||||
import { useTempStore, seedProTenant } from '../helpers.js';
|
||||
import { connectNats, ensureStreams, closeNats } from '../../src/nats/connection.js';
|
||||
import { startJobPollerWorker, stopJobPollerWorker } from '../../src/workers/jobPollerWorker.js';
|
||||
import { createTimestampAndWait } from '../../src/services/timestampService.js';
|
||||
import { resolveAuthContext } from '../../src/services/authService.js';
|
||||
import { clearMockJobs } from '../../src/clients/veraeClient.js';
|
||||
|
||||
describe('GATE 9 wait via NATS', () => {
|
||||
/** @type {ReturnType<typeof useTempStore>} */
|
||||
let ctx;
|
||||
let authCtx;
|
||||
|
||||
before(async () => {
|
||||
assert.equal(config.mockVerae, true);
|
||||
config.natsEnabled = true;
|
||||
process.env.NATS_FORCE_CONNECT = '1';
|
||||
config.natsUrl = process.env.NATS_URL || 'nats://127.0.0.1:4222';
|
||||
config.jobPollIntervalMs = 40;
|
||||
config.jobPollMaxAttempts = 40;
|
||||
config.waitTimeoutMs = 8000;
|
||||
config.mockJobCompleteMs = 80;
|
||||
|
||||
clearMockJobs();
|
||||
ctx = useTempStore();
|
||||
const seeded = seedProTenant();
|
||||
authCtx = await resolveAuthContext(seeded.apiKey);
|
||||
|
||||
await connectNats(config.natsUrl);
|
||||
await ensureStreams();
|
||||
await startJobPollerWorker();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopJobPollerWorker();
|
||||
await closeNats();
|
||||
ctx.cleanup();
|
||||
process.env.NATS_FORCE_CONNECT = '';
|
||||
});
|
||||
|
||||
it('wait returns completed when worker finishes before timeout', async () => {
|
||||
const status = await createTimestampAndWait(authCtx, { data: 'wait-fast' });
|
||||
assert.equal(status.status, 'completed');
|
||||
assert.ok(status.id || status.jobId);
|
||||
});
|
||||
|
||||
it('wait returns pending + jobId on timeout', async () => {
|
||||
const prevWait = config.waitTimeoutMs;
|
||||
const prevDelay = config.mockJobCompleteMs;
|
||||
config.waitTimeoutMs = 60;
|
||||
config.mockJobCompleteMs = 30_000;
|
||||
try {
|
||||
const status = await createTimestampAndWait(authCtx, { data: 'wait-slow' });
|
||||
assert.equal(status.status, 'pending');
|
||||
assert.ok(status.jobId || status.id);
|
||||
} finally {
|
||||
config.waitTimeoutMs = prevWait;
|
||||
config.mockJobCompleteMs = prevDelay;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -18,6 +18,7 @@ describe('config', () => {
|
|||
'tokenSecret',
|
||||
'jobPollIntervalMs',
|
||||
'jobPollMaxAttempts',
|
||||
'waitTimeoutMs',
|
||||
'storePath',
|
||||
]) {
|
||||
assert.notEqual(config[key], undefined, `missing config.${key}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue