From 11086030758c703cf4cc5700520505277797dc7b Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sun, 18 Oct 2026 13:15:56 +0200 Subject: [PATCH] fix(cli): adopt shared archera amount validation Reject invalid selected amounts and preserve accepted long decimal values using the existing formatter. Keep all provider module pins unchanged. --- cmd/archera_pkg_adoption_test.go | 101 +++++++++++++++++++++++++++++++ cmd/archera_render.go | 2 +- go.mod | 2 +- go.sum | 4 +- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 cmd/archera_pkg_adoption_test.go diff --git a/cmd/archera_pkg_adoption_test.go b/cmd/archera_pkg_adoption_test.go new file mode 100644 index 000000000..ac091297e --- /dev/null +++ b/cmd/archera_pkg_adoption_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/insurance" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestArcheraComparisonSelectedAmountContract(t *testing.T) { + for row, candidates := range []int{2, 1} { + for candidate := -1; candidate < candidates; candidate++ { + for _, variant := range []string{"missing", "null", "negative", "zero"} { + t.Run(fmt.Sprintf("row%d/offer%d/%s", row, candidate, variant), func(t *testing.T) { + var body map[string]any + decoder := json.NewDecoder(strings.NewReader(archeraComparisonFixture())) + decoder.UseNumber() + require.NoError(t, decoder.Decode(&body)) + line := body["data"].([]any)[row].(map[string]any) + offer := line["current"].(map[string]any) + if candidate >= 0 { + offer = line["candidates"].([]any)[candidate].(map[string]any) + } + switch variant { + case "missing": + delete(offer, "selected_amount") + case "null": + offer["selected_amount"] = nil + case "negative": + offer["selected_amount"] = json.Number("-1") + case "zero": + offer["selected_amount"] = json.Number("0") + } + wire, err := json.Marshal(body) + require.NoError(t, err) + rec := setupArchera(t, func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(wire) }) + out, stderr, err := runArchera(t, fullEnv(), "--format", "json") + if variant == "zero" { + require.NoError(t, err) + assert.NotEmpty(t, out) + } else { + require.ErrorContains(t, err, "selected_amount") + assert.Empty(t, out) + assert.NotContains(t, stderr+err.Error(), archeraTestKey) + } + require.Len(t, rec.requests, 1) + }) + } + } + } +} + +func TestArcheraComparisonAcceptedWireDecimalPrecision(t *testing.T) { + cases := []struct{ name, token, want string }{ + {"101 places", "0.1e-100", "0." + strings.Repeat("0", 100) + "1"}, + {"64 byte token", "0." + strings.Repeat("0", 56) + "1e-100", "0." + strings.Repeat("0", 156) + "1"}, + {"65 byte token rejected", "0." + strings.Repeat("0", 57) + "1e-100", ""}, + {"exponent rejected", "1e-101", ""}, + } + require.Len(t, cases[1].token, 64) + require.Len(t, cases[2].token, 65) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + wire := strings.Replace(archeraComparisonFixture(), `"total":101`, `"total":`+tc.token, 1) + require.NotEqual(t, archeraComparisonFixture(), wire) + rec := setupArchera(t, func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(wire)) }) + client, err := insurance.NewClient(insurance.Config{APIKey: insurance.NewSecret(archeraTestKey), OrgID: archeraTestOrg}, archeraHTTPClient) + require.NoError(t, err) + comparison, err := client.Comparison(context.Background(), insurance.ComparisonRequest{PlanID: archeraTestPlan}) + if tc.want == "" { + require.Error(t, err) + assert.Nil(t, comparison) + } else { + require.NoError(t, err, "the real client must accept the wire token before formatting is tested") + require.NotNil(t, comparison) + got := archeraMoney(comparison.Current.Monthly.CommitmentCostTotal) + require.NotNil(t, got) + assert.Equal(t, tc.want, *got) + } + out, stderr, err := runArchera(t, fullEnv(), "--format", "json") + if tc.want == "" { + require.Error(t, err) + assert.Empty(t, out) + assert.NotContains(t, stderr+err.Error(), archeraTestKey) + } else { + require.NoError(t, err) + var dto archeraComparisonDTO + require.NoError(t, json.Unmarshal([]byte(out), &dto)) + require.NotNil(t, dto.Current.Monthly.CommitmentCostTotal) + assert.Equal(t, tc.want, *dto.Current.Monthly.CommitmentCostTotal) + } + assert.Len(t, rec.requests, 2) + }) + } +} diff --git a/cmd/archera_render.go b/cmd/archera_render.go index 7aa093d3c..ae19dfd79 100644 --- a/cmd/archera_render.go +++ b/cmd/archera_render.go @@ -24,7 +24,7 @@ const ( archeraPremiumNote = "Totals include the Archera premium. The API reports no currency, so amounts are unlabelled." archeraMaxFieldSize = 256 archeraMaxErrorSize = 512 - archeraMaxMoneyPrec = 100 + archeraMaxMoneyPrec = 164 ) // archeraSanitize strips control characters (ESC, C1 and the rest of diff --git a/go.mod b/go.mod index 3d08a3539..7fb6388f5 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013212804-9b35837e78d1 + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016175124-570d168c6bc7 github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261013212804-9b35837e78d1 github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261013212804-9b35837e78d1 github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261013212804-9b35837e78d1 diff --git a/go.sum b/go.sum index 0047803b7..9ae81a29c 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013212804-9b35837e78d1 h1:i4up20HOy0TFHwnryHWSUzfE0t4EgyYIG6zvosCdssg= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013212804-9b35837e78d1/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016175124-570d168c6bc7 h1:mAvqQSMkTuw4P8m8o+/wdyoxGE9L6eiEzZFanGXqYcI= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016175124-570d168c6bc7/go.mod h1:SCDEgHG6evjPh0RsoXof+J3aMmGXNFMrwbJIle7rMpQ= github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261013212804-9b35837e78d1 h1:c4JuGObXDFP+oqC7CD+0Sn9ljzlUciVikW7iMDCNNa4= github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261013212804-9b35837e78d1/go.mod h1:no7i/7i+W37+qhEOVU8F/0dP5FhO3wtzvKIN0+tvL3A= github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261013212804-9b35837e78d1 h1:aYYu7JDJQcoriUKg/m+IgTJruc2ugfBLXafAC2LQy00=