Repository navigation
Conversation
GET /api/history/analytics and /api/history/breakdown passed interval and dimension straight to the Postgres analytics client. Its allowlist rejected unknown values with a plain error, which the handler wrapped and the router answered as a 500. The handlers now check both values against the same allowlist functions (intervalToTruncUnit, dimensionToColumn) before any query runs and answer 400 with the accepted values. Defaults are unchanged: interval hourly, dimension service. Refs LeanerCloud#1102 (item 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateAccount validated the request's provider and returned it in the response, but UpdateCloudAccount never writes the provider column, so the change was silently dropped and the caller was told it had been saved. Saving it is not safe either: credentials, service overrides and plan memberships all depend on the provider, and plan membership is only checked against it when an account is added to a plan. So the provider is treated as fixed at creation: a PUT whose provider differs from the stored one now gets a 400 telling the caller to delete and re-add the account, and nothing is saved. The check runs after the account-scope check, so a caller who cannot see the account still gets a 404. The settings UI always sends the account's own provider, so it is unaffected. Refs LeanerCloud#1102 (item 3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/purchases/{id}/revoke asks GetExecutionByID whether the ID is a
scheduled execution before it looks in purchase_history, and falls
through on ErrNotFound. An Azure purchase ID is an ARM path, and
execution_id is a uuid column, so Postgres refused the cast with SQLSTATE
22P02. GetExecutionByID returned that as a plain error, and every Azure
revoke failed with a 500 before reaching the Azure return path. The unit
tests mocked the lookup as ErrNotFound for ARM paths, so they never saw
it.
GetExecutionByID now maps 22P02 to ErrNotFound: no execution can have an
ID that is not a UUID. Every other error is returned unchanged, so a real
database failure is still a 500.
Tests, on real Postgres:
- the store returns ErrNotFound for ARM paths, an arbitrary string and "",
and still finds a real execution afterwards;
- revokePurchase with stubbed Azure SDK clients returns the Azure
reservation and records revoked_at, and answers 404 for an unknown
ARM ID. Both failed with the 22P02 error before.
Refs LeanerCloud#1102 (item 5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
approveRegistration flipped the registration to approved, then created the cloud account and linked it in separate statements. When the create failed (the provider + external_id already registered, or any database error), the registration stayed approved with no account. Approving it again was refused with a 409 and the UI offered no way back. A failed link was only logged. A new store method, ApproveAccountRegistration, runs the conditional status change (WHERE status = 'pending'), the account insert and the link in one transaction. The status change goes first, so its row lock queues any concurrent approve or reject of the same registration behind the transaction. After a commit the other request matches no row and gets 409; after a rollback it proceeds as if the approval never ran. The handler maps a conflict or a duplicate account to 409 and anything else to 500. Credentials are still stored after the commit, best effort, as before. The in-memory reset of reg.RegCredentialPayload is removed: neither registration UPDATE writes that column, so it never cleared anything. That gap is pre-existing and left for a separate fix. Tests: - unit: the handler makes the single store call and none of the old piecemeal ones, and maps conflict/duplicate/other errors; - real Postgres: a duplicate account leaves the registration pending and unreviewed (failed before: approved, no account); 8 concurrent approvals yield one success, seven 409s, one account, and a link to it; a reject issued while an approval is inside its account insert waits on the row lock and then gets 409 (approval committed) or succeeds with no account left (approval failed), with the insert slowed or failed by a test-only trigger. Refs LeanerCloud#1102 (item 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
VERDICT: REQUEST_CHANGES 340cf18 Findings:
Item verification (all on the merged and resolved tree):
Attacked:
|
|
Independent review 2 (claude-fleet, cold): APPROVE 340cf18 The approval covers the code. The PR does not merge cleanly into current origin/main, so it needs a mechanical rebase first. The hand-resolved merged tree passes (see below). Findings:
Attacked:
Verified locally, on
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
Resolve the conflict in internal/api/handler_analytics.go with main's resolveAnalyticsAccountFilter (LeanerCloud#1161): drop this branch's local accountID lines and keep analyticsInterval/analyticsDimension next to main's account filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… found Review of LeanerCloud#1141 found that mapping every Postgres error to ErrNotFound passed all tests, so a real DB failure could have become a 404 on the revoke route. Add pgxmock unit tests on the real PostgresStore: 22P02 is ErrNotFound, while 57P01, 53300, 42P01 and 22P03 are returned as they are, with the *pgconn.PgError still reachable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for both reviews. New head: Review 1 (REQUEST_CHANGES at 340cf18)
Review 2 (APPROVE at 340cf18)
PR body: the verification line now names Verification on
CI on |
Resolve the conflict in internal/config/store_postgres.go: main's LeanerCloud#1176 (e43ba7c) moved GetExecutionByID's body into getExecutionByIDOn so it can run on a transaction, and this PR maps SQLSTATE 22P02 to ErrNotFound in the same spot. Keep main's queryExecutionsOn(ctx, q, ...) call and apply the 22P02 mapping after it. On a transaction the lookup only runs after an UPDATE on the same execution_id matched no row, so a non-UUID ID fails that UPDATE first and the mapping never hides an aborted tx. Update main's TestApproveRegistration_EmptyRoleARNRequiresHostAccount (LeanerCloud#1160) to this PR's single ApproveAccountRegistration store call: it mocked TransitionRegistrationStatus/UpdateAccountRegistration and asserted CreateCloudAccount, which approveRegistration no longer calls. The rejected cases now assert ApproveAccountRegistration is never called, and the host-account case asserts it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main ( Conflict:
Semantic conflict (auto-merged but failed):
Verification on
The PR stays a draft. The body still says |
Fixes four of the five handler bugs listed in #1102, one commit each. Item 4 (the email revoke form) is left out because it needs an owner decision; see below. The issue therefore stays open.
Refs #1102
Fixes
1. Unsupported analytics
interval/dimensionreturned 500 (217e4534)Root cause.
GET /api/history/analyticsand/api/history/breakdownpassedintervalanddimensionstraight to the Postgres analytics client. Its allowlist (intervalToTruncUnit,dimensionToColumn) rejected unknown values with a plain error, which the handler wrapped and the router answered with a 500.Fix. The handlers now run the same allowlist functions before any query and answer 400 with the accepted values. The defaults are unchanged (
hourly,service), and so is the case-sensitive matching.2. Registration approval wasn't atomic (
340cf181)Root cause.
approveRegistrationflipped the registration toapproved, then created the cloud account, then linked them, all in separate statements. If the create failed (a duplicate provider + external_id, or any DB error), the registration stayed approved with no account. Approving it again returned 409 and the UI had no way back. This is the same drift migration 000021 had to clean up once already. A failed link was only logged.Fix. A new store method,
ApproveAccountRegistration, runs three steps in one transaction:WHERE status = 'pending'),The status change runs first, so its row lock makes any concurrent approve or reject of the same registration wait for the transaction:
The handler maps a conflict or a duplicate account to 409 and anything else to 500. Credentials are still stored after the commit, best effort, as before.
The interface gains one method. It is implemented in
internal/mocksand in the two hand-written test stores (internal/server,internal/analytics).3.
PUT /api/accounts/{id}silently droppedprovider(bbb5ebae)Root cause. The handler validated the request's
providerand echoed it back, butUpdateCloudAccountnever writes the provider column. The change was dropped while the response said it had been saved.Fix. A provider change is now refused with a 400 that tells the caller to delete and re-add the account. I chose this over persisting the new provider because credentials, service overrides and plan memberships all depend on the provider. Plan membership in particular is only checked against it when an account is added to a plan, so saving a new provider would bypass that check. The check runs after the account-scope check, so a caller who can't see the account still gets a 404. The settings UI always sends the account's own provider (the edit modal is opened from the per-provider list), so it is unaffected. If you'd rather make provider editable, that is a larger change and should be its own issue.
5. Azure revokes returned 500 (
106608d4), now verifiedRoot cause. Confirmed on real Postgres.
POST /api/purchases/{id}/revokeasksGetExecutionByIDwhether the ID is a scheduled execution before looking inpurchase_history, and falls through only onErrNotFound. An Azure purchase ID is an ARM path, andexecution_idis auuidcolumn, so Postgres refused the cast withinvalid input syntax for type uuid ... (SQLSTATE 22P02). Every Azure revoke died with a 500 before reaching the Azure return path. The unit tests never saw it because they mockGetExecutionByIDas returningErrNotFoundfor ARM paths.Fix.
GetExecutionByIDnow maps SQLSTATE 22P02 toErrNotFound: no execution can have an ID that is not a UUID. This makes the store honor the contract the handler and its unit tests already assumed, so no existing fixture changes. Every other DB error still surfaces as a 500 (TestRevokePurchase_GetExecutionByIDDBError_Returns500still passes).Left out: item 4, the email revoke form's POST always gets 401
Confirmed by reading the code; there is no fix here. The revoke email links straight to
GET /api/purchases/revoke/{id}?token=..., which renders a plain HTML form. The form's POST carries noAuthorizationor CSRF header, sorequireSessionBeforeLookupalways answers 401. Approve and cancel avoid this because their emails link to SPA routes (/purchases/approve/...,/purchases/cancel/...) that call the API with the session. The frontend has no revoke deep link.Both possible fixes need a decision from you:
/purchases/revoke/:iddeep link, with the email pointing at it. This is frontend work, which I'm not touching while the UI work is in progress.Verification
Run on macOS against commit
340cf181, and again onffafbd0a(origin/main merged in, plus the 22P02 unit tests below). All tests ran withAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, because this machine's default AWS profile is invalid. Integration tests use a throwaway Postgres container (testcontainers). No cloud API was called: the Azure SDK clients are stubbed through the existingazureRevokeClientFactoryseam.Fail-before / pass-after. Each fix was checked by restoring that commit's production file from its parent and rerunning the new tests.
TestHandler_getHistoryAnalytics_UnsupportedIntervalIs400,TestHandler_getHistoryBreakdown_UnsupportedDimensionIs400..._SupportedIntervalsReachStore,..._SupportedDimensionsReachStoreTestApproveRegistration_FailedAccountCreateLeavesRegistrationPendingapproved,reviewed_atset, no accountpending, rejectableTestApproveRegistration_RejectQueuedBehindApproval(commit and rollback subtests)pg_stat_activitythat the reject is blocked on the lockTestApproveRegistration_ConcurrentApprovalsCreateOneAccount(8 goroutines)TestApproveRegistration_ApprovesAndCreatesInOneStoreCall,..._StoreErrorsMapToResponsesTestUpdateAccount_ProviderChangeIsRejectedProvider: "gcp", no errorTestUpdateAccount_SameProviderIsSavedTestGetExecutionByID_NonUUIDIsNotFoundPostgresinvalid input syntax for type uuid ... (SQLSTATE 22P02)TestRevokePurchase_AzureARMPurchaseIDOnRealPostgresrevoke: GetExecutionByID /providers/...: ... (SQLSTATE 22P02)revoked_atis persistedTestGetExecutionByID_InvalidTextRepresentationIsNotFound,TestGetExecutionByID_OtherPostgresErrorsAreNotNotFound(57P01, 53300, 42P01, 22P03)PostgresStore)ErrNotFound: FAILCommands
Coverage gaps.
CalculateRefund/Returnclients only.Out of scope, found while reading
reg_credential_payloadis never wiped after approval. The handler setreg.RegCredentialPayload = """so we don't keep encrypted credentials longer than needed", but neitherUpdateAccountRegistrationnorTransitionRegistrationStatuswrites that column, so the encrypted payload stays inaccount_registrationsindefinitely. This PR removes the dead in-memory assignment so it no longer claims otherwise, but doesn't clear the column. That deserves its own issue.StoreInterfaceandMockConfigStore, in different hunks. Whichever lands second may need a trivial merge.Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/few,effort/m,type/bug. The issue has notriagedlabel.🤖 Generated with Claude Code