diff --git a/CHANGELOG.md b/CHANGELOG.md index 483168ea..9cb5536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Added +- **Azure `NewPartialSubscriptionFailure` constructor:** builds a + `PartialSubscriptionFailureError`, optionally with skip reasons readable + through `Skips()`, so code outside the module can construct one, for example + in tests. - **Azure currency refusals wrap `common.ErrCurrencyNotAccepted`:** reservation purchase, savings plan purchase and reservation exchange refusals can now be detected with `errors.Is`; each existing message gains a diff --git a/providers/azure/recommendations_multi_subscription.go b/providers/azure/recommendations_multi_subscription.go index 2c0f5939..074748b1 100644 --- a/providers/azure/recommendations_multi_subscription.go +++ b/providers/azure/recommendations_multi_subscription.go @@ -445,6 +445,19 @@ func newPartialSubscriptionFailure(attempted int, failed []SubscriptionFailure, return e } +// NewPartialSubscriptionFailure builds a PartialSubscriptionFailureError for +// callers that need to construct one, for example in tests of code that +// handles it. attempted is how many subscriptions the sweep covered and failed +// lists those that errored; Succeeded is derived as attempted minus +// len(failed). skipReasons are the recommendations that reachable subscriptions +// dropped: empty or nil leaves Skips() nil, otherwise Skips() reports them. The +// slice is copied. Reasons are used as given, as final strings: the collection +// path prefixes each with ": ", so callers wanting that shape +// must add it. attempted should be >= len(failed). +func NewPartialSubscriptionFailure(attempted int, failed []SubscriptionFailure, skipReasons []string) *PartialSubscriptionFailureError { + return newPartialSubscriptionFailure(attempted, failed, append([]string(nil), skipReasons...)) +} + // GetRecommendationsForService retrieves recommendations for a single // service across every subscription. func (m *MultiSubscriptionRecommendationsClient) GetRecommendationsForService(ctx context.Context, service common.ServiceType) ([]common.Recommendation, error) { diff --git a/providers/azure/recommendations_partial_failure_ctor_test.go b/providers/azure/recommendations_partial_failure_ctor_test.go new file mode 100644 index 00000000..61afb2e2 --- /dev/null +++ b/providers/azure/recommendations_partial_failure_ctor_test.go @@ -0,0 +1,52 @@ +package azure_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/providers/azure" +) + +func TestNewPartialSubscriptionFailure(t *testing.T) { + cause := errors.New("boom") + failed := []azure.SubscriptionFailure{{SubscriptionID: "sub-b", Err: cause}} + + t.Run("without skips", func(t *testing.T) { + for _, reasons := range [][]string{nil, {}} { + err := fmt.Errorf("wrapped: %w", azure.NewPartialSubscriptionFailure(3, failed, reasons)) + got := azure.AsPartialSubscriptionFailure(err) + require.NotNil(t, got) + assert.Nil(t, got.Skips()) + assert.Equal(t, 3, got.Attempted) + assert.Equal(t, 2, got.Succeeded) + assert.Equal(t, failed, got.Failed) + assert.ErrorIs(t, err, cause) + } + }) + + t.Run("with skips", func(t *testing.T) { + reasons := []string{"a: skipped", "b: skipped"} + e := azure.NewPartialSubscriptionFailure(3, failed, reasons) + got := azure.AsPartialSubscriptionFailure(e) + require.NotNil(t, got) + skips := got.Skips() + require.NotNil(t, skips) + assert.Equal(t, 2, skips.SkippedCount()) + assert.Equal(t, []string{"a: skipped", "b: skipped"}, skips.SkipReasons()) + + reasons[0] = "mutated" + assert.Equal(t, "a: skipped", got.Skips().SkipReasons()[0], "input slice must be copied") + got.Skips().SkipReasons()[1] = "mutated" + assert.Equal(t, "b: skipped", got.Skips().SkipReasons()[1], "returned slice must be a copy") + }) + + t.Run("not a partial result", func(t *testing.T) { + e := azure.NewPartialSubscriptionFailure(3, failed, []string{"x"}) + var p interface{ Partial() bool } + assert.False(t, errors.As(e, &p)) + }) +}