Skip to content

test(user-repository): add failure-handling regression coverage #1027 - #1179

Open
ejiromowarin wants to merge 1 commit into
RevoraOrg:masterfrom
ejiromowarin:fix/1027-user-failure-handling-regression
Open

ejiromowarin wants to merge 1 commit into
RevoraOrg:masterfrom
ejiromowarin:fix/1027-user-failure-handling-regression

Conversation

@ejiromowarin

@ejiromowarin ejiromowarin commented Sep 27, 2026 •

Copy link
Copy Markdown

Overview

This PR adds a focused, test-only regression suite that pins the three explicit failure / empty-result exits of UserRepository named in the issue — src/db/repositories/userRepository.ts:129, :182 and :201. Production code is untouched, so the existing public contract is preserved exactly and the suite documents current behaviour instead of redefining it.

Related Issue

Closes #1027

Changes

🧪 User Repository Failure Handling

  • [ADD] src/db/repositories/userRepository.regression.test.ts — 41 focused cases
    • line 129 — createUser with an empty RETURNING set: exact Error('Failed to create user') class/message (never UniqueConstraintError), never resolves with a partial or fabricated user, still throws when the driver reports rowCount: 1 for zero rows (the guard is pinned to rows.length), rejects on an unusable row (rows: [null]), deterministic across repeated calls, and the message leaks no SQL text, email or password hash.
    • line 182 — updateUser({ id }) with no updatable field: exact Error('User not found'), exactly one SELECT and never an UPDATE, explicitly-undefined optionals count as “not provided”, an empty-string id is an ordinary lookup miss, deterministic, and never resolves with a user.
    • line 201 — updateUser whose UPDATE … RETURNING * matched no row: exact Error('Failed to update user'), one statement only, keyed on rows.length (a stale rowCount still throws), never resolves with a partial user, 23505 stays UniqueConstraintError(field: 'email'), non-23505 errors are re-thrown by identity, and nothing sensitive leaks into the message.
    • Neighbouring success paths and boundaries for all three: fully mapped rows; five positional insert parameters; every supported update field bound in declaration order with the id last; startup/standard defaults and unknown stored tier → standard; stored NULL name → undefined; last_oidc_groups: null treated as a provided value that clears the column while [] serialises to "[]"; empty-string credentials/emails forwarded; hostile email payloads bound as parameters and never interpolated into SQL; the no-op update read returns the existing user without a write.
    • updateKycRiskTier delegation: success plus propagation of the exact 'Failed to update user' / UniqueConstraintError contracts.

📄 Documentation

  • [ADD] docs/user-failure-handling-regression.md — pinned contract per line, security assumptions, an abuse/failure-path table, deliberately pinned follow-up items, run instructions, evidence and the CI note below.

⚙️ Tooling

  • [MODIFY] package.json — adds test:coverage:user-failure, a focused run (existing contract suite + new regression suite) with a 95% threshold applied to src/db/repositories/userRepository.ts.

Verification Results

npx jest src/db/repositories/userRepository.regression.test.ts --coverage=false
✅ 41/41 passed (1 suite)

npm run test:coverage:user-failure
✅ 70/70 passed (2 suites)
✅ userRepository.ts: 100% statements / 100% branches / 100% functions / 100% lines
   (gate: 95% — every failure exit is provably executed)

surrounding suite (existing contract + direct consumers)
  src/db/repositories/userRepository.test.ts
  src/db/repositories/userRepository.regression.test.ts
  src/services/__tests__/kycRiskTierService.test.ts
  src/auth/register/registerService.test.ts
  src/services/startupAuthService.test.ts
✅ 101/101 passed (5 suites)

npx eslint src/db/repositories/userRepository.regression.test.ts
✅ exit 0 — no errors or warnings

npx tsc --noEmit
✅ 30 diagnostics, all pre-existing; none reference the new regression file

CI note

The upstream CI Gate workflow (.github/workflows/ci.yml) targets the main branch while the repository default branch is master, and it runs only audit:ci and validate:alert-mappings — it does not execute Jest. The pull-request check that does run (check-postmortem) passes. Adding a Jest job and pointing the workflow at the default branch is a repository-wide change, deliberately left out of this test-only PR; happy to follow up separately if maintainers want it.

Security Notes

  • A lost write (routing/replica surprise, aborted transaction) yields an empty RETURNING set — the suite proves the call fails loudly instead of reporting success.
  • Failure messages are fixed literals, asserted to leak no SQL text, email or password hash: safe for logs/alerts and not a probe surface for statement or credential detail.
  • Hostile email content (including '; DROP TABLE users; --) stays a bound parameter on both createUser and updateUser; the generated SQL never contains the payload.
  • Duplicate-email races stay distinct from empty-result failures: 23505 → UniqueConstraintError({ field: 'email' }), empty results → plain Error, so a softened guard cannot be mistaken for (or masked by) a conflict.
  • Non-23505 driver errors are re-thrown by identity, preserving upstream retry/alerting classification.
  • Deliberately pinned open items (documented, not changed): the repository performs no input validation/normalisation (callers own it), rows: [null] throws a TypeError, and updatePasswordHash resolves for an unknown id.

Acceptance Criteria Status

Acceptance Criteria Status
Cover the named behavior with focused automated tests, including the relevant success and failure paths ✅ 41 focused cases — all three failure exits, their neighbouring success paths, and boundary inputs
Preserve the existing public contract unless the change includes an explicit compatibility plan ✅ test-only diff; userRepository.ts is unchanged, messages/classes pinned as-is
Make error and boundary behavior observable and deterministic ✅ exact class + message assertions, rows.length vs rowCount, empty-string id/email, NULL name, last_oidc_groups: null, hostile SQL payloads
Run the focused test file and the surrounding suite ✅ 41/41 focused, 101/101 across five suites
Run the repository's configured lint, type, build, or contract checks ✅ eslint clean on the new file; tsc at its pre-existing baseline (nothing in this file)
Include the exercised cases and results in the pull request description ✅ this description + docs/user-failure-handling-regression.md

@ejiromowarin
ejiromowarin force-pushed the fix/1027-user-failure-handling-regression branch 2 times, most recently from d8f0a46 to 6a2c1b2 Compare October 15, 2026 05:26
…raOrg#1027)

Pin the createUser/updateUser empty-result guards and the no-op update 'User not found' exit with exact error contracts, boundary inputs and security assertions. Test-only; no production behaviour changes.

Closes RevoraOrg#1027
@ejiromowarin
ejiromowarin force-pushed the fix/1027-user-failure-handling-regression branch from 6a2c1b2 to 4082e66 Compare October 17, 2026 15:48
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.

Add regression coverage for User failure handling

1 participant