Skip to content

refactor(azure): build reservation purchase bodies from SDK enum constants - #448

Merged
cristim merged 1 commit into
mainfrom
chore/22-azure-purchase-sdk-enums
Oct 16, 2026
Merged

cristim merged 1 commit into
mainfrom
chore/22-azure-purchase-sdk-enums

Conversation

@cristim

@cristim cristim commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

The seven Azure reservation clients (cache, compute, cosmosdb, database, managedredis, search, synapse) built their calculatePrice / purchase bodies from string literals: "appliedScopeType": "Shared" and fmt.Sprintf("P%dY", termYears).

  • They now send string(armreservations.AppliedScopeTypeShared).
  • They send string(term), where term comes from the new reservations.ReservationTermForYears(termYears) in providers/azure/services/internal/reservations/term.go. It is an explicit switch: 1 maps to ReservationTermP1Y, 3 maps to ReservationTermP3Y, and any other value is an error. It never formats a string.
  • ParseTermYears still runs first and only yields 1 or 3, so no previously accepted input changes.
  • The stale comment in internal/recommendations/converter.go:48, which said the clients hardcode "Shared", now names the SDK constant.

Closes #22. Closes #14, the appliedScopeType SDK-constant part.

Not touched: internal/reservations/{idempotencymarker,purchase,strict}.go, which open PRs #422, #423 and #439 own.

Verification (macOS, Go 1.26.9)

Byte identity. One table test, providers/azure/services/purchasebody_test.go (package services_test), captures each service's request body through a fake HTTP client (internal/reservations/reservationstest). It compares the body against literal golden JSON for 1yr and 3yr, after stripping only the timestamped displayName. The same test passes unchanged with the original HEAD client.go files swapped in, so the wire format is identical.

Unsupported term. The 5yr case pins the early rejection by ParseTermYears, with zero requests sent. term_test.go covers the helper, including rejecting 0 and 5.

Mutation probes. The independent reviewer ran these in a scratch copy, and each made TestPurchaseBodies fail:

  • each client sending AppliedScopeTypeSingle, including search with its import kept;
  • mapping 3 years to P1Y.

Checks.

  • (cd providers/azure && GOWORK=off go test -race -short ./...) passes.
  • make build and make vet pass.
  • golangci-lint v2.10.1 (the CI pin) reports 0 issues.
  • gocyclo -over 10 is clean.

Review. The same non-author reviewer did two passes. Its pass-1 finding was that the 5-year branches were unreachable, so they were trimmed. No actionable findings on tree 986aacda.

Fake HTTP transport only; no Azure call or purchase.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Azure reservation purchases now use the supported one-year and three-year terms consistently across cache, compute, database, Cosmos DB, managed Redis, search, and Synapse services.
    • Unsupported reservation terms, such as five years, now return an error before a purchase request is sent.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

  • Run on-demand review

This review includes 12 billable files and costs up to $3.00.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 39 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 65 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: c084b50e-71fe-44f5-b683-b843544d1950
📥 Commits

Reviewing files that changed from the base of the PR and between 74b3ce1 and bc23972.

📒 Files selected for processing: 12
  • providers/azure/internal/recommendations/converter.go
  • providers/azure/services/cache/client.go
  • providers/azure/services/compute/client.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/database/client.go
  • providers/azure/services/internal/reservations/reservationstest/capture.go
  • providers/azure/services/internal/reservations/term.go
  • providers/azure/services/internal/reservations/term_test.go
  • providers/azure/services/managedredis/client.go
  • providers/azure/services/purchasebody_test.go
  • providers/azure/services/search/client.go
  • providers/azure/services/synapse/client.go
📝 Walkthrough

Azure reservation purchase clients now validate term years through a shared SDK-enum mapping and use SDK enum values for purchase terms and shared scope. Tests check request bodies for supported terms and verify that unsupported terms fail without making captured requests.

Changes: Azure reservation purchases

Layer / File(s) Summary
Reservation term mapping
providers/azure/services/internal/reservations/term.go, providers/azure/services/internal/reservations/term_test.go
ReservationTermForYears maps 1 and 3 years to SDK term enums and returns errors for other values. Unit tests cover supported and unsupported year counts.
Purchase request enum values
providers/azure/services/{cache,compute,cosmosdb,database,managedredis,search,synapse}/client.go, providers/azure/internal/recommendations/converter.go
Seven service clients validate term years before building purchase requests. They use SDK enum values for term and appliedScopeType. The recommendation comment identifies the SDK shared-scope enum.
Purchase body tests and capture transport
providers/azure/services/internal/reservations/reservationstest/capture.go, providers/azure/services/purchasebody_test.go
The test transport captures purchase request bodies and counts calls. Tests check 1-year and 3-year bodies and verify that unsupported 5-year terms return errors without captured calls.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 74b3c

No actionable issue is established for the current purchase workflow. The change is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 clearly and concisely describes the main change: using Azure SDK enum constants to build reservation purchase bodies.
Linked Issues check Passed The changes satisfy #22 and #14. All seven listed clients now use string(armreservations.AppliedScopeTypeShared). They now use reservations.ReservationTermForYears instead of formatted term strings. T…
Out of Scope Changes check Passed The changed comment, shared term helper, fake HTTP capture transport, and regression tests directly support #22 and #14. The seven client changes are limited to the linked enum conversions and early e…
Full details: Linked Issues check

Explanation

The changes satisfy #22 and #14. All seven listed clients now use string(armreservations.AppliedScopeTypeShared). They now use reservations.ReservationTermForYears instead of formatted term strings. The helper returns ReservationTermP1Y and ReservationTermP3Y and returns an error for other values. Tests cover both supported terms, unsupported input, zero requests on early rejection, and the expected request bodies for all seven clients. The exact wire values remain Shared, P1Y, and P3Y.

Full details: Out of Scope Changes check

Explanation

The changed comment, shared term helper, fake HTTP capture transport, and regression tests directly support #22 and #14. The seven client changes are limited to the linked enum conversions and early error handling. No unrelated service or reservation workflow changes are shown.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim cristim added effort/s Hours impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter labels Oct 16, 2026
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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 added the bug Something isn't working label Oct 16, 2026
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Independent review verdict: CLEAN at 4780538

Reviewer: fresh Claude agent (Sonnet, high effort), not the author. Coordinator: claude-cc-reviewer.

Equivalence check: the seven client.go files from origin/main were swapped in, and every calculatePrice body was captured via reservationstest.Capture for 12 term strings x 7 payment options per service (588 cases). Bodies are byte-identical to the PR head once the displayName timestamp/hash token is normalized. Error messages and call counts are identical too.

Service Result
cache, compute, cosmosdb, database, managedredis, search, synapse identical

Compared fields: reservedResourceType, billingScopeId, term, billingPlan, quantity, appliedScopeType, sku.name, location, renew, tags, displayName.

  • Term mapping:
    • "", "1", "1yr" and "1y" map to P1Y.
    • " 3YR ", "3", "3y" and "3yr" map to P3Y.
    • "5yr", "0", "P1Y" and "1 Year" error out.
    • ReservationTermForYears (term.go) is an explicit switch that errors on anything else, so nothing silently defaults.
  • SDK constants match (module cache constants.go): AppliedScopeTypeShared = "Shared" (:21), ReservationTermP1Y/P3Y (:476-477), and billing plans Monthly/Upfront (:429-430).
  • go vet ./... is clean, and go test -race -count=1 ./... in providers/azure gives 13 packages ok.
  • Mutation probes: mapping 3yr to P1Y, changing Monthly to Upfront, and a hardcoded P1Y in synapse are each killed by TestPurchaseBodies/TestReservationTermForYears.

Non-blocking:

  • The "unsupported" branch in ReservationTermForYears is unreachable through ParseTermYears; it is defense in depth.
  • purchasebody_test.go covers only no-upfront 1yr/3yr. The upfront and term-alias variants were verified only by the reviewer's matrix.
  • Out of scope: services/savingsplans/benefit_recommendations.go:103 still uses fmt.Sprintf("P%dY", years).

Gate:

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

CI is green on head 4780538a: pre-commit 38930439656 and Build & Test 38930439594.

@cristim
cristim force-pushed the chore/22-azure-purchase-sdk-enums branch from 4780538 to 74b3ce1 Compare October 16, 2026 16:36
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (BEHIND). The range-diff is identical, so the CLEAN verdict carries over to 4780538. (claude-cc-reviewer)

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Correction: the rebased head is 74b3ce1; my earlier note read the head before GitHub finished updating. git range-diff: 1: 4780538 = 1: 74b3ce1, so the commit is identical and the CLEAN verdict carries over. (claude-cc-reviewer)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔇 Additional comments: 12
providers/azure/services/purchasebody_test.go: 1

59-70: Pin the loop variables for closure safety.

The subtests capture svc, term, and want. The go.mod declares Go 1.26.9. Per-iteration loop variable semantics apply, so the code is correct. No change is needed.

providers/azure/services/internal/reservations/term.go: 1

9-21: LGTM!

providers/azure/services/cache/client.go: 1

324-328: LGTM!

Also applies to: 344-344, 355-355

providers/azure/services/compute/client.go: 1

416-419: LGTM!

Also applies to: 427-427, 438-438

providers/azure/services/cosmosdb/client.go: 1

325-329: LGTM!

Also applies to: 345-345, 356-356

providers/azure/services/database/client.go: 1

357-361: LGTM!

Also applies to: 377-377, 388-388

providers/azure/services/managedredis/client.go: 1

306-310: LGTM!

Also applies to: 326-326, 329-329

providers/azure/services/search/client.go: 1

17-17: LGTM!

Also applies to: 245-249, 270-270, 281-281

providers/azure/services/synapse/client.go: 1

315-319: LGTM!

Also applies to: 335-335, 338-338

providers/azure/internal/recommendations/converter.go: 1

48-48: LGTM!

providers/azure/services/internal/reservations/term_test.go: 1

10-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Convert this test to a table-driven test.

The test uses two loops with different case shapes. A single table with years, want, and wantErr fields is clearer. Run each case with t.Run. The failure output then names the exact case.

♻️ Proposed refactor
 func TestReservationTermForYears(t *testing.T) {
-	for years, want := range map[int]string{1: "P1Y", 3: "P3Y"} {
-		got, err := ReservationTermForYears(years)
-		require.NoError(t, err)
-		assert.Equal(t, want, string(got))
-	}
-	for _, years := range []int{0, 5} {
-		_, err := ReservationTermForYears(years)
-		assert.Error(t, err, "years=%d", years)
-	}
+	tests := []struct {
+		name    string
+		years   int
+		want    string
+		wantErr bool
+	}{
+		{"one year", 1, "P1Y", false},
+		{"three years", 3, "P3Y", false},
+		{"zero", 0, "", true},
+		{"five years", 5, "", true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ReservationTermForYears(tt.years)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, string(got))
+		})
+	}
 }

Based on learnings: prefer table-driven tests for multiple input/output scenarios of the same function.

Source: Learnings

providers/azure/services/internal/reservations/reservationstest/capture.go: 1

49-58: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Lock mu when reading BodyBytes.

Do writes BodyBytes under mu. BodyWithoutDisplayName reads it without the lock. A concurrent call is a data race. The current tests call it sequentially, so no failure occurs today. Acquire the lock for the read to keep the helper safe for reuse.

🔒️ Proposed fix
 func (c *Capture) BodyWithoutDisplayName() ([]byte, error) {
+	c.mu.Lock()
+	raw := c.BodyBytes
+	c.mu.Unlock()
 	var body map[string]any
-	if err := json.Unmarshal(c.BodyBytes, &body); err != nil {
+	if err := json.Unmarshal(raw, &body); err != nil {

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 2ae819c4-abbd-4028-8f9f-49e018fca280
📥 Commits

Reviewing files that changed from the base of the PR and between b69a17e and 74b3ce1.

📒 Files selected for processing: 12
  • providers/azure/internal/recommendations/converter.go
  • providers/azure/services/cache/client.go
  • providers/azure/services/compute/client.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/database/client.go
  • providers/azure/services/internal/reservations/reservationstest/capture.go
  • providers/azure/services/internal/reservations/term.go
  • providers/azure/services/internal/reservations/term_test.go
  • providers/azure/services/managedredis/client.go
  • providers/azure/services/purchasebody_test.go
  • providers/azure/services/search/client.go
  • providers/azure/services/synapse/client.go

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

…tants

The seven Azure reservation clients wrote appliedScopeType and term as
string literals ("Shared", fmt.Sprintf("P%dY", years)). They now use
armreservations.AppliedScopeTypeShared and ReservationTermP1Y/P3Y through
one reservations.ReservationTermForYears helper, which errors on any other
term. The request bodies are byte-identical: one table test captures
every service's calculatePrice body and passes unchanged against the old
clients.

Closes #22
Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim
cristim force-pushed the chore/22-azure-purchase-sdk-enums branch from 74b3ce1 to bc23972 Compare October 16, 2026 16:57
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Pure rebase onto main after go#449 merged. git range-diff of reviewed 74b3ce1 against new head bc23972 shows the commit as identical (=), so the CLEAN verdict carries to bc23972. Merge after CI is green. (claude-cc-reviewer)

@cristim
cristim merged commit a45b889 into main Oct 16, 2026
15 checks passed
@cristim
cristim deleted the chore/22-azure-purchase-sdk-enums branch October 16, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working effort/s Hours impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

1 participant