Around remaining blockers (Zapier login, live Verae): generate PDF receipts without extra deps; AES-256-GCM object store with tenant isolation and share tokens; scripts/nats-tunnel.sh to NS1; SHA256 Hash Text local Zapier action; scripts/test-offline.sh for the no-login suite. Learned: other-tenant object GET is 403; share token is the mock unwrap path.
124 lines
4 KiB
JavaScript
124 lines
4 KiB
JavaScript
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, seedFreeTenant } from '../helpers.js';
|
|
import { clearMockJobs } from '../../src/clients/veraeClient.js';
|
|
import { config } from '../../src/config.js';
|
|
|
|
describe('encrypted objects + share + pdf receipt', () => {
|
|
let ctx;
|
|
let server;
|
|
let port;
|
|
let ownerKey;
|
|
let otherKey;
|
|
|
|
before(async () => {
|
|
config.mockVerae = true;
|
|
config.natsEnabled = false;
|
|
config.mockJobCompleteMs = 40;
|
|
clearMockJobs();
|
|
ctx = useTempStore();
|
|
ownerKey = seedProTenant().apiKey;
|
|
otherKey = seedFreeTenant({ id: 'tenant-other', veraeUsername: 'other', veraePassword: 'otherpass' }).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('stores encrypted blob, hides it from other tenants, shares via token', async () => {
|
|
const created = await call(port, 'POST', '/zapier/v1/objects', ownerKey, {
|
|
filename: 'secret.txt',
|
|
contentBase64: Buffer.from('top-secret').toString('base64'),
|
|
publicMetadata: { title: 'pub' },
|
|
privateMetadata: { ssn: '000' },
|
|
});
|
|
assert.equal(created.status, 201);
|
|
const id = created.body.id;
|
|
|
|
const mine = await call(port, 'GET', `/zapier/v1/objects/${id}`, ownerKey);
|
|
assert.equal(mine.status, 200);
|
|
assert.equal(Buffer.from(mine.body.contentBase64, 'base64').toString(), 'top-secret');
|
|
assert.equal(mine.body.privateMetadata.ssn, '000');
|
|
|
|
const theirs = await call(port, 'GET', `/zapier/v1/objects/${id}`, otherKey);
|
|
assert.equal(theirs.status, 403);
|
|
|
|
const share = await call(port, 'POST', `/zapier/v1/objects/${id}/share`, ownerKey, { label: 'for-other' });
|
|
assert.equal(share.status, 201);
|
|
const redeemed = await call(port, 'GET', `/zapier/v1/shares/${share.body.token}`, otherKey);
|
|
assert.equal(redeemed.status, 200);
|
|
assert.equal(redeemed.body.filename, 'secret.txt');
|
|
assert.ok(redeemed.body.dek);
|
|
assert.equal(redeemed.body.publicMetadata.title, 'pub');
|
|
});
|
|
|
|
it('returns a PDF receipt starting with %PDF', async () => {
|
|
const wait = await call(port, 'POST', '/zapier/v1/timestamp/wait', ownerKey, { data: 'pdf-receipt' });
|
|
const jobId = wait.body.id;
|
|
const rec = await raw(port, 'GET', `/zapier/v1/receipts/${jobId}?format=pdf`, ownerKey);
|
|
assert.equal(rec.status, 200);
|
|
assert.match(rec.body.toString('latin1').slice(0, 5), /%PDF-/);
|
|
});
|
|
});
|
|
|
|
function call(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) => {
|
|
const chunks = [];
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('end', () => {
|
|
const buf = Buffer.concat(chunks);
|
|
let parsed = {};
|
|
try {
|
|
parsed = buf.length ? JSON.parse(buf.toString()) : {};
|
|
} catch {
|
|
parsed = { raw: buf.toString() };
|
|
}
|
|
resolve({ status: res.statusCode, body: parsed });
|
|
});
|
|
},
|
|
);
|
|
req.on('error', reject);
|
|
if (body) req.write(JSON.stringify(body));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function raw(port, method, path, apiKey) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
{
|
|
hostname: '127.0.0.1',
|
|
port,
|
|
path,
|
|
method,
|
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
},
|
|
(res) => {
|
|
const chunks = [];
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
|
|
},
|
|
);
|
|
req.on('error', reject);
|
|
req.end();
|
|
});
|
|
}
|