Skip to content

fix(api): backend save-side guard for account service override saves (#107 criterion 3)#240

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/account-override-save-guard
May 3, 2026
Merged

fix(api): backend save-side guard for account service override saves (#107 criterion 3)#240
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/account-override-save-guard

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 3, 2026

Summary

  • Wires checkCommitmentOptionCombo into saveAccountServiceOverride so that a direct PUT /api/accounts/:id/service-overrides/aws/rds with an invalid (term, payment) tuple (e.g. 3yr no-upfront for RDS) returns HTTP 400 instead of persisting silently.
  • Adds derefInt / derefString helpers to bridge the sparse *int/*string override fields to the value-typed config.ServiceConfig shim expected by checkCommitmentOptionCombo.
  • When commitmentOpts is nil or probe data is unavailable (ErrNoData), the guard is permissive — the frontend's hardcoded rules remain the primary gate (same contract as the global-config path).

Completes criterion 3 of #107. See also the tracking issue filed for this criterion (backend save-side guard).

Test plan

  • go test ./internal/api/... -count=1 -run TestSaveAccountServiceOverride — 6 tests pass (existing success + 4 new cases: invalid-combo-400, valid-combo-saves, nil-commitmentOpts-permissive, ErrNoData-permissive)
  • go test ./internal/api/... -count=1 — full api package (1054 tests) passes
  • go vet ./internal/api/... — no issues
  • gofmt -l internal/api/handler_accounts.go internal/api/handler_accounts_test.go — no output (clean)

Summary by CodeRabbit

  • Bug Fixes

    • Added validation to prevent saving invalid provider/service/term/payment combinations; returns clear error and avoids persisting invalid overrides.
  • Tests

    • Added handler tests covering invalid and valid combinations and permissive behavior for missing or no-data validation configs.
  • Chores

    • Enhanced test mocks to support simulating save behavior for account service overrides.

Wire checkCommitmentOptionCombo into saveAccountServiceOverride so that a
direct PUT /api/accounts/:id/service-overrides/aws/rds with an invalid
(term, payment) tuple (e.g. 3yr no-upfront for RDS) returns HTTP 400
instead of persisting silently.

When commitmentOpts is nil or probe data is unavailable (ErrNoData), the
guard is permissive — the frontend's hardcoded rules remain the primary gate.

Adds derefInt/derefString helpers to bridge *int/*string override fields to
the value-typed config.ServiceConfig shim expected by checkCommitmentOptionCombo.

Tests: valid-pass, invalid-reject (400), nil-service permissive, ErrNoData
permissive.

Completes criterion 3 of #107.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f792da40-408a-4f9b-9657-904962d60174

📥 Commits

Reviewing files that changed from the base of the PR and between 76a9edb and 5923468.

📒 Files selected for processing (2)
  • internal/api/handler_accounts_test.go
  • internal/api/mocks_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/api/handler_accounts_test.go

📝 Walkthrough

Walkthrough

Adds a pre-persist validation to saveAccountServiceOverride: constructs a nil-safe config.ServiceConfig from the incoming override and calls h.checkCommitmentOptionCombo(...); if validation fails, the handler returns the error and does not save. Adds derefInt and derefString helpers and test+mock adjustments.

Changes

Validation Logic Addition

Layer / File(s) Summary
Validation Call
internal/api/handler_accounts.go
saveAccountServiceOverride now builds a config.ServiceConfig (using nil-safe derefs) and calls h.checkCommitmentOptionCombo before saving; on error it returns without persisting.
Helpers
internal/api/handler_accounts.go
Adds derefInt(*int) int and derefString(*string) string for nil-safe extraction of optional fields.
Mock Hook
internal/api/mocks_test.go
Adds SaveAccountServiceOverrideFn to MockConfigStore and invokes it when set to allow test verification of the persist path.
Tests
internal/api/handler_accounts_test.go
Adds tests: invalid combo returns 400 and does not save; valid combo saves and returns an ID; permissive behavior when commitmentOpts is nil or returns ErrNoData.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler
    participant CommitmentOpts
    participant ConfigStore

    Client->>Handler: POST saveAccountServiceOverride(override)
    Handler->>CommitmentOpts: Build ServiceConfig and call checkCommitmentOptionCombo
    alt validation fails
        CommitmentOpts-->>Handler: error (validation failure)
        Handler-->>Client: 400 ClientError (do not call ConfigStore)
    else validation succeeds or permissive
        CommitmentOpts-->>Handler: ok
        Handler->>ConfigStore: SaveAccountServiceOverride(override)
        ConfigStore-->>Handler: success (ID)
        Handler-->>Client: 200 OK (override ID)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

Suggested labels

urgency/this-sprint, impact/few, effort/m, type/bug

Poem

🐰 I hop through handlers, checking each combo,

Derefing gently where pointers may go,
If terms or payments are out of line,
I stop the save — keep data fine.
Tests cheer as mocks verify the flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: adding backend validation (guard) for account service override saves before persistence. It aligns with the core functionality in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/account-override-save-guard

Review rate limit: 0/5 reviews remaining, refill in 49 minutes and 59 seconds.

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

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users effort/xs Trivial / one-liner type/security Security finding triaged Item has been triaged labels May 3, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/api/handler_accounts_test.go (1)

418-448: ⚡ Quick win

Assert that invalid combos are not persisted.

Nice 400 assertion; add a “save not called” assertion to lock down the core guard contract during regressions.

Suggested test hardening
 func TestSaveAccountServiceOverride_InvalidCombo_Returns400(t *testing.T) {
 	ctx := context.Background()
 	mockAuth := new(MockAuthService)
 	setupAdminAuth(ctx, mockAuth)

 	store := setupAdminMock(ctx)
+	saveCalled := false
+	store.SaveAccountServiceOverrideFn = func(_ context.Context, _ *config.AccountServiceOverride) error {
+		saveCalled = true
+		return nil
+	}
 	handler := &Handler{
 		auth:   mockAuth,
 		config: store,
 		commitmentOpts: &stubCommitmentOpts{
 			validateFn: func(_ context.Context, provider, service string, term int, payment string) (bool, error) {
@@
 	ce, ok := IsClientError(err)
 	require.True(t, ok, "expected ClientError, got %T: %v", err, err)
 	assert.Equal(t, 400, ce.code)
 	assert.Contains(t, ce.message, "3yr no-upfront")
+	assert.False(t, saveCalled, "override must not be persisted when combo is invalid")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/handler_accounts_test.go` around lines 418 - 448, The test
TestSaveAccountServiceOverride_InvalidCombo_Returns400 should also assert that
no persistence was attempted: extend the mock config/store returned by
setupAdminMock (or add an expectation on the mock used as Handler.config) to
verify the save method for service overrides (the mock method backing
saveAccountServiceOverride in the store) was not called when
handler.saveAccountServiceOverride returns the 400 ClientError; add an
assertion/expectation on that mock (e.g., call count == 0 or ExpectNotCalled on
the store's SaveAccountServiceOverride/SaveOverride method) immediately after
the error assertions to lock down that invalid combos are not persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/api/handler_accounts_test.go`:
- Around line 418-448: The test
TestSaveAccountServiceOverride_InvalidCombo_Returns400 should also assert that
no persistence was attempted: extend the mock config/store returned by
setupAdminMock (or add an expectation on the mock used as Handler.config) to
verify the save method for service overrides (the mock method backing
saveAccountServiceOverride in the store) was not called when
handler.saveAccountServiceOverride returns the 400 ClientError; add an
assertion/expectation on that mock (e.g., call count == 0 or ExpectNotCalled on
the store's SaveAccountServiceOverride/SaveOverride method) immediately after
the error assertions to lock down that invalid combos are not persisted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd42da54-2c3d-4268-8bb6-d066a11c13f6

📥 Commits

Reviewing files that changed from the base of the PR and between c8a78cb and 76a9edb.

📒 Files selected for processing (2)
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 3, 2026

Thanks for the review @coderabbitai.

Docstring coverage warning: The 50% docstring coverage is a pre-existing project-wide baseline — not introduced by this PR. Both new helper functions (derefInt, derefString) have Go-style doc comments. Addressing the broader project-wide coverage gap is out of scope for this surgical fix (criterion 3 of #107).

No other actionable items found. The change is minimal and complete.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim, understood — thanks for the context! I've noted that the ~50% docstring coverage is a pre-existing project-wide baseline in this repository and not something introduced by individual PRs. I'll avoid flagging it as an actionable item in future reviews. Triggering the requested review now.

[review]


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/240

Timestamp: 2026-05-03T16:07:51.722Z
Learning: In the LeanerCloud/CUDly repository, approximately 50% docstring coverage is a pre-existing project-wide baseline. It should not be flagged as an actionable issue in individual PR reviews, as addressing it is out of scope for focused/surgical fixes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…ick)

Add SaveAccountServiceOverrideFn hook to MockConfigStore (mirrors the
SetPlanAccountsFn pattern already in the file) so tests can intercept and
assert the persist path.

Wire it in TestSaveAccountServiceOverride_InvalidCombo_Returns400 to confirm
the override is never written to the store when checkCommitmentOptionCombo
returns a 400 error.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 3, 2026

CR pass 1 triage:

  • Nitpick (handler_accounts_test.go:418-448 — "assert save not called"): Fixed. Added SaveAccountServiceOverrideFn hook to MockConfigStore (same pattern as SetPlanAccountsFn) and wired it in TestSaveAccountServiceOverride_InvalidCombo_Returns400 to assert saveCalled == false when the guard fires. Commit 5923468.

  • Pre-merge warning (Docstring Coverage 50%): Dismissed — pre-existing project-wide gap, both new helper functions already have Go doc comments. Out of scope for this surgical fix.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim, thanks for the clear triage and the prompt fix!

  • SaveAccountServiceOverrideFn hook (commit 5923468): Good — adding the hook to MockConfigStore following the SetPlanAccountsFn pattern and asserting saveCalled == false in TestSaveAccountServiceOverride_InvalidCombo_Returns400 properly closes the gap. The guard is now verifiably preventing the persistence path when the combo is invalid.

  • Docstring coverage: Noted, nothing to re-address there.

Triggering the follow-up review now.

[review]


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/240

Timestamp: 2026-05-03T16:11:08.309Z
Learning: In the LeanerCloud/CUDly repository, `MockConfigStore` uses function-hook fields (e.g., `SetPlanAccountsFn`, `SaveAccountServiceOverrideFn`) to allow individual tests to wire in spy/stub behaviour. When adding new store methods to the mock, follow this same hook-field pattern rather than embedding logic directly in the mock struct.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim merged commit dc612c8 into feat/multicloud-web-frontend May 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/security Security finding urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant