159 lines
5.5 KiB
JavaScript
159 lines
5.5 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('shares a directory tree of encrypted files', async () => {
|
|
const tree = await call(port, 'POST', '/zapier/v1/objects/trees', ownerKey, {
|
|
prefix: 'docs/',
|
|
files: [
|
|
{ path: 'docs/a.txt', data: 'alpha' },
|
|
{ path: 'docs/b.txt', data: 'beta' },
|
|
],
|
|
});
|
|
assert.equal(tree.status, 201);
|
|
assert.equal(tree.body.fileCount, 2);
|
|
const share = await call(port, 'POST', `/zapier/v1/objects/trees/${tree.body.id}/share`, ownerKey, {});
|
|
assert.equal(share.status, 201);
|
|
assert.equal(share.body.kind, 'tree');
|
|
const redeemed = await call(port, 'GET', `/zapier/v1/shares/${share.body.token}`, otherKey);
|
|
assert.equal(redeemed.status, 200);
|
|
assert.equal(redeemed.body.kind, 'tree');
|
|
assert.equal(redeemed.body.files.length, 2);
|
|
assert.ok(redeemed.body.files[0].dek);
|
|
});
|
|
|
|
it('hash lookup never includes privateMetadata', async () => {
|
|
const created = await call(port, 'POST', '/zapier/v1/timestamp', ownerKey, {
|
|
data: 'meta-private',
|
|
publicMetadata: { vis: 'yes' },
|
|
privateMetadata: { secret: 'nope' },
|
|
});
|
|
const sha = created.body.sha256;
|
|
const look = await call(port, 'GET', `/zapier/v1/hashes/${sha}`, ownerKey);
|
|
assert.equal(look.status, 200);
|
|
assert.equal(look.body.publicMetadata.vis, 'yes');
|
|
assert.equal(look.body.privateMetadata, undefined);
|
|
const st = await call(port, 'GET', `/zapier/v1/status/${created.body.jobId}`, ownerKey);
|
|
assert.equal(st.body.privateMetadata.secret, 'nope');
|
|
});
|
|
|
|
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();
|
|
});
|
|
}
|