fix nda
This commit is contained in:
139
scripts/ds-stub.mjs
Normal file
139
scripts/ds-stub.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* A stand-in for the two Dropbox Sign endpoints the NDA inbox uses, with the
|
||||
* one property that broke the refresh in production: **the search index lags
|
||||
* behind the list**.
|
||||
*
|
||||
* `GET /v3/signature_request/list` answers from two different sources:
|
||||
*
|
||||
* * without `query=` — the live list, ordered by last activity (signed_at,
|
||||
* falling back to created_at), exactly like the real endpoint;
|
||||
* * with `query=` — the *index*, which only holds the snapshot a fixture entry
|
||||
* declares in `indexed`. A brand-new request is missing from it entirely,
|
||||
* and a request signed a minute ago is still in it as pending. That is what
|
||||
* an incremental refresh must not depend on.
|
||||
*
|
||||
* The range syntax is parsed for real, brackets and all, so `[a TO b]`
|
||||
* (inclusive) and `{a TO b}` (exclusive) actually behave differently — which
|
||||
* is what makes the boundary-day assertion meaningful.
|
||||
*
|
||||
* Run standalone for manual pokes:
|
||||
* node scripts/ds-stub.mjs --port 8099 --fixture some.json
|
||||
*/
|
||||
import { createServer } from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const dayOf = (unixSeconds) => new Date(unixSeconds * 1000).toISOString().slice(0, 10);
|
||||
|
||||
/** signed_at when there is one, created_at otherwise — the list's sort key. */
|
||||
const activityOf = (request) => request.signatures?.[0]?.signed_at ?? request.created_at;
|
||||
|
||||
/**
|
||||
* The subset of the query language the refresh sends:
|
||||
* `title:"<phrase>" AND created:[<day> TO <day|*>]`, either bracket style.
|
||||
*/
|
||||
export function matchesQuery(request, queryString) {
|
||||
const phrase = /title:"([^"]*)"/.exec(queryString)?.[1];
|
||||
if (phrase && !(request.title ?? '').includes(phrase)) return false;
|
||||
|
||||
const range = /created:([[{])\s*(\S+)\s+TO\s+(\S+?)\s*([\]}])/.exec(queryString);
|
||||
if (range) {
|
||||
const [, open, from, to, close] = range;
|
||||
const day = dayOf(request.created_at);
|
||||
// '[' includes the boundary day, '{' excludes it — the whole point.
|
||||
if (from !== '*' && (open === '[' ? day < from : day <= from)) return false;
|
||||
if (to !== '*' && (close === ']' ? day > to : day >= to)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* `fixture` is a list of `{request, indexed}`: the truth the list and the
|
||||
* per-id endpoint serve, and the possibly stale (or missing) copy the search
|
||||
* index has of it.
|
||||
*/
|
||||
export function createStub(fixture) {
|
||||
/** Every call the app made — the test asserts on what it asked for. */
|
||||
const calls = [];
|
||||
|
||||
const byActivity = (a, b) => activityOf(b) - activityOf(a);
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://stub');
|
||||
const send = (status, body) => {
|
||||
const payload = JSON.stringify(body);
|
||||
res.writeHead(status, { 'content-type': 'application/json' });
|
||||
res.end(payload);
|
||||
};
|
||||
|
||||
if (url.pathname === '/v3/signature_request/list') {
|
||||
const page = Number(url.searchParams.get('page') ?? '1');
|
||||
const pageSize = Number(url.searchParams.get('page_size') ?? '20');
|
||||
const queryString = url.searchParams.get('query');
|
||||
calls.push({ endpoint: 'list', page, query: queryString });
|
||||
|
||||
const source = queryString
|
||||
? fixture
|
||||
.map((entry) => entry.indexed)
|
||||
.filter(Boolean)
|
||||
.filter((request) => matchesQuery(request, queryString))
|
||||
: fixture.map((entry) => entry.request);
|
||||
const ordered = [...source].sort(byActivity);
|
||||
const numPages = Math.max(1, Math.ceil(ordered.length / pageSize));
|
||||
const slice = ordered.slice((page - 1) * pageSize, page * pageSize);
|
||||
return send(200, {
|
||||
signature_requests: slice,
|
||||
list_info: { page, num_pages: numPages, num_results: ordered.length },
|
||||
});
|
||||
}
|
||||
|
||||
const single = /^\/v3\/signature_request\/([^/]+)$/.exec(url.pathname);
|
||||
if (single) {
|
||||
const id = decodeURIComponent(single[1]);
|
||||
calls.push({ endpoint: 'get', id });
|
||||
const entry = fixture.find((e) => e.request.signature_request_id === id);
|
||||
// Unknown ids 404 like the real API — the sync walks over real rows the
|
||||
// stub knows nothing about and must simply report them as failures.
|
||||
if (!entry) return send(404, { error: { error_msg: `no such request: ${id}` } });
|
||||
return send(200, { signature_request: entry.request });
|
||||
}
|
||||
|
||||
return send(404, { error: { error_msg: `unhandled path: ${url.pathname}` } });
|
||||
});
|
||||
|
||||
return {
|
||||
calls,
|
||||
/** Resolves to the base URL once it is listening. */
|
||||
listen(port = 0) {
|
||||
return new Promise((resolve) => {
|
||||
server.listen(port, '127.0.0.1', () =>
|
||||
resolve(`http://127.0.0.1:${server.address().port}`),
|
||||
);
|
||||
});
|
||||
},
|
||||
close: () => new Promise((resolve) => server.close(resolve)),
|
||||
/** What a searching walk would have found — the lag, made visible. */
|
||||
search: (queryString) =>
|
||||
fixture
|
||||
.map((entry) => entry.indexed)
|
||||
.filter(Boolean)
|
||||
.filter((request) => matchesQuery(request, queryString)),
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- standalone mode
|
||||
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/^.*\//, ''))) {
|
||||
const args = process.argv.slice(2);
|
||||
const arg = (name, fallback) => {
|
||||
const at = args.indexOf(`--${name}`);
|
||||
return at === -1 ? fallback : args[at + 1];
|
||||
};
|
||||
const file = arg('fixture');
|
||||
if (file) {
|
||||
const fixture = JSON.parse(await readFile(file, 'utf8'));
|
||||
const stub = createStub(fixture);
|
||||
console.log(`[ds-stub] listening on ${await stub.listen(Number(arg('port', '8099')))}`);
|
||||
} else {
|
||||
console.error('usage: node scripts/ds-stub.mjs --fixture <file.json> [--port 8099]');
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user