openmeter / e2e /entitlement_test.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 5)
b47934a verified
Raw
History Blame Contribute Delete
26.6 kB
package e2e
import (
"context"
"fmt"
"math"
"net/http"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
cloudevents "github.com/cloudevents/sdk-go/v2/event"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
api "github.com/openmeterio/openmeter/api/client/go"
"github.com/openmeterio/openmeter/pkg/convert"
)
func TestEntitlementV2(t *testing.T) {
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
meterSlug := "entitlement_v2_meter"
subject := "ent_customer_v2"
// ensure subject exists
{
resp, err := client.UpsertSubjectWithResponse(ctx, api.UpsertSubjectJSONRequestBody{
api.SubjectUpsert{Key: subject},
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
}
apiMONTH := &api.RecurringPeriodInterval{}
require.NoError(t, apiMONTH.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
apiYEAR := &api.RecurringPeriodInterval{}
require.NoError(t, apiYEAR.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumYEAR))
// V2: Create entitlement via Customer API and list them (with real customer)
t.Run("V2 Create and List Customer Entitlements", func(t *testing.T) {
// Set up dedicated subject and customer
v2Subject := "ent_customer_v2"
{
resp, err := client.UpsertSubjectWithResponse(ctx, api.UpsertSubjectJSONRequestBody{api.SubjectUpsert{Key: v2Subject}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
}
var v2CustomerID string
{
resp, err := client.CreateCustomerWithResponse(ctx, api.CreateCustomerJSONRequestBody{
Name: "Entitlement V2 Customer",
UsageAttribution: &api.CustomerUsageAttribution{SubjectKeys: []string{v2Subject}},
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", resp.Body)
v2CustomerID = resp.JSON201.Id
}
// Create feature for V2 flow
var v2FeatureId string
{
randKey := fmt.Sprintf("entitlement_v2_list_feature_%d", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement V2 List Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
v2FeatureId = resp.JSON201.Id
}
// Create entitlement via customer V2 endpoint
{
iv := &api.RecurringPeriodInterval{}
require.NoError(t, iv.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
me := api.EntitlementMeteredV2CreateInputs{
Type: "metered",
FeatureId: &v2FeatureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *iv,
},
}
var body api.CreateCustomerEntitlementV2JSONRequestBody
require.NoError(t, body.FromEntitlementMeteredV2CreateInputs(me))
res, err := client.CreateCustomerEntitlementV2WithResponse(ctx, v2CustomerID, body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, res.StatusCode(), "Invalid status code [response_body=%s]", string(res.Body))
v2, err := res.JSON201.AsEntitlementMeteredV2()
require.NoError(t, err)
require.Equal(t, v2FeatureId, v2.FeatureId)
require.Equal(t, v2CustomerID, v2.CustomerId)
}
// List customer entitlements V2
{
resp, err := client.ListCustomerEntitlementsV2WithResponse(ctx, v2CustomerID, &api.ListCustomerEntitlementsV2Params{
Page: lo.ToPtr(1),
PageSize: lo.ToPtr(10),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
require.NotNil(t, resp.JSON200)
require.GreaterOrEqual(t, resp.JSON200.TotalCount, 1)
}
})
// V2: Validate error mapping when customer does not exist
t.Run("V2 Create with missing customer should map to 404", func(t *testing.T) {
iv := &api.RecurringPeriodInterval{}
require.NoError(t, iv.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
me := api.EntitlementMeteredV2CreateInputs{
Type: "metered",
FeatureKey: lo.ToPtr("nonexistent_feature_key"),
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *iv,
},
}
var body api.CreateCustomerEntitlementV2JSONRequestBody
require.NoError(t, body.FromEntitlementMeteredV2CreateInputs(me))
// Use random customer id to ensure not found
randomID := fmt.Sprintf("missing-%d", time.Now().UnixNano())
res, err := client.CreateCustomerEntitlementV2WithResponse(ctx, randomID, body)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, res.StatusCode(), "expected 404 mapping, got %d", res.StatusCode())
})
}
func TestEntitlementWithUniqueCountAggregation(t *testing.T) {
// This takes a minute to run in itself due to Entitlements being one minute rounded and we need to wait in the last
// test for the minute to pass.
if !shouldRunSlowTests(t) {
t.Skip("Skipping slow test, please reenable when we have a second resolution for entitlements")
}
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
meterSlug := "entitlement_uc_meter"
subject := "ent_customer"
var featureId string
var entitlementId string
// ensure subject exists
{
resp, err := client.UpsertSubjectWithResponse(ctx, api.UpsertSubjectJSONRequestBody{
api.SubjectUpsert{Key: subject},
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
}
apiMONTH := &api.RecurringPeriodInterval{}
require.NoError(t, apiMONTH.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
apiYEAR := &api.RecurringPeriodInterval{}
require.NoError(t, apiYEAR.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumYEAR))
t.Run("Create Feature", func(t *testing.T) {
randKey := fmt.Sprintf("entitlement_uc_test_feature_%d", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement Test Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
featureId = resp.JSON201.Id
})
t.Run("Create a Entitlement", func(t *testing.T) {
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: &featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *apiMONTH,
},
}
body := &api.CreateEntitlementJSONRequestBody{}
err := body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
require.NoError(t, err)
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
metered, err := resp.JSON201.AsEntitlementMetered()
require.NoError(t, err)
require.Equal(t, metered.SubjectKey, subject)
entitlementId = metered.Id
})
t.Run("Create entitlement via Customer V2 API", func(t *testing.T) {
// Ensure subject exists and matches a customer mapping (use same subject)
{
resp, err := client.UpsertSubjectWithResponse(ctx, api.UpsertSubjectJSONRequestBody{api.SubjectUpsert{Key: subject}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
}
// Create a feature for V2 test
var v2FeatureId string
{
randKey := fmt.Sprintf("entitlement_v2_test_feature_%d", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement V2 Test Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
v2FeatureId = resp.JSON201.Id
}
// Create entitlement via customer V2 endpoint
{
apiMONTH := &api.RecurringPeriodInterval{}
require.NoError(t, apiMONTH.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
meteredEntitlement := api.EntitlementMeteredV2CreateInputs{
Type: "metered",
FeatureId: &v2FeatureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *apiMONTH,
},
}
// Build union body for V2
var createBody api.CreateCustomerEntitlementV2JSONRequestBody
require.NoError(t, createBody.FromEntitlementMeteredV2CreateInputs(meteredEntitlement))
// Use customerIdOrKey that maps 1:1 to subject
res, err := client.CreateCustomerEntitlementV2WithResponse(ctx, subject, createBody)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, res.StatusCode(), "Invalid status code [response_body=%s]", string(res.Body))
// Validate V2 union response minimally
v2, err := res.JSON201.AsEntitlementMeteredV2()
require.NoError(t, err)
require.Equal(t, v2FeatureId, v2.FeatureId)
// CustomerKey may be nil if we resolved by customerId only; assert customerId presence
require.Equal(t, subject, v2.CustomerId)
}
})
grantAmount := 100.0
t.Run("Create Grant", func(t *testing.T) {
effectiveAt := time.Now().Truncate(time.Minute)
priority := uint8(1)
maxRolloverAmount := 100.0
minRolloverAmount := 0.0
// Create grant
resp, err := client.CreateGrantWithResponse(ctx, subject, entitlementId, api.EntitlementGrantCreateInput{
Amount: grantAmount,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{
Duration: "MONTH",
Count: 1,
},
Priority: &priority,
MaxRolloverAmount: &maxRolloverAmount,
MinRolloverAmount: &minRolloverAmount,
Recurrence: &api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *apiYEAR,
},
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", resp.Body)
})
uniqueEventCount := 10
t.Run("Report usage", func(t *testing.T) {
now := time.Now()
for i := 0; i < uniqueEventCount*2; i++ {
timestamp := gofakeit.DateRange(now, now.Add(time.Second))
ev := cloudevents.New()
ev.SetID(gofakeit.UUID())
ev.SetSource("my-app")
ev.SetType("credit_event_uc")
ev.SetSubject(subject)
ev.SetTime(timestamp)
_ = ev.SetData("application/json", map[string]string{
// Let's have 50% of the events with the same value
"value": fmt.Sprintf("%v", math.Floor(float64(i)/2)),
})
resp, err := client.IngestEventWithResponse(ctx, ev)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode())
}
// Wait for events to be processed
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := client.QueryMeterWithResponse(ctx, meterSlug, &api.QueryMeterParams{
To: convert.ToPointer(time.Now().Truncate(time.Minute)),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.Len(t, resp.JSON200.Data, 1)
assert.Equal(t, float64(uniqueEventCount), resp.JSON200.Data[0].Value)
}, 2*time.Minute, time.Second)
})
t.Run("Should calculate usage correctly", func(t *testing.T) {
resp, err := client.GetEntitlementValueWithResponse(ctx, subject, entitlementId, &api.GetEntitlementValueParams{
Time: convert.ToPointer(time.Now().Truncate(time.Minute)),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.NotNil(t, resp.JSON200.Balance)
assert.Equal(t, grantAmount-float64(uniqueEventCount), *resp.JSON200.Balance)
assert.Equal(t, float64(uniqueEventCount), *resp.JSON200.Usage)
})
t.Run("Should not count usage of previous period twice", func(t *testing.T) {
// let's wait till the next minute so we can reset
currMinute := time.Now().Truncate(time.Minute)
waitUntil := currMinute.Add(time.Minute + time.Second)
time.Sleep(time.Until(waitUntil))
effectiveAt := time.Now().Truncate(time.Minute)
// Reset usage
_, err := client.ResetEntitlementUsageWithResponse(ctx, subject, entitlementId, api.ResetEntitlementUsageJSONRequestBody{
EffectiveAt: &effectiveAt,
})
require.NoError(t, err)
resp, err := client.GetEntitlementValueWithResponse(ctx, subject, entitlementId, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.NotNil(t, resp.JSON200.Balance)
// Grant can roll over with full amount
assert.Equal(t, grantAmount-float64(uniqueEventCount), *resp.JSON200.Balance)
assert.Equal(t, float64(0), *resp.JSON200.Usage)
})
}
func TestVoidGrantAtParam(t *testing.T) {
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
meterSlug := "entitlement_uc_meter"
customer := fmt.Sprintf("void_at_customer_%d", time.Now().UnixNano())
subject := customer + "-subject"
CreateCustomerWithSubject(t, client, customer, subject)
apiMONTH := &api.RecurringPeriodInterval{}
require.NoError(t, apiMONTH.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
// Create feature
var featureId string
{
randKey := fmt.Sprintf("void_at_feature_%d", time.Now().UnixNano())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Void At Test Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "body: %s", resp.Body)
featureId = resp.JSON201.Id
}
// Create entitlement
var entitlementId string
{
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: &featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *apiMONTH,
},
}
body := &api.CreateEntitlementJSONRequestBody{}
require.NoError(t, body.FromEntitlementMeteredCreateInputs(meteredEntitlement))
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "body: %s", resp.Body)
metered, err := resp.JSON201.AsEntitlementMetered()
require.NoError(t, err)
entitlementId = metered.Id
}
// Helper: create a grant and return its ID
createGrant := func(t *testing.T) string {
t.Helper()
effectiveAt := time.Now().Truncate(time.Minute)
priority := uint8(1)
resp, err := client.CreateGrantWithResponse(ctx, subject, entitlementId, api.EntitlementGrantCreateInput{
Amount: 100,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{
Duration: "MONTH",
Count: 1,
},
Priority: &priority,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "body: %s", resp.Body)
return resp.JSON201.Id
}
t.Run("Void grant without at param (default behavior)", func(t *testing.T) {
grantId := createGrant(t)
resp, err := client.VoidGrantWithResponse(ctx, grantId, nil)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode(), "body: %s", resp.Body)
})
t.Run("Void grant with at param in current period", func(t *testing.T) {
grantId := createGrant(t)
// Use current time which is within the current usage period
at := time.Now().Truncate(time.Minute)
resp, err := client.VoidGrantWithResponse(ctx, grantId, &api.VoidGrantParams{
At: &at,
})
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode(), "body: %s", resp.Body)
})
t.Run("Void grant with future at param returns 400", func(t *testing.T) {
grantId := createGrant(t)
futureTime := time.Now().Add(1 * time.Hour)
resp, err := client.VoidGrantWithResponse(ctx, grantId, &api.VoidGrantParams{
At: &futureTime,
})
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode(), "body: %s", resp.Body)
})
t.Run("Void grant with at before usage period returns 400", func(t *testing.T) {
grantId := createGrant(t)
// Usage period anchor is 2024-01-01 with monthly recurrence,
// so a time well before the current period start should fail.
oldTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
resp, err := client.VoidGrantWithResponse(ctx, grantId, &api.VoidGrantParams{
At: &oldTime,
})
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode(), "body: %s", resp.Body)
})
}
func TestEntitlementISOUsagePeriod(t *testing.T) {
t.Run("Should create entitlement with ISO usage period", func(t *testing.T) {
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
meterSlug := "entitlement_uc_meter"
customer := "ent_customer_2"
subject := customer + "-subject"
var featureId string
var entitlementId string
CreateCustomerWithSubject(t, client, customer, subject)
iv2w := &api.RecurringPeriodInterval{}
require.Nil(t, iv2w.FromRecurringPeriodInterval0("P2W"))
t.Run("Create Feature", func(t *testing.T) {
randKey := fmt.Sprintf("entitlement_uc_test_feature_%d", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement Test Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
featureId = resp.JSON201.Id
})
t.Run("Create a Entitlement", func(t *testing.T) {
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: &featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *iv2w,
},
}
body := &api.CreateEntitlementJSONRequestBody{}
err := body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
require.NoError(t, err)
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
metered, err := resp.JSON201.AsEntitlementMetered()
require.NoError(t, err)
require.Equal(t, metered.SubjectKey, subject)
entitlementId = metered.Id
})
t.Run("Create Grant", func(t *testing.T) {
effectiveAt := time.Now().Truncate(time.Minute)
priority := uint8(1)
maxRolloverAmount := 100.0
minRolloverAmount := 0.0
// Create grant
resp, err := client.CreateGrantWithResponse(ctx, subject, entitlementId, api.EntitlementGrantCreateInput{
Amount: 100,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{
Duration: "MONTH",
Count: 1,
},
Priority: &priority,
MaxRolloverAmount: &maxRolloverAmount,
MinRolloverAmount: &minRolloverAmount,
Recurrence: &api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *iv2w,
},
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", resp.Body)
})
})
}
func TestEntitlementWithLatestAggregation(t *testing.T) {
client := initClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
meterSlug := "entitlement_latest_meter"
subject := "ent_latest_customer"
customer := "ent_latest_customer"
var featureId string
var entitlementId string
CreateCustomerWithSubject(t, client, customer, subject)
apiMONTH := &api.RecurringPeriodInterval{}
require.NoError(t, apiMONTH.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumMONTH))
t.Run("Create Meter with LATEST aggregation", func(t *testing.T) {
resp, err := client.CreateMeterWithResponse(ctx, api.MeterCreate{
Slug: meterSlug,
Name: convert.ToPointer("Latest Aggregation Meter"),
Description: convert.ToPointer("Meter for testing LATEST aggregation type"),
Aggregation: api.MeterAggregationLatest,
EventType: "latest_event",
ValueProperty: convert.ToPointer("$.value"),
})
require.NoError(t, err)
// meter API returns 200 instead of 201
require.Equal(t, http.StatusOK, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
})
t.Run("Create Feature", func(t *testing.T) {
randKey := fmt.Sprintf("entitlement_latest_test_feature_%d", time.Now().Unix())
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
Name: "Entitlement Latest Test Feature",
MeterSlug: convert.ToPointer(meterSlug),
Key: randKey,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
featureId = resp.JSON201.Id
})
t.Run("Create Entitlement", func(t *testing.T) {
mu := api.MeasureUsageFrom{}
require.NoError(t, mu.FromMeasureUsageFromTime(time.Now().Add(-10*time.Minute).Truncate(time.Minute)))
meteredEntitlement := api.EntitlementMeteredCreateInputs{
Type: "metered",
FeatureId: &featureId,
UsagePeriod: api.RecurringPeriodCreateInput{
Anchor: convert.ToPointer(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)),
Interval: *apiMONTH,
},
MeasureUsageFrom: &mu,
}
body := &api.CreateEntitlementJSONRequestBody{}
err := body.FromEntitlementMeteredCreateInputs(meteredEntitlement)
require.NoError(t, err)
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
metered, err := resp.JSON201.AsEntitlementMetered()
require.NoError(t, err)
require.Equal(t, metered.SubjectKey, subject)
entitlementId = metered.Id
})
grantAmount := 1000.0
t.Run("Create Grant", func(t *testing.T) {
effectiveAt := time.Now().Add(-9 * time.Minute).Truncate(time.Minute)
priority := uint8(1)
// Create grant
resp, err := client.CreateGrantWithResponse(ctx, subject, entitlementId, api.EntitlementGrantCreateInput{
Amount: grantAmount,
EffectiveAt: effectiveAt,
Expiration: api.ExpirationPeriod{
Duration: "MONTH",
Count: 1,
},
Priority: &priority,
})
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", resp.Body)
})
t.Run("Report usage events with different values", func(t *testing.T) {
now := time.Now()
// Send multiple events with different values - LATEST should use the most recent value
events := []struct {
value float64
timestamp time.Time
}{
{100.0, now.Add(-5 * time.Minute)}, // Earlier event
{200.0, now.Add(-3 * time.Minute)}, // Middle event
{150.0, now.Add(-1 * time.Minute)}, // Latest event (this should be used)
}
for i, event := range events {
ev := cloudevents.New()
ev.SetID(gofakeit.UUID())
ev.SetSource("my-app")
ev.SetType("latest_event")
ev.SetSubject(subject)
ev.SetTime(event.timestamp)
_ = ev.SetData("application/json", map[string]interface{}{
"value": event.value,
})
resp, err := client.IngestEventWithResponse(ctx, ev)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode(), "Failed to ingest event %d", i)
}
// Wait for events to be processed
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := client.QueryMeterWithResponse(ctx, meterSlug, &api.QueryMeterParams{
To: convert.ToPointer(time.Now().Truncate(time.Minute)),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.Len(t, resp.JSON200.Data, 1)
// For LATEST aggregation, should return the most recent value (150.0)
assert.Equal(t, 150.0, resp.JSON200.Data[0].Value)
}, 2*time.Minute, time.Second)
})
t.Run("Should calculate balance correctly with LATEST aggregation", func(t *testing.T) {
resp, err := client.GetEntitlementValueWithResponse(ctx, subject, entitlementId, &api.GetEntitlementValueParams{
Time: convert.ToPointer(time.Now()),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.NotNil(t, resp.JSON200.Balance, "got body %s", string(resp.Body))
require.NotNil(t, resp.JSON200.Usage, "got body %s", string(resp.Body))
// Balance should be grant amount minus the latest usage value (150.0)
assert.Equal(t, grantAmount-150.0, *resp.JSON200.Balance)
assert.Equal(t, 150.0, *resp.JSON200.Usage)
})
t.Run("Should handle new latest value correctly", func(t *testing.T) {
if !shouldRunSlowTests(t) {
t.Skip("Skipping slow test, please reenable when we have a second resolution for entitlements")
}
// Send a new event with a different value
newLatestValue := 300.0
ev := cloudevents.New()
ev.SetID(gofakeit.UUID())
ev.SetSource("my-app")
ev.SetType("latest_event")
ev.SetSubject(subject)
ev.SetTime(time.Now())
_ = ev.SetData("application/json", map[string]interface{}{
"value": newLatestValue,
})
resp, err := client.IngestEventWithResponse(ctx, ev)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode())
// Wait for event to be processed
assert.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := client.GetEntitlementValueWithResponse(ctx, subject, entitlementId, &api.GetEntitlementValueParams{
Time: convert.ToPointer(time.Now().Truncate(time.Minute)),
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.NotNil(t, resp.JSON200.Balance)
require.NotNil(t, resp.JSON200.Usage)
// Balance should now reflect the new latest value
assert.Equal(t, grantAmount-newLatestValue, *resp.JSON200.Balance)
assert.Equal(t, newLatestValue, *resp.JSON200.Usage)
}, 2*time.Minute, time.Second)
})
}