Skip to content

test(api): cover unregistered, ambiguous and account-list-error RI exchange history scope - #1280

Draft
IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-558
Draft

IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-558

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

Scoped GET /api/ri-exchange/history matches each exchange record's STS account id through exchangeRecordInScope (internal/api/handler_ri_exchange.go). The id resolves to its single registered AWS account (uniqueAccountByExternalID), and the scope is checked against that account's UUID or name. If the id is unregistered or ambiguous, only a literal allow-list entry matches. If the account list cannot be loaded, the request errors. The issue reports that TestHandler_getRIExchangeHistory_AccountScope did not pin three of these branches: an unregistered host, an ambiguous id, and a ListCloudAccounts error.

This PR adds tests only. Production code is unchanged.

Scope: the maintainer's comment on the issue narrowed it to these three test gaps (Part A). The Part B approve-by-id claim was found stale: fetchAndAuthorizeRIExchange already runs the reshape scope check (#536), and the token paths are capability-gated by design. Part B is not touched here.

What main covers today

I probed main before writing anything (go test ./internal/api/ -run TestHandler_getRIExchangeHistory). #1169 merged after the issue was filed and closed part of the gap:

Mutant (local, reverted) Result on main
A1: unregistered id never matches (acct == nil -> return false) killed, but only indirectly: #1169's exact id-set expectations drop literal non-STS entries such as "u-b"/"prod"
A2: unregistered id always matches (acct == nil -> return true) killed by ScopedDropsOutOfScopeStoreRows
B1: an ambiguous id resolves to its first match (uniqueAccountByExternalID returns found instead of nil) survives
B2: an ambiguous id resolves to its last match survives
C1: a ListCloudAccounts error is ignored (accounts = nil, literal-only matching) killed by ScopedErrors/account_list_error, but only through a mock panic on an unexpected call
C2: a ListCloudAccounts error falls back to the unscoped GetRIExchangeHistory killed the same way (mock panic)

So the ambiguous-id gap was still open on main. The other two were covered only indirectly.

Change

New file internal/api/handler_ri_exchange_history_scope_test.go. It is a separate file so that it does not touch the lines #1116 edits in router_handlers_test.go, and because that file is far over the 500-line guideline. It reuses the existing exchangeAcctA/B/Other placeholder ids and scopedExchangeHistoryHandler.

Each table case checks two things through the real getRIExchangeHistory handler:

  1. The exact account-id set pushed into GetRIExchangeHistoryForAccounts, which is the SQL predicate from fix(api): RI exchange history caps at 500 rows before the account filter runs #308/fix(api): apply the RI exchange history limit after the account filter #1169. The ids are captured and asserted, so a wrong set fails an assertion instead of a mock panic.
  2. The returned record ids. The mock store returns every stored row whatever ids it gets, so the handler's per-row defense-in-depth re-check decides what comes back.

Tests:

  • TestHandler_getRIExchangeHistory_UnregisteredHostAccount
    • The literal STS id of an unregistered host sees that host's exchanges.
    • A registered account's UUID does not reach them.
    • UUID plus the literal id together see both.
  • TestHandler_getRIExchangeHistory_AmbiguousHostAccount
    • Two AWS accounts share one STS id. Each duplicate's UUID reaches nothing. Both are tested, so both first-match and last-match resolution fail.
    • The literal STS id still sees the shared id's exchanges.
    • An unambiguous account next to the duplicates still resolves.
  • TestHandler_getRIExchangeHistory_AccountListErrorFailsClosed
    • ListCloudAccounts fails.
    • Both history queries are set up with .Maybe() and would return rows.
    • The test asserts the wrapped error, a nil result, and that neither query ran.
    • The existing ScopedErrors/account_list_error only caught C1/C2 through a mock panic. This test fails them with clean assertions.

Interaction with #1116 (open; ID-only scope matching)

Verification

All of this is unit/mock evidence: the MockConfigStore/MockAuthService fixtures drive the real handler. Nothing touches Postgres or cloud APIs. All runs used AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, Go 1.26.9 on Linux.

  • go build ./..., go vet ./... and go vet -tags integration ./... pass.
  • go test ./internal/api/ -count=1 and go test ./internal/mocks/ -count=1 pass.
  • golangci-lint run --allow-parallel-runners ./internal/api/... (v2.10.1) reports 0 issues.
  • gocyclo: no non-test files changed.

Fail-before / pass-after (mutation probes). Each mutant was applied to handler_ri_exchange.go or handler_purchases_revoke.go (uniqueAccountByExternalID), then reverted with git checkout. I ran go test ./internal/api/ -count=1 -run '<new test>' for each new test separately:

Mutant Killed by (new test / failing subtests)
A1 unregistered never matches UnregisteredHostAccount: all three subtests (id set and records)
A2 unregistered always matches UnregisteredHostAccount/a_registered_account's_uuid_does_not_reach_the_unregistered_host (records)
B1 ambiguous -> first match AmbiguousHostAccount/uuid_of_the_first_duplicate_reaches_nothing (id set and records)
B2 ambiguous -> last match AmbiguousHostAccount/uuid_of_the_second_duplicate_reaches_nothing (id set and records)
C1 list error ignored AccountListErrorFailsClosed ("An error is expected but got nil")
C2 list error -> unscoped query AccountListErrorFailsClosed ("An error is expected but got nil")
D1 per-row re-check removed (if true) UnregisteredHostAccount and AmbiguousHostAccount (records)

B1 and B2 survive the whole existing TestHandler_getRIExchangeHistory* suite on main. They are killed by the new ambiguous-id test.

No integration test was added. The change is test-only, and an ambiguous id cannot be produced on real Postgres because cloud_accounts has UNIQUE(provider, external_id) (migration 000011). The ambiguous branch is reachable only through a store that does not enforce that, which is why the unit test models it. The existing TestRIExchangeHistory_ScopedUserSeesOlderOwnRows_Integration already exercises the real SQL predicate.

Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ 936996ad = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-337 ff3420c
fix/issue-169 7d75eb5
fix/issue-342 66b221f
fix/issue-310 5e11c21
fix/issue-326 5f0ea59
fix/issue-243 365e959
fix/issue-343 6c94036
fix/issue-312 d2848a8
fix/issue-316 acd60dd
fix/issue-339 75ea9b9
fix/issue-339-commitment-type 73bdb19
fix/issue-353 5d25fcd
fix/issue-313 3156bc4
fix/issue-357 53d9e58
fix/issue-368 0ba727d
fix/issue-419 61648c9
fix/issue-111 b04c372
fix/issue-686 0ad90b7
fix/issue-674 793095d
fix/issue-1055 4ec96f1
fix/issue-559 dfa9525
fix/issue-558 034eed4
fix/issue-874 1720539
fix/issue-666 1e120da
fix/issue-668 fb26345
fix/issue-568 ce04173
fix/issue-311 e54f250
fix/issue-177 0cbb88a
fix/issue-151 c21377c
fix/issue-166 41fb77b
fix/issue-27 2d67622
fix/issue-323 69288ae
fix/issue-59 36463e8
fix/issue-43 35e4069
fix/issue-158 6f76f8a
fix/issue-104 bb00491
fix/issue-55 971ef1b
fix/issue-361 89ac220
fix/issue-120 b458719
fix/issue-36 42bac6e
fix/issue-525 57c97dd
fix/issue-712 e5bcda0
fix/issue-72 6cc63ed

fix/issue-72 and fix/issue-312 conflict on one line in internal/api/handler_dashboard.go (both change the calculateCommitmentMetrics call). The tree used #72's side with #312's validated provider variable; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:

  • Passed at 936996ad: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks at 936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. tests/e2e is a separate module, so it was vetted under its own e2e tag. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1, 60-minute local timeout because the VM was shared):
    • At 936996ad, these pass: internal/server, internal/credentials, internal/scheduler, internal/database/postgres/migrations, internal/purchase and internal/api.
    • internal/config passes except TestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).
    • These were run on the first build of this tree (26795376, same branches at earlier heads): internal/analytics, internal/auth, internal/commitmentopts, internal/database and cmd/lambda/clear-rate-limit all pass. The later fixes don't touch those packages.
  • What the combined run caught, each fixed on its own branch before this PR opened:
    • a compile break between fix/issue-104's test and fix/issue-326's httpToLambdaRequest signature change;
    • fix/issue-316 rejecting legacy-keyed plans;
    • fix/issue-120 versus main's global-filter test.
  • Secrets: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope

  • Part B of the issue (approve-by-id scope). The maintainer verified it as stale or by design (see the issue comment). The optional defense-in-depth check of record.AccountID on session approve was offered only "if wanted" and is not done here.
  • Moving the existing TestHandler_getRIExchangeHistory_* tests out of the oversized router_handlers_test.go was left alone to avoid conflicts with fix(auth): bind group account scope entries to account IDs #1116.

Closes #558

Please mirror the issue's triage labels onto this PR: type/chore, triaged, priority/p3, severity/low, urgency/eventually, impact/few, effort/s.

🤖 Generated with Claude Code

…change history scope

The scoped RI exchange history tests did not pin three branches of the
account matching. Add unit tests that do:

- an exchange record whose STS id no registered AWS account carries is
  reached only by that literal id in the allow-list, never by a
  registered account's UUID;
- an STS id that two registered AWS accounts share resolves to neither,
  so neither UUID reaches it (both duplicates are tested, so resolving
  to the first or the last match each fails), while the literal id still
  does;
- a ListCloudAccounts failure returns an error and runs no history query,
  with both store queries set up to answer so a handler that carried on
  fails an assertion rather than a mock panic.

Each case checks both the account-id set pushed into the store query and
the handler's per-row re-check (the store returns every row).

Closes LeanerCloud#558

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/s Hours labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(api): RI exchange history scope tests miss unregistered host, ambiguous id and account-list error

1 participant