Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ web_modules/
.env.production.local
.env.local

# Local Postman deployment environments
/test/postman/dev.postman_environment.json
/test/postman/test.postman_environment.json
/test/postman/int.postman_environment.json
/test/postman/prod.postman_environment.json

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ npm run coverage
npm run quality
```

An importable Postman collection for manual `/search` contract and filter testing is documented in [test/postman/README.md](test/postman/README.md).

### Operational Logging

Every request receives an `X-Request-ID` response header. A caller-provided `X-Request-ID` is preserved when it contains only letters, numbers, `.`, `_`, `:`, or `-` and is no longer than 128 characters; otherwise the API generates a UUID. The same value appears as `request-id` in access logs and `requestId` in structured `/search` events.
Expand Down
1 change: 1 addition & 0 deletions controllers/searchController.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ async function executeOptimizedPostFilterSearch(

const fullPageHits = await fetchFullHitsByCveIds(pageCveIds, metrics, signal);
const response = {
status: 'ok',
data: {
took: combineTook(metrics.openSearchTookMs, metrics.pageFetchTookMs),
hits: {
Expand Down
10 changes: 10 additions & 0 deletions controllers/searchController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ describe('Search controller SEARCH handler', () => {
);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json.mock.calls[0][0].status).toBe('ok');
expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(0);
expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([]);
expect(mockApiSearch).not.toHaveBeenCalled();
Expand Down Expand Up @@ -549,6 +550,7 @@ describe('Search controller SEARCH handler', () => {
page: 1,
pageSize: 1,
totalPages: 1,
status: 'ok',
data: {
hits: {
total: {
Expand Down Expand Up @@ -632,6 +634,7 @@ describe('Search controller SEARCH handler', () => {
'cveMetadata.cveId',
'containers.adp.metrics.other'
]);
expect(res.json.mock.calls[0][0].status).toBe('ok');
expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(1);
expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([fullRecord]);
});
Expand Down Expand Up @@ -695,6 +698,7 @@ describe('Search controller SEARCH handler', () => {
'cveMetadata.cveId',
'containers.adp.metrics.other'
]);
expect(res.json.mock.calls[0][0].status).toBe('ok');
expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(1);
expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([fullRecord]);
});
Expand Down Expand Up @@ -764,6 +768,7 @@ describe('Search controller SEARCH handler', () => {
page: 1,
pageSize: 1,
totalPages: 1,
status: 'ok',
data: {
hits: {
total: {
Expand Down Expand Up @@ -829,6 +834,7 @@ describe('Search controller SEARCH handler', () => {
page: 1,
pageSize: 1,
totalPages: 1,
status: 'ok',
data: {
took: undefined,
hits: {
Expand Down Expand Up @@ -889,6 +895,7 @@ describe('Search controller SEARCH handler', () => {
page: 1,
pageSize: 1,
totalPages: 1,
status: 'ok',
data: {
took: undefined,
hits: {
Expand Down Expand Up @@ -994,6 +1001,7 @@ describe('Search controller SEARCH handler', () => {
page: 1,
pageSize: 1,
totalPages: 1,
status: 'ok',
data: {
hits: {
total: {
Expand Down Expand Up @@ -1261,6 +1269,7 @@ describe('Search controller SEARCH handler', () => {
}
}
]);
expect(res.json.mock.calls[0][0].status).toBe('ok');
});

test('SEARCH builds version-specific CVSS v3 metric filters', async () => {
Expand Down Expand Up @@ -1320,6 +1329,7 @@ describe('Search controller SEARCH handler', () => {
}
}
]);
expect(res.json.mock.calls[0][0].status).toBe('ok');
});

test('SEARCH escapes Lucene reserved characters in keywordSearch terms', async () => {
Expand Down
6,486 changes: 6,486 additions & 0 deletions test/postman/CVE-Search-API.postman_collection.json

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions test/postman/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Postman Manual Contract Tests

This directory contains an importable Postman collection for manually validating the CVE Search API. It covers every documented `POST /search` request field, representative intentional filter combinations, and the API's deterministic validation errors. The deprecated `/webSearch` endpoint is intentionally excluded.

## Files

- `CVE-Search-API.postman_collection.json`: generated Postman collection.
- `local.postman_environment.json`: local environment with `baseUrl` set to `http://localhost:4000`.
- `buildCollection.js`: maintainable source used to generate and consistency-check the JSON artifacts.

Deployment-specific environment files are intentionally ignored by Git so deployment URLs remain local. Use the local-only filenames `dev.postman_environment.json`, `test.postman_environment.json`, `int.postman_environment.json`, and `prod.postman_environment.json`.

## Run The Collection

1. Start the API and its configured OpenSearch dependency.
2. Import `CVE-Search-API.postman_collection.json` into Postman.
3. Import `local.postman_environment.json` or an ignored deployment-specific environment file.
4. Confirm that `baseUrl` points to the intended API instance.
5. Open the collection runner and run folders `00` through `16`.

Folders are grouped by service checks, positive filter coverage, intentional combinations, and expected validation failures. Positive search requests assert the response contract and use stable CVE fixtures to check totals or returned IDs. Negative requests assert the exact HTTP status, error code, and error message.

The collection checks that each response includes an `X-Request-ID`. Search responses also must contain the documented status, pagination, total, and hit-array fields.

## Environments

The collection uses the `baseUrl` variable and does not embed deployment credentials or URLs. The tracked localhost environment is a safe template for creating a deployment environment:

1. Duplicate `local.postman_environment.json`.
2. Rename the copy for the target deployment using one of the ignored filenames listed above.
3. Give the environment a distinct Postman `id` and `name`.
4. Replace only the `baseUrl` value with the approved deployment URL.
5. Keep `runFaultInjection` set to `false` unless performing an explicitly approved fault test.

The deployment file should resemble:

```json
{
"name": "CVE Search API - <Environment>",
"values": [
{ "key": "baseUrl", "value": "https://<environment-host>", "type": "default", "enabled": true },
{ "key": "runFaultInjection", "value": "false", "type": "default", "enabled": true }
]
}
```

Review and approve tests before targeting a shared or production deployment. The included fixtures were calibrated against both the dev CVE index and a smaller 1,111-record local index on August 20, 2026.

| Fixture | Coverage |
| ---------------- | --------------------------------------------------------- |
| `CVE-2024-5981` | CNA keyword, CWE, CVSS v2-v4, SSVC, references, and dates |
| `CVE-2022-31640` | ADP CWE, CVSS, SSVC, and references |
| `CVE-2020-1472` | KEV, SSVC, CVSS, and cross-container matching |
| `CVE-2022-31279` | Rejected-record filtering |
| `CVE-2022-38625` | Disputed-record filtering |
| `CVE-2025-23022` | CPE name, virtual match string, and version boundaries |

Index-wide searches assert only that at least one result exists. Exact totals are limited to requests constrained by one or more fixture `cveIds`, so different index sizes do not cause false failures. Constrained zero-result requests intentionally verify scope exclusion or literal substring behavior.

Because CVE records can be updated, refresh a fixture when a positive request stops satisfying its documented filter rather than weakening its assertions without investigating the data change. A local index must include the fixtures above to run every positive folder unchanged; validation-error folders do not depend on index contents.

## Fault-Injection Folder

Folder `17 - Manual fault injection - skipped by default` describes runtime errors that cannot be created by an ordinary black-box request. Its requests are skipped while `runFaultInjection` is `false`.

Setting `runFaultInjection` to `true` only enables the requests; it does not create their required failure conditions. Prepare and run one request at a time because the conditions are different and can conflict.

| Request | Required condition | Expected response |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------- |
| OpenSearch readiness failure | Make the configured OpenSearch index unavailable to the API. | `503 SEARCH_NOT_READY` |
| OpenSearch unavailable search | Make OpenSearch connectivity, permissions, or index access unavailable. | `503 OPEN_SEARCH_ERROR` |
| Search timeout | Deliberately lower the request deadline or delay OpenSearch beyond the configured deadline. | `504 SEARCH_TIMEOUT` |
| Cold-search queue full | Constrain cold CPE concurrency and queue depth, then keep all active and queued slots occupied. | `429 SEARCH_QUEUE_FULL` |
| Cold-search queue timeout | Keep cold CPE workers occupied longer than a deliberately shortened queue wait timeout. | `429 SEARCH_QUEUE_TIMEOUT` |
| Application rate limit | Enable the application limiter with a low threshold and exhaust the allowance before sending the check. | `429 SEARCH_RATE_LIMITED` |
| Unexpected upstream response | Use a test double or proxy that returns an invalid or incomplete OpenSearch response. | `502 UPSTREAM_SEARCH_ERROR` |

Run a fault check as follows:

1. Target a disposable local environment and prepare exactly one condition from the table.
2. Set `runFaultInjection` to `true` in the selected Postman environment.
3. Open folder `17` and send only the request for the prepared condition.
4. Confirm the asserted HTTP status, sanitized error code, message, and `X-Request-ID`.
5. Set `runFaultInjection` back to `false` and restore the normal application and dependency configuration.

The two queue checks require concurrent external setup because the Postman collection runner sends requests sequentially. A separate request must occupy the worker or queue before sending the Postman assertion request.

Do not enable fault injection against a shared or production environment without the target owner's approval. A WAF or load balancer can return a different error contract than the application-level response asserted by this folder.

## Known Validation Boundary

`VERSION_RANGE_VALUE` remains a defined middleware error, but it is not independently reachable through the current HTTP route. Empty or non-string `versionStart` and `versionEnd` values are rejected earlier by the route's shared string validation as `SEARCH_FIELD_STRING`. The collection tests the observable route contract and does not include an unreachable black-box assertion.

## Regenerate Or Check Artifacts

After changing the `/search` request schema, validation errors, or collection source, regenerate the Postman JSON files:

```bash
node test/postman/buildCollection.js
```

Check that committed artifacts match the builder without rewriting them:

```bash
node test/postman/buildCollection.js --check
```

The builder fails when a documented `SearchRequest` property has no positive Postman example. This protects field coverage but does not replace unit, integration, or live environment testing.
Loading
Loading