diff --git a/specs/010-java-services/tasks.md b/specs/010-java-services/tasks.md index 5d20c1af..ffe65ba9 100644 --- a/specs/010-java-services/tasks.md +++ b/specs/010-java-services/tasks.md @@ -45,11 +45,11 @@ - compare status, a fixed header set and the SHA-256 of the body; - per-class normalisers, declared in `tools/java-parity/normalisers.ts`; - JSON report of identical, different and both-error counts, with no bodies or token values in the output. -- [x] T007 Add a self-test to `tools/java-parity/parity.spec.mjs`. Two stub servers that differ in one byte must report a difference; identical stubs must not. Show it failing with the comparison disabled. — #507 +- [x] T007 Add a self-test to `tools/java-parity/parity.spec.ts`. Two stub servers that differ in one byte must report a difference; identical stubs must not. Show it failing with the comparison disabled. — #507 - [ ] T008 [P] (With the first PDF comparison, T030: it needs `qpdf` in a container.) Add the PDF normaliser to `tools/java-parity/normalisers.ts`: - `qpdf --qdf --object-streams=disable --static-id`, run in a throwaway container; - mask CreationDate, ModDate and the cover-page date. - - Test it in `tools/java-parity/parity.spec.mjs`: two PDFs differing only in those fields compare equal, and a PDF with a changed body does not. + - Test it in `tools/java-parity/parity.spec.ts`: two PDFs differing only in those fields compare equal, and a PDF with a changed body does not. **Checkpoint**: routing tests know three outcomes; the harness detects a one-byte difference. diff --git a/specs/013-server-types/tasks.md b/specs/013-server-types/tasks.md index 78be9506..5594e85d 100644 --- a/specs/013-server-types/tasks.md +++ b/specs/013-server-types/tasks.md @@ -73,7 +73,7 @@ Each task: rename to `.ts`, update imports, file names that name it (R3 list), f - [ ] T023 The tools beside the services that they do not import (`tools/java-parity`, `tools/library-update`, `tools/svg-export-harness`, the check specs): converted the same way, or recorded as left and why. - Measured 23 Oct with `checkJs` on: 1,229 errors in 47 `.mjs` files, 70 of them in the five tools and 1,159 in 42 specs. The tools are converted, typed, held at zero both counts, and still run as before (parity and the library guard directly under Node, the harness through its new `playwright.config.ts`, which also mends the README's command: it had found no tests since the harness stopped being called a spec) — #643. The specs go next, in batches: the ten check specs outside the content service (load, started, workflows, check-move, check-baseline-totals, the HAR, flaky and lint checks, and content-node's hermetic), 60 errors — #644. The content service's ten smallest (exporter, cache-headers, molecule-details-bound, component-of, diseases, participating-entities, interactors, query-ids, map-results, mapping), 139 — #645. The next eight (pathways-low, complexes, overview-export, schema-objects, orthology, query-attribute-endpoint, interactors-static, referrers), 183 — #647. Seven more (contained-events, pathways-of, flag, query-attribute, discover, diagram-search, references-mapping), 235 — #649. icon, facet-query and the Java parity spec, 151 — #650. + Measured 23 Oct with `checkJs` on: 1,229 errors in 47 `.mjs` files, 70 of them in the five tools and 1,159 in 42 specs. The tools are converted, typed, held at zero both counts, and still run as before (parity and the library guard directly under Node, the harness through its new `playwright.config.ts`, which also mends the README's command: it had found no tests since the harness stopped being called a spec) — #643. The specs go next, in batches: the ten check specs outside the content service (load, started, workflows, check-move, check-baseline-totals, the HAR, flaky and lint checks, and content-node's hermetic), 60 errors — #644. The content service's ten smallest (exporter, cache-headers, molecule-details-bound, component-of, diseases, participating-entities, interactors, query-ids, map-results, mapping), 139 — #645. The next eight (pathways-low, complexes, overview-export, schema-objects, orthology, query-attribute-endpoint, interactors-static, referrers), 183 — #647. Seven more (contained-events, pathways-of, flag, query-attribute, discover, diagram-search, references-mapping), 235 — #649. icon, facet-query and the Java parity spec, 151 — #650. query-search and species-search, 151 — #651. - [ ] T024 `CLAUDE.md`: the rule for new service code (typed, and a load test entry for a new entry point) if one emerges. diff --git a/tools/content-node/query-search.spec.mjs b/tools/content-node/query-search.spec.ts similarity index 76% rename from tools/content-node/query-search.spec.mjs rename to tools/content-node/query-search.spec.ts index f4fa0994..479a3efa 100644 --- a/tools/content-node/query-search.spec.mjs +++ b/tools/content-node/query-search.spec.ts @@ -5,39 +5,47 @@ // search-core's fallback chain, grouped and plain search, paging, and the // report of a search that finds nothing. Java's answers measured 17 Oct 2026. import { URLSearchParams } from 'node:url'; +import type { RequestListener, Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; import { describe, expect, it, beforeAll, afterAll, vi } from 'vitest'; vi.mock('./graph.ts', () => ({ read: async () => [{ version: 97 }], configured: () => true, close: async () => {}, - envSetting: (key, fallback = '') => process.env[key] || fallback, + envSetting: (key: string, fallback = '') => process.env[key] || fallback, })); const { app } = await import('./server.ts'); const { facetQueryParams } = await import('./search-endpoints.ts'); -let base; -let server; -let solrServer; -let reportServer; -let asked; -let answer; -let reports; +/** A form or query as the service sent it to Solr: name and value, in order. */ +type Sent = [string, string][]; +let base: string; +let server: Server; +let solrServer: Server; +let reportServer: Server; +/** What the stand-in Solr was asked: its handler, and what it was sent. */ +let asked: [string, Sent][]; +/** The stand-in Solr's answer to a request. */ +let answer: (handler: string, sent: Sent) => unknown; +/** What reached the stand-in report service. */ +let reports: { url: string | undefined; body: string }[]; -const listen = async (handler) => { +const listen = async (handler: RequestListener) => { const { createServer } = await import('node:http'); const listening = createServer(handler); - await new Promise((resolve) => listening.listen(0, '127.0.0.1', resolve)); + await new Promise((resolve) => listening.listen(0, '127.0.0.1', resolve)); return listening; }; +const portOf = (listening: Server) => (listening.address() as AddressInfo).port; beforeAll(async () => { solrServer = await listen((request, response) => { let body = ''; - request.on('data', (chunk) => (body += chunk)); + request.on('data', (chunk: Buffer) => (body += chunk.toString())); request.on('end', () => { - const [path, query = ''] = request.url.split('?'); + const [path = '', query = ''] = (request.url ?? '').split('?'); const sent = [...new URLSearchParams(request.method === 'GET' ? query : body)]; const handler = path.replace('/solr', ''); asked.push([handler, sent]); @@ -47,38 +55,49 @@ beforeAll(async () => { }); reportServer = await listen((request, response) => { let body = ''; - request.on('data', (chunk) => (body += chunk)); + request.on('data', (chunk: Buffer) => (body += chunk.toString())); request.on('end', () => { reports.push({ url: request.url, body }); response.end(); }); }); - process.env.SOLR_URL = `http://127.0.0.1:${solrServer.address().port}/solr/reactome`; - process.env.REPORT_URL = `http://127.0.0.1:${reportServer.address().port}`; - await new Promise((resolve) => { - server = app().listen(0, '127.0.0.1', resolve); + process.env['SOLR_URL'] = `http://127.0.0.1:${portOf(solrServer)}/solr/reactome`; + process.env['REPORT_URL'] = `http://127.0.0.1:${portOf(reportServer)}`; + await new Promise((resolve) => { + server = app().listen(0, '127.0.0.1', () => resolve()); }); - base = `http://127.0.0.1:${server.address().port}`; + base = `http://127.0.0.1:${portOf(server)}`; }); afterAll(async () => { for (const name of ['SOLR_URL', 'REPORT_URL']) delete process.env[name]; - await new Promise((resolve) => server.close(resolve)); - await new Promise((resolve) => solrServer.close(resolve)); - await new Promise((resolve) => reportServer.close(resolve)); + for (const closing of [server, solrServer, reportServer]) + await new Promise((resolve) => closing.close(() => resolve())); }); -const get = (path) => fetch(`${base}/ContentService/search/${path}`); -const until = async (ready) => { +/** The nth request Solr was asked; one not asked is the test's failure. */ +const request = (at: number) => { + const found = asked[at]; + if (!found) throw new Error(`Solr was not asked a request ${at}`); + return found; +}; +/** The nth report sent; one not sent is the test's failure. */ +const report = (at: number) => { + const found = reports[at]; + if (!found) throw new Error(`no report ${at} was sent`); + return found; +}; +const get = (path: string) => fetch(`${base}/ContentService/search/${path}`); +const until = async (ready: () => boolean) => { for (let tries = 0; !ready() && tries < 400; tries += 1) await new Promise((resolve) => setTimeout(resolve, 5)); expect(ready()).toBe(true); }; -const fqs = (sent) => sent.filter(([name]) => name === 'fq').map(([, value]) => value); -const value = (sent, name) => sent.find(([key]) => key === name)?.[1]; +const fqs = (sent: Sent) => sent.filter(([name]) => name === 'fq').map(([, value]) => value); +const value = (sent: Sent, name: string) => sent.find(([key]) => key === name)?.[1]; /** Facets that found `found`, with two types. */ -const facets = (found) => ({ +const facets = (found: number) => ({ response: { numFound: found }, facet_counts: { facet_fields: { @@ -89,14 +108,14 @@ const facets = (found) => ({ }, }, }); -const doc = (id, name) => ({ +const doc = (id: string, name: string) => ({ dbId: id, stId: `R-HSA-${id}`, name, type: 'Protein', exactType: 'Protein', }); -const grouped = (groups) => ({ +const grouped = (groups: [string | null, object[]][]) => ({ grouped: { type_facet: { matches: 8, @@ -113,7 +132,13 @@ const grouped = (groups) => ({ describe('facetQueryParams, as the fallback chain changes the query', () => { it('asks for the deleted entries, and with another parser', () => { expect( - facetQueryParams({ query: 'x', scope: 'BOTH', deleted: true, parserType: 'DISMAX' }) + facetQueryParams({ + query: 'x', + scope: 'BOTH', + deleted: true, + parserType: 'DISMAX', + // The endpoint fills every list; an absent one reads as empty. + } as Parameters[0]) ).toEqual([ ['fq', 'deleted:true'], ['defType', 'dismax'], @@ -142,7 +167,7 @@ describe('/query', () => { '/reactome/facet', '/reactome/search/grouped', ]); - expect(asked[1][1]).toEqual([ + expect(request(1)[1]).toEqual([ ['wt', 'json'], ['json.nl', 'flat'], ['fq', ''], @@ -214,7 +239,10 @@ describe('/query', () => { answer = (handler) => handler.endsWith('/facet') ? facets(0) : { response: { numFound: 0, docs: [] } }; const response = await get('query?query=zzz&types=Protein&scope=PHYSICAL_ENTITY'); - expect([response.status, (await response.json()).targets]).toEqual([404, null]); + expect([response.status, ((await response.json()) as { targets: unknown }).targets]).toEqual([ + 404, + null, + ]); // As given, then without filters in the default scope, then deleted, // then by dismax, then in every scope; then the target core. expect( @@ -234,8 +262,8 @@ describe('/query', () => { ['/target/search', '', ''], ]); await until(() => reports.length === 1); - expect(reports[0].url.split('?')[0]).toBe('/report/search/notfound'); - expect(reports[0].body).toBe('{"term":"zzz","resource":""}'); + expect((report(0).url ?? '').split('?')[0]).toBe('/report/search/notfound'); + expect(report(0).body).toBe('{"term":"zzz","resource":""}'); }); it('with facets but an empty page, names the targets in the 404 and reports them', async () => { @@ -252,25 +280,25 @@ describe('/query', () => { return grouped([]); }; const response = await get('query?query=P15056&scope=BOTH&Start%20row=900'); - expect([response.status, (await response.json()).targets]).toEqual([ + expect([response.status, ((await response.json()) as { targets: unknown }).targets]).toEqual([ 404, [{ term: 'P15056', resource: 'UNIPROT', target: true }], ]); await until(() => reports.length === 1); - expect(reports[0].url.split('?')[0]).toBe('/report/search/targets'); + expect((report(0).url ?? '').split('?')[0]).toBe('/report/search/targets'); }); it('drops a filter the facets did not select from the search too, unless filters are forced', async () => { for (const [forced, calls, type] of [ ['', 2, ''], ['&Force%20filters=true', 1, 'type_facet:("Nope")'], - ]) { + ] as const) { asked = []; answer = (handler) => handler.endsWith('/facet') ? facets(8) : grouped([['Protein', [doc('1', 'TP53')]]]); await get(`query?query=TP53&types=Nope${forced}`); expect(asked.filter(([handler]) => handler.endsWith('/facet'))).toHaveLength(calls); - expect(fqs(asked.at(-1)[1])[1]).toBe(type); + expect(fqs(request(asked.length - 1)[1])[1]).toBe(type); } }); @@ -278,7 +306,7 @@ describe('/query', () => { answer = (handler) => (handler.endsWith('/facet') ? facets(8) : grouped([])); const blank = await get('query?query=TP53&scope=%20'); expect(blank.status).toBe(500); - expect((await blank.json()).messages).toEqual([ + expect(((await blank.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); answer = () => ({ response: { numFound: 0, docs: [] } }); @@ -299,6 +327,7 @@ describe('/query', () => { const response = await get('query?query=TP53&species=Nowhere'); expect(response.status).toBe(404); const search = asked.find(([handler]) => handler.endsWith('/search/grouped')); + if (!search) throw new Error('the grouped search was not asked'); expect([value(search[1], 'group.offset'), value(search[1], 'group.limit')]).toEqual(['0', '0']); await until(() => reports.length === 1); }); @@ -333,9 +362,9 @@ describe('/query', () => { }; return { response: { numFound: 8, docs: [] } }; }; - const body = await ( + const body = (await ( await get('query?query=%20P04637&cluster=false&Start%20row=900&scope=BOTH') - ).json(); + ).json()) as { targetResults: unknown }; // In the order Java's HashSet writes them, not merely both present. expect(body.targetResults).toEqual([ { term: 'P04637', resource: 'UNIPROT', target: true }, @@ -348,7 +377,7 @@ describe('/query', () => { handler.endsWith('/facet') ? facets(8) : { response: { numFound: 0, docs: [] } }; const response = await get('query?query=TP53'); expect(response.status).toBe(500); - expect((await response.json()).messages).toEqual([ + expect(((await response.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); }); @@ -356,8 +385,8 @@ describe('/query', () => { it('leaves out the type of a group with none', async () => { answer = (handler) => handler.endsWith('/facet') ? facets(8) : grouped([[null, [doc('1', 'TP53')]]]); - const body = await (await get('query?query=TP53')).json(); - expect(Object.keys(body.results[0])).toEqual(['entries', 'entriesCount', 'rowCount']); + const body = (await (await get('query?query=TP53')).json()) as { results: object[] }; + expect(Object.keys(body.results[0] ?? {})).toEqual(['entries', 'entriesCount', 'rowCount']); }); it("is Spring's 400s, and Java's 500 for a blank cluster before Solr is asked", async () => { @@ -365,11 +394,11 @@ describe('/query', () => { const missing = await get('query'); expect(missing.status).toBe(400); const parser = await get('query?query=x&parserType=lucene'); - expect((await parser.json()).messages[0]).toMatch( + expect(((await parser.json()) as { messages: string[] }).messages[0]).toMatch( /No enum constant org\.reactome\.server\.search\.domain\.ParserType\.lucene$/ ); const blank = await get('query?query=x&cluster=%20'); - expect([blank.status, (await blank.json()).messages]).toEqual([ + expect([blank.status, ((await blank.json()) as { messages: string[] }).messages]).toEqual([ 500, ['Something unexpected happened and the error has been reported.'], ]); @@ -384,7 +413,7 @@ describe('/query/paginated', () => { handler.endsWith('/facet') ? facets(8) : grouped([['Protein', [doc('1', 'TP53')]]]); await get('query/paginated?query=TP53&page=3&rowCount=10'); // Two types: five rows each, from the tenth. - expect([value(asked[1][1], 'group.offset'), value(asked[1][1], 'group.limit')]).toEqual([ + expect([value(request(1)[1], 'group.offset'), value(request(1)[1], 'group.limit')]).toEqual([ '10', '5', ]); @@ -411,7 +440,7 @@ describe('/query/paginated', () => { }; const response = await get('query/paginated?query=TP53&page=1&rowCount=10'); expect(response.status).toBe(500); - expect((await response.json()).messages).toEqual([ + expect(((await response.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); // This search's own report, not a late one from the test before. @@ -430,7 +459,7 @@ describe('/query/paginated', () => { : grouped([['Protein', [doc('1', 'TP53')]]]); const response = await get('query/paginated?query=TP53&page=1&rowCount=10'); expect(response.status).toBe(500); - expect((await response.json()).messages).toEqual([ + expect(((await response.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); }); @@ -443,13 +472,13 @@ describe('/query/paginated', () => { 'rowCount=10', "Required request parameter 'page' for method parameter type Integer is not present", ], - ]) { + ] as const) { const response = await get(`query/paginated?query=x&${query}`); - expect([query, response.status, (await response.json()).messages]).toEqual([ + expect([ query, - 400, - [message], - ]); + response.status, + ((await response.json()) as { messages: string[] }).messages, + ]).toEqual([query, 400, [message]]); } }); }); diff --git a/tools/content-node/species-search.spec.mjs b/tools/content-node/species-search.spec.ts similarity index 70% rename from tools/content-node/species-search.spec.mjs rename to tools/content-node/species-search.spec.ts index a72ad75a..75fbcb71 100644 --- a/tools/content-node/species-search.spec.mjs +++ b/tools/content-node/species-search.spec.ts @@ -5,54 +5,74 @@ // the reactome and target cores) and for the report service a search that // finds nothing is reported to. Java's answers measured 16 Oct 2026. import { URLSearchParams } from 'node:url'; +import type { RequestListener, Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; import { describe, expect, it, beforeAll, afterAll, vi } from 'vitest'; +/** What the stand-in graph shares with the tests: how often it was read, and whether it is down. */ +const graphState = globalThis as { graphReads?: number; graphDown?: boolean }; + vi.mock('./graph.ts', () => ({ // The release, for the report -- or the graph down, when a test says so. read: async () => { - globalThis.graphReads = (globalThis.graphReads ?? 0) + 1; - if (globalThis.graphDown) throw new Error('graph down'); + const shared = globalThis as { graphReads?: number; graphDown?: boolean }; + shared.graphReads = (shared.graphReads ?? 0) + 1; + if (shared.graphDown) throw new Error('graph down'); return [{ version: 97 }]; }, configured: () => true, close: async () => {}, - envSetting: (key, fallback = '') => process.env[key] || fallback, + envSetting: (key: string, fallback = '') => process.env[key] || fallback, })); const { app } = await import('./server.ts'); -let base; -let server; -let solrServer; -let reportServer; -let asked; -let answer; -let reports; +/** A form or query as the service sent it to Solr: name and value, in order. */ +type Sent = [string, string][]; +/** What reached the stand-in report service. */ +type Report = { + url: string | undefined; + authorization: string | undefined; + type: string | undefined; + bytes: Buffer; + body: string; +}; +let base: string; +let server: Server; +let solrServer: Server; +let reportServer: Server; +/** What the stand-in Solr was asked: method and handler, and what it was sent. */ +let asked: [string, Sent][]; +/** The stand-in Solr's answer to a request. */ +let answer: (url: string, sent: Sent) => unknown; +let reports: Report[]; let hanging = false; -const listen = async (handler) => { +const listen = async (handler: RequestListener) => { const { createServer } = await import('node:http'); const listening = createServer(handler); - await new Promise((resolve) => listening.listen(0, '127.0.0.1', resolve)); + await new Promise((resolve) => listening.listen(0, '127.0.0.1', resolve)); return listening; }; +const portOf = (listening: Server) => (listening.address() as AddressInfo).port; beforeAll(async () => { solrServer = await listen((request, response) => { let body = ''; - request.on('data', (chunk) => (body += chunk)); + request.on('data', (chunk: Buffer) => (body += chunk.toString())); request.on('end', () => { // A GET carries its parameters in the URL, a POST in its body. - const [path, query = ''] = request.url.split('?'); + const url = request.url ?? ''; + const [path = '', query = ''] = url.split('?'); const sent = [...new URLSearchParams(request.method === 'GET' ? query : body)]; - asked.push([`${request.method} ${path.replace('/solr', '')}`, sent]); + asked.push([`${request.method ?? ''} ${path.replace('/solr', '')}`, sent]); response.setHeader('Content-Type', 'application/json'); - response.end(JSON.stringify(answer(request.url, sent))); + response.end(JSON.stringify(answer(url, sent))); }); }); reportServer = await listen((request, response) => { - const chunks = []; - request.on('data', (chunk) => chunks.push(chunk)); + const chunks: Buffer[] = []; + request.on('data', (chunk: Buffer) => chunks.push(chunk)); request.on('end', () => { reports.push({ url: request.url, @@ -65,32 +85,43 @@ beforeAll(async () => { if (!hanging) response.end(); }); }); - process.env.SOLR_URL = `http://127.0.0.1:${solrServer.address().port}/solr/reactome`; - process.env.REPORT_URL = `http://127.0.0.1:${reportServer.address().port}`; - process.env.REPORT_USER = 'reporter'; - process.env.REPORT_PASSWORD = 'not-a-secret'; - await new Promise((resolve) => { - server = app().listen(0, '127.0.0.1', resolve); + process.env['SOLR_URL'] = `http://127.0.0.1:${portOf(solrServer)}/solr/reactome`; + process.env['REPORT_URL'] = `http://127.0.0.1:${portOf(reportServer)}`; + process.env['REPORT_USER'] = 'reporter'; + process.env['REPORT_PASSWORD'] = 'not-a-secret'; + await new Promise((resolve) => { + server = app().listen(0, '127.0.0.1', () => resolve()); }); - base = `http://127.0.0.1:${server.address().port}`; + base = `http://127.0.0.1:${portOf(server)}`; }); afterAll(async () => { for (const name of ['SOLR_URL', 'REPORT_URL', 'REPORT_USER', 'REPORT_PASSWORD']) delete process.env[name]; - await new Promise((resolve) => server.close(resolve)); - await new Promise((resolve) => solrServer.close(resolve)); - await new Promise((resolve) => reportServer.close(resolve)); + for (const closing of [server, solrServer, reportServer]) + await new Promise((resolve) => closing.close(() => resolve())); }); -const get = (path, headers = {}) => +/** The nth request Solr was asked; one not asked is the test's failure. */ +const request = (at: number) => { + const found = asked[at]; + if (!found) throw new Error(`Solr was not asked a request ${at}`); + return found; +}; +/** The nth report sent; one not sent is the test's failure. */ +const report = (at: number) => { + const found = reports[at]; + if (!found) throw new Error(`no report ${at} was sent`); + return found; +}; +const get = (path: string, headers: Record = {}) => fetch(`${base}/ContentService/search/fireworks${path}`, { headers }); -const until = async (ready) => { +const until = async (ready: () => boolean) => { for (let tries = 0; !ready() && tries < 400; tries += 1) await new Promise((resolve) => setTimeout(resolve, 5)); expect(ready()).toBe(true); }; -const fqs = (sent) => sent.filter(([name]) => name === 'fq').map(([, value]) => value); +const fqs = (sent: Sent) => sent.filter(([name]) => name === 'fq').map(([, value]) => value); describe('/fireworks, the species-wide search', () => { it('asks /fireworks for the species and those without, writing entries with their species', async () => { @@ -157,15 +188,15 @@ describe('/fireworks, the species-wide search', () => { ], ]); await until(() => reports.length === 1); - const [report] = reports; - expect(report.url).toBe( + const sent = report(0); + expect(sent.url).toBe( '/report/search/notfound?releaseNumber=97&ip=203.0.113.7&agent=a+browser' ); - expect(report.authorization).toBe( + expect(sent.authorization).toBe( `Basic ${Buffer.from('reporter:not-a-secret').toString('base64')}` ); - expect(report.type).toBe('application/json'); - expect(report.body).toBe('{"term":"zzz qqq","resource":""}'); + expect(sent.type).toBe('application/json'); + expect(sent.body).toBe('{"term":"zzz qqq","resource":""}'); }); it('names the terms the target core knows, in their HashSet order, and reports the targets', async () => { @@ -202,10 +233,13 @@ describe('/fireworks, the species-wide search', () => { '"messages":["No entries found for query: BRAF VHL"],' + '"targets":[{"term":"VHL","resource":"UNIPROT","target":false},{"term":"BRAF","resource":"UNIPROT","target":true}]}' ); - expect(fqs(asked[1][1])).toEqual([]); + expect(fqs(request(1)[1])).toEqual([]); await until(() => reports.length === 2); const byPath = Object.fromEntries( - reports.map((report) => [report.url.split('?')[0], report.body]) + reports.map((report): [string, string] => [ + (report.url ?? '').split('?')[0] ?? '', + report.body, + ]) ); expect(byPath).toEqual({ '/report/search/targets': '[{"term":"BRAF","resource":"UNIPROT"}]', @@ -226,10 +260,10 @@ describe('/fireworks, the species-wide search', () => { }).end() ); await until(() => reports.length === 1); - expect([...reports[0].bytes]).toEqual([ + expect([...report(0).bytes]).toEqual([ ...Buffer.from('{"term":"café ?","resource":""}', 'latin1'), ]); - expect(reports[0].url).toMatch(/&agent$/); + expect(report(0).url).toMatch(/&agent$/); }); it('answers without waiting on a report service that never answers', async () => { @@ -253,10 +287,9 @@ describe('/fireworks, the species-wide search', () => { ? { response: { numFound: 1, docs: [{ accessions: ['P1'], resource: 'UNIPROT' }] } } : { response: { numFound: 0, docs: [] } }; const response = await get('?query=x&scope=BOTH'); - expect([response.status, (await response.json()).messages]).toEqual([ - 500, - ['Something unexpected happened and the error has been reported.'], - ]); + expect([response.status, ((await response.json()) as { messages: string[] }).messages]).toEqual( + [500, ['Something unexpected happened and the error has been reported.']] + ); // Reports go in order: once a later search's report has come, one from // this search would have come before it. answer = () => ({ response: { numFound: 0, docs: [] } }); @@ -270,43 +303,43 @@ describe('/fireworks, the species-wide search', () => { answer = () => ({ response: { numFound: 0, docs: [] } }); await get('?query=a%1Fb', { 'User-Agent': 'x*y~z' }); await until(() => reports.length === 1); - expect(reports[0].url).toMatch(/&agent=x\*y%7Ez$/); - expect(reports[0].body).toBe('{"term":"a\\u001Fb","resource":""}'); + expect(report(0).url).toMatch(/&agent=x\*y%7Ez$/); + expect(report(0).body).toBe('{"term":"a\\u001Fb","resource":""}'); // A backslash of the term's own, before "u001f", is no escape to change. reports = []; await get('?query=c%5Cu001fd'); await until(() => reports.length === 1); - expect(reports[0].body).toBe('{"term":"c\\\\u001fd","resource":""}'); + expect(report(0).body).toBe('{"term":"c\\\\u001fd","resource":""}'); }); it("authenticates with Java's default credentials when none are set", async () => { reports = []; answer = () => ({ response: { numFound: 0, docs: [] } }); - delete process.env.REPORT_USER; - delete process.env.REPORT_PASSWORD; + delete process.env['REPORT_USER']; + delete process.env['REPORT_PASSWORD']; try { await get('?query=zzz'); await until(() => reports.length === 1); } finally { - process.env.REPORT_USER = 'reporter'; - process.env.REPORT_PASSWORD = 'not-a-secret'; + process.env['REPORT_USER'] = 'reporter'; + process.env['REPORT_PASSWORD'] = 'not-a-secret'; } - expect(reports[0].authorization).toBe( + expect(report(0).authorization).toBe( `Basic ${Buffer.from('default:default').toString('base64')}` ); }); it('finds results without the graph: the release is read only to report', async () => { answer = () => ({ response: { numFound: 1, docs: [{ dbId: '1' }] } }); - globalThis.graphDown = true; - const reads = globalThis.graphReads ?? 0; + graphState.graphDown = true; + const reads = graphState.graphReads ?? 0; try { const response = await get('?query=KNTC1'); expect(response.status).toBe(200); // Not even the cached release is asked for. - expect(globalThis.graphReads ?? 0).toBe(reads); + expect(graphState.graphReads ?? 0).toBe(reads); } finally { - globalThis.graphDown = false; + graphState.graphDown = false; } }); @@ -317,7 +350,7 @@ describe('/fireworks, the species-wide search', () => { : { response: { numFound: 0, docs: [] } }; const response = await get('?query=x&scope=BOTH'); expect(response.status).toBe(500); - expect((await response.json()).messages).toEqual([ + expect(((await response.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); }); @@ -338,7 +371,8 @@ describe('/fireworks, the species-wide search', () => { : { response: { numFound: 0, docs: [] } }; await get(`?query=${['AC', 'AR', ...fillers].join('%20')}&scope=BOTH`); await until(() => reports.length === 2); - const targets = reports.find((report) => report.url.startsWith('/report/search/targets')); + const targets = reports.find((report) => report.url?.startsWith('/report/search/targets')); + if (!targets) throw new Error('no targets report was sent'); expect(targets.body).toBe( '[{"term":"AR","resource":"UNIPROT"},{"term":"AC","resource":"UNIPROT"}]' ); @@ -350,7 +384,7 @@ describe('/fireworks, the species-wide search', () => { expect(missing.status).toBe(400); const blank = await get('?query=x&includeInteractors=%20'); expect(blank.status).toBe(500); - expect((await blank.json()).messages).toEqual([ + expect(((await blank.json()) as { messages: string[] }).messages).toEqual([ 'Something unexpected happened and the error has been reported.', ]); expect(asked).toHaveLength(0); @@ -367,7 +401,7 @@ describe('/fireworks, the species-wide search', () => { ) ).not.toThrow(); await until(() => reports.length === 1); - expect(reports[0].url).toMatch(/&ip=%3F&/); - expect(reports[0].body).toBe('{"term":"x?","resource":""}'); + expect(report(0).url).toMatch(/&ip=%3F&/); + expect(report(0).body).toBe('{"term":"x?","resource":""}'); }); });