Repository navigation
test(api): assert mock expectations in the first half of the api tests - #1593
Conversation
Every internal/api test in the files up to handler_config_test.go that registers a required .On(...) now fails when that call never happens. Each testify mock gets a t.Cleanup(AssertExpectations) where it is built, and the shared constructors assert their own mocks (scopedHandler, newScopeTestHandler, newAPIKeyRoundtripHandler, sharedNameHandler, analyticsParamsCall). Helpers that built mocks without a *testing.T (hostCheckHandler, setupRouterForDispatch, sessionApproveHandler, tokenApproveHandler) now take one. Expectations that never fired were stubs for a path the test does not take, not missed production calls: - shared fixtures stub calls a direct gate call, a list or an early refusal never reaches; relaxUnreachedStubs marks those optional per test, next to the AssertNotCalled/side-channel checks that already pin them - the analytics helper's per-endpoint query stubs are optional, and a served request must now run exactly its endpoint's queries and read the allow-list - direct execute no longer saves the row itself (DirectExecute stamps the audit fields after its claim), and approvePurchase refuses a sessionless caller with a 401 before the execution lookup: the stale stubs are removed and the 401 is asserted - the service-config PUT now checks that a refused save never saves and an accepted one saves once No production code changes. Refs LeanerCloud#35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rQ4. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rQ3. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
Part A of #35:
internal/apitest files whose name sorts up to and includinghandler_config_test.go. Part B (the rest ofinternal/api, plusinternal/auth,internal/scheduler,internal/serverandinternal/email) is a separate branch. This PR changes tests only.Root cause
Most
internal/apitests registered testify expectations with.On(...)but never checked them. A handler could stop calling a collaborator, such as skippingRevokeAPIKeyAPIorRequestPasswordReset, and the success-path tests stayed green. Onmain, 22 of the 39 in-range files with.On(had noAssertExpectationsat all. 240 functions registered expectations on a mock that the function never asserted.Fix
t.Cleanup(func() { m.AssertExpectations(t) })where it is built. This follows the package's existing pattern; part B was told not to touch shared helper files, so there is no new shared assert helper.scopedHandler(its callers include part-B files, which pass on this branch),newScopeTestHandler(callers that pass a barenew(...)are covered too),newAPIKeyRoundtripHandler,sharedNameHandlerandanalyticsParamsCall.*testing.Tnow take one:hostCheckHandler,setupRouterForDispatch,sessionApproveHandlerandtokenApproveHandler. All their callers are in-range files.csrfGuardAuthand ofadminAnalyticsReqin range add the cleanup.adminAnalyticsReqkeeps its signature because part-B files call it.Expectations that never fired
No production code was wrong. Each case was a stub for a path the test does not take:
TestHandler_approvePurchase_EmptyTokenGetExecutionByIDapprovePurchasenow runsrequireSessionBeforeLookupfirst, so a caller with no session gets a 401 before the lookup. The test comment described the old order.recoverfails the test.TestExecutedNotification_DirectExecutePath,..._DirectExecute_NilNotifierNoPanicSavePurchaseExecutionDirectExecute, after its claim. The handler no longer saves the row.TestHandler_updateServiceConfig_CommitmentOptsOutsideTransaction(refused cases)SaveServiceConfig.Maybe(), and new checks: an accepted PUT saves exactly once, a refused one never saves.analyticsParamsCall(all analytics scope/account_ids tests)Query*,GetAllowedAccountsAPI.Maybe(), and new check: a served (200) request must read the allow-list and run exactly its endpoint's queries (analyticsPathQueries).TestAutoPurchaseGate_CapValidation/quoted numberGetPurchasePlandecodeRefused).TestApproveRegistration_EmptyRoleARNRequiresHostAccount(rejected cases)ApproveAccountRegistrationAssertNotCalled..Maybe().requireExecutionAccess,approveRegistration, the shared-name and editor-external-ID gate tests,getAccountwith an invalid UUIDValidateSession(andHasPermissionAPI)relaxUnreachedStubson those methods, in those tests only.TestPlannedPurchases_HidesPartiallyScopedParent,..._PermissionRefusedBeforeScope,..._FourEyesOn_RefusedBeforeAnyWriteGetExecutionByID/ListStoredRecommendationsAssertNotCalledor no-write checks.relaxUnreachedStubs, in those tests only.GetGroup,CreateAPIKey(andGetUserByIDfor the zero cap)*stored == nilcheck pins the second part.relaxUnreachedStubs, in those tests only. The create tests keep all three required.relaxUnreachedStubs(m *mock.Mock, methods...)lives inhandler_accounts_test.go. It marks a shared fixture's expectations for the named methods.Maybe().Times(0), which also clears a.Once()count. It is the same idea asoptionalCallsininternal/purchase(#1570), under a name that will not collide with part B.Left unasserted on purpose
These mocks have only
.Maybe()expectations, soAssertExpectationswould check nothing:apgCaller.handlerAndReq: part-B files call it, so its signature stays.newAuthFreeTxHandler,newMePermsAuth,meScopeAuthandapgConfigCall.bastion_resolve_test.go, which usesFnfields only.After this change, 26 functions still register
.Onwithout asserting in the same function. Each is one of these.Maybe()-only helpers, or a helper whose mocks the caller or the constructor asserts. At file level, 2 of the 39 files are left:discover_org_scope_test.go(asserted viascopedHandler) andhandler_auth_me_permissions_apikey_test.go(.Maybe()only).Refs #35
Verification
All of this is unit, mock and fixture evidence. There is no real-account path involved.
go build ./...,go vet ./...,go vet -tags integration ./...: clean.go test ./internal/api/ -count=1: ok.go test -race ./internal/api/ -count=1: ok, no data races.go test -race ./internal/api/ -count=4: ok. This checks that the cleanups do not flake against async calls.go test ./internal/mocks/ -count=1: ok.go test -tags integration -count=1 -timeout 20m -run '^(TestAnalyticsAccountIDs_SumsSelectedAccountsOnly_DB|TestCombinedPlanPurchase_ManualOffRoutedApproval|TestCombinedPlanPurchase_NonUSDApprovalRefusedUnderCap|TestCombinedPlanPurchase_AutomaticOnScheduledExecution|TestCombinedPlanPurchase_AutomaticAttachmentOverCapRefuses|TestCombinedPlanPurchase_PlannedProjectionReportsUnits|TestUpdateConfigPropagatesOnlyChangedDefaults|TestUpdateConfigPaymentDefaultSkipsAzureAndGCP|TestUpdateServiceConfig_ConcurrentPartialPutsKeepBothKeys|TestUpdateServiceConfig_ConcurrentFirstSavesKeepBothKeys|TestUpdateServiceConfig_ParallelSettingsSaveDoesNotExhaustPool)$' ./internal/api/: all 11 pass.golangci-lint run --allow-parallel-runners ./internal/api/, with and without--build-tags integration: 0 issues.Mutation probes. Each production change was applied temporarily to
origin/mainand to this branch, the package was run withgo test -count=1 ./internal/api/, and the change was reverted. The tests listed below survive onmainand fail here:revokeAPIKeyskipsRevokeAPIKeyAPIand returns successTestHandler_revokeAPIKey_SuccessdeleteAPIKeyskipsDeleteAPIKeyAPITestHandler_deleteAPIKey_SuccessforgotPasswordskipsRequestPasswordResetTestHandler_forgotPassword_Success,TestHandler_forgotPassword_ErrorStillReturnsSuccessQueryByServiceTestHandler_getAnalyticsTrends_MonthlyCommitmentAndUsageand the account_ids/scope endpoint matrices throughanalyticsPathQueriesOn
main, these were caught only by error-path tests elsewhere, if at all. The success-path tests now prove the call happened.Combined-tree verification. Tested on a local, never-pushed tree:
queue/platform@594ac3ec= origin/mainae6854b3plus: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.go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....--build-tags integration.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, one package at a time):internal/api,internal/auth,internal/config,internal/scheduler,internal/serverandinternal/testutilpass.scripts/test-git-secrets-allowlist.shpasses.Out of scope
internal/apifiles afterhandler_config_test.go,internal/auth,internal/scheduler,internal/server,internal/email): separate branch. TEST-03: 19 Go test files register .On() expectations without mock.AssertExpectations, incl. money-path regression suite #35 stays open until both parts merge.handler_coverage_test.goasserts nothing about response bodies is a separate issue. That file is in part B's range anyway.Maintainer: please mirror #35's triage labels onto this PR:
effort/s,impact/internal,priority/p3,severity/low,triaged,type/chore,urgency/eventually.🤖 Generated with Claude Code