| package e2e |
|
|
| import ( |
| "net/http" |
| "testing" |
|
|
| "github.com/samber/lo" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
|
|
| v3sdk "github.com/openmeterio/openmeter/api/v3/client" |
| ) |
|
|
| func TestV3Addon(t *testing.T) { |
| c := newV3Client(t) |
|
|
| addonBody := validAddonRequest("test_v3_addon") |
| addonKey := addonBody.Key |
|
|
| var addonID string |
|
|
| t.Run("Should create an addon in draft status", func(t *testing.T) { |
| addon, err := c.Addons.Create(t.Context(), addonBody) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, addon) |
|
|
| assert.Equal(t, addonKey, addon.Key) |
| assert.EqualValues(t, 1, addon.Version) |
| assert.Equal(t, v3sdk.AddonStatusDraft, addon.Status) |
| assert.Nil(t, addon.EffectiveFrom) |
|
|
| addonID = addon.ID |
| }) |
|
|
| t.Run("Should get the addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| addon, err := c.Addons.Get(t.Context(), addonID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, addon) |
|
|
| assert.Equal(t, addonID, addon.ID) |
| assert.Equal(t, v3sdk.AddonStatusDraft, addon.Status) |
| }) |
|
|
| t.Run("Should list addons and find the created addon", func(t *testing.T) { |
| |
| |
| page, err := c.Addons.List(t.Context(), v3sdk.AddonListParams{ |
| Page: &v3sdk.PageParams{Size: lo.ToPtr(1000)}, |
| }) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, page) |
|
|
| found := false |
| for _, a := range page.Data { |
| if a.ID == addonID { |
| found = true |
| assert.NotEmpty(t, a.Currency) |
| assert.NotEmpty(t, a.Status) |
| assert.NotEmpty(t, a.RateCards) |
| break |
| } |
| } |
| assert.True(t, found, "created addon not found in list") |
| }) |
|
|
| t.Run("Should update the addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| updateBody := v3sdk.UpsertAddonRequest{ |
| Name: "Test V3 Addon Updated", |
| InstanceType: addonBody.InstanceType, |
| RateCards: addonBody.RateCards, |
| } |
|
|
| addon, err := c.Addons.Update(t.Context(), addonID, updateBody) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, addon) |
|
|
| assert.Equal(t, "Test V3 Addon Updated", addon.Name) |
| assert.Equal(t, v3sdk.AddonStatusDraft, addon.Status) |
| }) |
|
|
| t.Run("Should publish the addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| addon, err := c.Addons.Publish(t.Context(), addonID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, addon) |
|
|
| assert.Equal(t, v3sdk.AddonStatusActive, addon.Status) |
| assert.NotNil(t, addon.EffectiveFrom) |
| }) |
|
|
| t.Run("Should not allow deleting an active addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| err := c.Addons.Delete(t.Context(), addonID) |
| requireProblem(t, err, http.StatusBadRequest) |
| }) |
|
|
| t.Run("Should archive the published addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| addon, err := c.Addons.Archive(t.Context(), addonID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, addon) |
|
|
| assert.Equal(t, v3sdk.AddonStatusArchived, addon.Status) |
| assert.NotNil(t, addon.EffectiveTo) |
| }) |
|
|
| t.Run("Should delete an archived addon", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| err := c.Addons.Delete(t.Context(), addonID) |
| c.requireStatus(http.StatusNoContent, err) |
| }) |
|
|
| t.Run("Should return deleted_at after deletion", func(t *testing.T) { |
| require.NotEmpty(t, addonID) |
|
|
| addon, err := c.Addons.Get(t.Context(), addonID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, addon) |
|
|
| assert.NotNil(t, addon.DeletedAt) |
| }) |
| } |
|
|
| |
| |
| |
| func TestV3AddonMixedRateCardRoundTrip(t *testing.T) { |
| c := newV3Client(t) |
|
|
| eventTypes := []string{ |
| uniqueKey("sanity_event"), |
| uniqueKey("sanity_event"), |
| } |
|
|
| meterKeys := []string{ |
| uniqueKey("sanity_meter"), |
| uniqueKey("sanity_meter"), |
| } |
|
|
| meters := make([]v3sdk.Meter, 0, len(meterKeys)) |
|
|
| for i := range meterKeys { |
| valueProperty := "$.value" |
|
|
| m, err := c.Meters.Create(t.Context(), v3sdk.CreateMeterRequest{ |
| Key: meterKeys[i], |
| Name: "Test Meter " + meterKeys[i], |
| Aggregation: v3sdk.MeterAggregationSum, |
| EventType: eventTypes[i], |
| ValueProperty: &valueProperty, |
| }) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, m) |
| require.NotEmpty(t, m.ID) |
|
|
| meters = append(meters, *m) |
| } |
|
|
| featureKeys := []string{ |
| uniqueKey("mix_unit"), |
| uniqueKey("mix_graduated"), |
| } |
|
|
| features := make([]v3sdk.Feature, 0, len(featureKeys)) |
|
|
| for i := range featureKeys { |
| f, err := c.Features.Create(t.Context(), v3sdk.CreateFeatureRequest{ |
| Key: featureKeys[i], |
| Name: "Test Feature " + featureKeys[i], |
| Meter: &v3sdk.FeatureMeterReferenceInput{ |
| ID: meters[i].ID, |
| }, |
| }) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, f) |
| require.NotEmpty(t, f.ID) |
|
|
| features = append(features, *f) |
| } |
|
|
| flat := validFlatRateCard("mix_flat") |
| unit := validUnitRateCard(features[0]) |
| percent := float64(10) |
| unit.Discounts = &v3sdk.RateCardDiscounts{Percentage: &percent} |
| graduated := validGraduatedRateCard(features[1]) |
|
|
| body := validAddonRequest("mixed_rc") |
| body.RateCards = []v3sdk.RateCardInput{flat, unit, graduated} |
|
|
| addon, err := c.Addons.Create(t.Context(), body) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, addon) |
|
|
| published, err := c.Addons.Publish(t.Context(), addon.ID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, published) |
|
|
| got, err := c.Addons.Get(t.Context(), addon.ID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, got) |
| require.Len(t, got.RateCards, 3) |
|
|
| byKey := map[string]v3sdk.RateCard{} |
| for _, rc := range got.RateCards { |
| byKey[rc.Key] = rc |
| } |
|
|
| gotFlat, ok := byKey[flat.Key] |
| require.True(t, ok, "flat rate card missing on round-trip") |
| flatPrice, err := gotFlat.Price.AsPriceFlat() |
| require.NoError(t, err, "flat price should decode as BillingPriceFlat") |
| assert.Equal(t, "10", flatPrice.Amount) |
|
|
| gotUnit, ok := byKey[unit.Key] |
| require.True(t, ok, "unit rate card missing on round-trip") |
| unitPrice, err := gotUnit.Price.AsPriceUnit() |
| require.NoError(t, err, "unit price should decode as BillingPriceUnit") |
| |
| assert.Equal(t, "0.1", unitPrice.Amount) |
| require.NotNil(t, gotUnit.Discounts, "percentage discount missing on round-trip") |
| require.NotNil(t, gotUnit.Discounts.Percentage) |
| assert.InDelta(t, 10.0, float64(*gotUnit.Discounts.Percentage), 0.001) |
|
|
| gotGraduated, ok := byKey[graduated.Key] |
| require.True(t, ok, "graduated rate card missing on round-trip") |
| gradPrice, err := gotGraduated.Price.AsPriceGraduated() |
| require.NoError(t, err, "graduated price should decode as BillingPriceGraduated") |
| assert.Len(t, gradPrice.Tiers, 2, "graduated tiers not preserved") |
| } |
|
|
| |
| |
| func TestV3AddonVersioningAndAutoArchive(t *testing.T) { |
| c := newV3Client(t) |
|
|
| createBody := validAddonRequest("addon_versioning") |
| sharedKey := createBody.Key |
|
|
| v1, err := c.Addons.Create(t.Context(), createBody) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, v1) |
| assert.EqualValues(t, 1, v1.Version) |
|
|
| v1Active, err := c.Addons.Publish(t.Context(), v1.ID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, v1Active) |
| require.Equal(t, v3sdk.AddonStatusActive, v1Active.Status) |
|
|
| v2Body := validAddonRequest("addon_versioning_v2") |
| v2Body.Key = sharedKey |
|
|
| v2, err := c.Addons.Create(t.Context(), v2Body) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, v2) |
| assert.EqualValues(t, 2, v2.Version) |
| assert.Equal(t, v3sdk.AddonStatusDraft, v2.Status) |
| assert.Equal(t, sharedKey, v2.Key) |
| assert.NotEqual(t, v1.ID, v2.ID) |
|
|
| v2Active, err := c.Addons.Publish(t.Context(), v2.ID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, v2Active) |
| require.Equal(t, v3sdk.AddonStatusActive, v2Active.Status) |
| require.NotNil(t, v2Active.EffectiveFrom) |
|
|
| v1After, err := c.Addons.Get(t.Context(), v1.ID) |
| c.requireStatus(http.StatusOK, err) |
| require.NotNil(t, v1After) |
| assert.Equal(t, v3sdk.AddonStatusArchived, v1After.Status) |
| require.NotNil(t, v1After.EffectiveTo) |
| assert.True(t, v1After.EffectiveTo.Equal(*v2Active.EffectiveFrom), |
| "v1.EffectiveTo (%s) must equal v2.EffectiveFrom (%s)", |
| v1After.EffectiveTo, v2Active.EffectiveFrom) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func TestV3AddonFeatureReferenceResolution(t *testing.T) { |
| c := newV3Client(t) |
|
|
| |
| const fakeFeatureID = "01HZZ0000000000000000FAKE1" |
|
|
| rc := validFlatRateCard("feature_ref") |
| rc.Feature = &v3sdk.FeatureReference{ID: fakeFeatureID} |
|
|
| body := validAddonRequest("feature_ref") |
| body.RateCards = []v3sdk.RateCardInput{rc} |
|
|
| _, err := c.Addons.Create(t.Context(), body) |
| problem := requireProblem(t, err, http.StatusBadRequest) |
| assertValidationCode(t, problem, "rate_card_feature_not_found") |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func TestV3AddonPaymentTermPriceCompatibility(t *testing.T) { |
| t.Run("flat + in_advance → 201", func(t *testing.T) { |
| c := newV3Client(t) |
|
|
| |
| body := validAddonRequest("payment_term_flat") |
|
|
| addon, err := c.Addons.Create(t.Context(), body) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, addon) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func TestV3AddonInstanceTypePriceCompatibility(t *testing.T) { |
| c := newV3Client(t) |
|
|
| meterKey := uniqueKey("single_unit") |
|
|
| m, err := c.Meters.Create(t.Context(), v3sdk.CreateMeterRequest{ |
| Key: meterKey, |
| Name: "Test Meter " + meterKey, |
| Aggregation: v3sdk.MeterAggregationSum, |
| EventType: uniqueKey("sanity_event"), |
| ValueProperty: lo.ToPtr("$.value"), |
| }) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, m) |
| require.NotEmpty(t, m.ID) |
|
|
| featureKey := uniqueKey("single_unit") |
|
|
| f, err := c.Features.Create(t.Context(), v3sdk.CreateFeatureRequest{ |
| Key: featureKey, |
| Name: "Test Feature " + featureKey, |
| Meter: &v3sdk.FeatureMeterReferenceInput{ |
| ID: m.ID, |
| }, |
| }) |
| c.requireStatus(http.StatusCreated, err) |
| require.NotNil(t, f) |
| require.NotEmpty(t, f.ID) |
|
|
| cases := []struct { |
| name string |
| instanceType v3sdk.AddonInstanceType |
| rateCard v3sdk.RateCardInput |
| expectedStatus int |
| }{ |
| { |
| name: "multiple + flat rate card → 201", |
| instanceType: v3sdk.AddonInstanceTypeMultiple, |
| rateCard: validFlatRateCard("multi_flat"), |
| expectedStatus: http.StatusCreated, |
| }, |
| { |
| name: "single + unit rate card → 201", |
| instanceType: v3sdk.AddonInstanceTypeSingle, |
| rateCard: validUnitRateCard(*f), |
| expectedStatus: http.StatusCreated, |
| }, |
| } |
|
|
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| body := validAddonRequest("instance_type_price") |
| body.InstanceType = tc.instanceType |
| body.RateCards = []v3sdk.RateCardInput{tc.rateCard} |
|
|
| addon, err := c.Addons.Create(t.Context(), body) |
| c.requireStatus(tc.expectedStatus, err) |
| require.NotNil(t, addon) |
| }) |
| } |
| } |
|
|