Skip to content

Fix failing CI: OpenAPI lint + 32 pre-existing test failures it was masking - #274

Merged
prodbycorne merged 2 commits into
SmartDropLabs:mainfrom
praizehimm:fix/ci-lint-and-test-failures
Sep 7, 2026
Merged

Fix failing CI: OpenAPI lint + 32 pre-existing test failures it was masking#274
prodbycorne merged 2 commits into
SmartDropLabs:mainfrom
praizehimm:fix/ci-lint-and-test-failures

Conversation

@praizehimm

Copy link
Copy Markdown
Contributor

main's CI test job has been failing on every push/PR since the OpenAPI lint step started rejecting openapi.yaml, which also silently masked several other pre-existing failures further down the same job (ESLint runs with continue-on-error, but npm test doesn't, so nothing after the lint step ever actually ran).

Lint OpenAPI spec (the step that was actually failing on main)

  • openapi.yaml uses exclusiveMinimum: 0 in three places, which is the JSON-Schema-2020-12 numeric form. This spec is OpenAPI 3.0.3, which requires the boolean-modifier form (minimum: 0 + exclusiveMinimum: true). Fixed all three.

Once that step could pass, npm test itself failed 32/670 tests — all pre-existing, just never reached before:

  • test/api-docs.test.js — missing helmet import; the test called helmet() without requiring it.
  • test/cors.test.js — the test's own inline error handler read err.status and returned a flat string body; the real AppError class sets statusCode, and the real errorHandler returns {error:{code,message}}. Swapped in the actual errorHandler middleware instead of hand-rolling a mismatched one.
  • src/errors/AppError.jsWEBHOOK_TARGET_BLOCKED, INVALID_URL, and TIMEOUT were all thrown via new AppError(code, ...) elsewhere in the codebase but were never added to the ERROR_CODES registry, so the constructor's own unknown-code guard threw a plain Error instead of the intended typed one. For the SSRF guard this was especially bad: it meant the guard silently no-op'd (crashed and got swallowed) rather than blocking anything.
  • src/services/webhookDispatcher.js + src/routes/webhooks.js — the SSRF guard (ssrfGuard.js) already existed, fully implemented, but was never actually called from the /webhooks/:id/test endpoint. Wired it in, and reduced the response's last_error to a generic unreachable/error_response category instead of echoing raw network error strings (raw detail is still stored on the delivery record and logged server-side for operators).
  • src/services/dbHealth.js — read raw process.env.DATABASE_URL (unset in CI) instead of the already-resolved config.databaseUrl (which has proper test/dev defaults), and attempted a live SELECT 1 even though nothing in the app queries the database yet (it was added for api_key_audit_logs, not on any request path). /health now reports configured: true, checked: false, status: 'unused' instead of crashing/misreporting.
  • src/db/migrations/20260801000000_add_api_key_audit_logs.js — rewrote using raw SQL to match every other migration in the repo. The original used knex's schema builder (knex.schema.dropTable(...)), which is functionally correct but invisible to migrationSafety's text-based DROP TABLE scan, so this migration's rollback safety was never actually verified by the audit it's supposed to go through.
  • test/coingecko.test.js, test/coinmarketcap.test.jscircuitBreaker.getState() gained a last_success_at field at some point; updated the two full-object toEqual assertions to include it.
  • test/priceWebSocket.test.js — the test constructed its own new PriceSubscriptionManager() instead of using the module's real exported singleton. The actual WS server registers connections on the singleton, so the test's separate instance never saw them — connectionCount stuck at 0 and notifyPriceUpdates() never reached the real connected socket. Fixed to use the singleton directly.
  • test/health.test.js — every one of the 6 per-test cache mocks (main describe + 5 local re-mocks) was missing getCommandQueueLength, which /health now calls. This alone accounted for 14 of the 32 failures and made the file look like it was hanging (each failing request was actually just erroring instantly — the appearance of slowness was --forceExit masking a real crash-then-recover loop; with the fix the file runs in ~7s instead of ~72s).

Verified locally:

  • Full npm test suite: 670/670 passing (--runInBand; a couple of tests are flaky under heavy parallel worker contention against a single local Redis, unrelated to this fix)
  • npx knex migrate:latest and migrate:rollback --all against a real local Postgres instance
  • npx @redocly/cli lint openapi.yaml — 0 errors (11 pre-existing warnings unchanged, not in scope)

Not touched: npm audit --audit-level=high currently reports 2 high-severity transitive vulnerabilities (via stellar-sdk's toml dependency). This is unrelated to the CI failure this PR fixes — audit's advisory database is live and time-varying, and this step was passing in the last recorded CI run. Flagging it here rather than pulling in an unrelated dependency bump.

…tiple test/behavior gaps)

- openapi.yaml: fix 3 exclusiveMinimum errors. This is OpenAPI 3.0.3, which
  requires the boolean-modifier form (minimum + exclusiveMinimum: true), not
  the JSON-Schema-2020-12 numeric form the spec was using -- redocly lint
  correctly rejected it with "Expected type boolean but got integer".
- test/api-docs.test.js: add the missing "helmet" import; the test called
  helmet() without requiring it.
- test/cors.test.js: replace the test's ad-hoc inline error handler (which
  read err.status and returned a flat string body) with the app's real
  errorHandler middleware. AppError sets statusCode, not status, and the
  real handler returns {error:{code,message}}, so the shim was producing a
  500 with the wrong body shape instead of the rejected-origin's actual 403.
- src/errors/AppError.js: register WEBHOOK_TARGET_BLOCKED, INVALID_URL, and
  TIMEOUT. All three were thrown via new AppError(code, ...) from
  ssrfGuard.js and timeout.js but were never added to ERROR_CODES, so the
  constructor's own unknown-code guard threw a plain Error instead of the
  intended typed error -- silently turning a 422/504 into an unrelated
  crash (for the SSRF guard, the request fell through to whatever the route
  was already doing instead of being blocked at all).
- src/services/webhookDispatcher.js + src/routes/webhooks.js: wire the
  already-written (but never-called) ssrfGuard into the /test endpoint, and
  reduce the response's last_error to a generic unreachable/error_response
  category instead of echoing raw network error strings -- the raw detail
  is still stored on the delivery record and logged server-side.
- src/services/dbHealth.js: read the resolved config.databaseUrl (with its
  proper test/dev defaults) instead of raw, unset-in-CI
  process.env.DATABASE_URL, and stop attempting a live connection -- the
  database isn't on any request path yet (added for api_key_audit_logs), so
  /health now reports it as configured-but-unused rather than pinging a
  dependency nothing uses.
- src/db/migrations/20260801000000_add_api_key_audit_logs.js: rewrite using
  raw SQL (matching every other migration in the repo) instead of knex's
  schema builder -- the up()/down() were correct, but migrationSafety's
  text-scan safety check can't see a DROP TABLE inside
  knex.schema.dropTable(), so this migration's rollback was invisible to
  the safety audit.
- test/coingecko.test.js, test/coinmarketcap.test.js: circuitBreaker's
  getState() gained a last_success_at field; update the two full-object
  assertions to include it.
- test/priceWebSocket.test.js: use the module's real singleton instead of
  constructing a disconnected new PriceSubscriptionManager() -- the real WS
  server registers connections on the singleton, so the test's separate
  instance never saw them (connectionCount stuck at 0, notifyPriceUpdates()
  never reached the actual connected socket).
- test/health.test.js: every per-test cache mock was missing
  getCommandQueueLength (added to src/index.js's /health route since these
  mocks were written), which crashed every request through that route with
  "cache.getCommandQueueLength is not a function".

Verified locally: full jest suite (670/670, --runInBand), migration
up+rollback against a real Postgres instance, redocly lint (0 errors),
eslint (continue-on-error in CI, unaffected either way).

Not touched: the "npm audit --audit-level=high" step currently reports 2
high-severity transitive vulnerabilities (stellar-sdk's toml dependency).
This is unrelated to the CI failure this PR fixes -- audit's advisory
database is live/time-varying, and this step was green in the last passing
run recorded in this repo's history. Flagging rather than silently
upgrading a dependency this PR doesn't otherwise need to touch.
…ng high-severity audit finding

The previous commit on this branch fixed the lint/test failures but left
"npm audit --audit-level=high" untouched as out of scope. It's now the only
thing failing this PR's CI run, so fixing it properly:

- stellar-sdk (the old, deprecated, unscoped package) transitively depends
  on a vulnerable toml package (high severity, GHSA-82x6-q7mm-w9cf and
  GHSA-v5mp-jgw5-2x6j) at every version up to and including its latest
  release (13.3.0) -- `npm audit fix --force` would only "fix" this by
  downgrading to stellar-sdk@0.2.1, which is not a real fix.
- The actual fix is migrating to @stellar/stellar-sdk, the actively
  maintained successor stellar-sdk's own deprecation notice points to.
  Confirmed @stellar/stellar-sdk only drops the vulnerable toml dependency
  (switches to smol-toml) starting at 16.0.0, so pinned to ^16.0.0 rather
  than an earlier same-numbered release that would still carry it.
- Updated all 5 files that import from stellar-sdk (StrKey, scValToNative,
  SorobanRpc, Asset, Horizon) to import from @stellar/stellar-sdk instead,
  plus the 6 test files that mock/require it.
- SorobanRpc was renamed to the lowercase `rpc` export as part of the
  SDK's own v16 changes; updated the one call site (eventPoller.js) and its
  two test mocks accordingly. Every other imported symbol (StrKey,
  scValToNative, Asset, Horizon, Horizon.Server) is unchanged between the
  two packages at this version.
- Added babel.config.js + jest.config.js: @stellar/stellar-sdk's own
  dependencies (@noble/hashes, @noble/ed25519, uint8array-extras) are
  ESM-only. Plain Node resolves them fine via package.json `exports`, but
  Jest's default config ignores all of node_modules for transformation and
  hits the raw import/export syntax. This is a test-tooling-only change --
  no app code needed it, confirmed by a plain `node -e "require('./src/index')"`
  smoke test succeeding before this config existed.

Verified locally:
- npm audit --audit-level=high -- exit 0 (was exit 1)
- Full jest suite -- 670/670 passing (--runInBand)
- node -e "require('./src/index')" -- boots cleanly
- npx knex migrate:latest / migrate:rollback --all against a real local
  Postgres -- both succeed
@prodbycorne
prodbycorne merged commit 31f2555 into SmartDropLabs:main Sep 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants