Skip to content

Commit 1cda562

Browse files
authored
Revert "fix: static entitlement deserialization" (#3718)
1 parent efa1207 commit 1cda562

File tree

5 files changed

+17
-316
lines changed

5 files changed

+17
-316
lines changed

api/v3/handlers/customers/entitlementaccess/mapping.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package customersentitlement
22

33
import (
4+
"encoding/json"
45
"errors"
6+
"fmt"
57

68
api "github.com/openmeterio/openmeter/api/v3"
79
"github.com/openmeterio/openmeter/openmeter/entitlement"
810
booleanentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/boolean"
911
meteredentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/metered"
1012
staticentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/static"
13+
"github.com/openmeterio/openmeter/pkg/models"
1114
)
1215

1316
// mapEntitlementValueToAPI maps an entitlement value to an API entitlement access result.
@@ -26,9 +29,17 @@ func mapEntitlementValueToAPI(featureKey string, entitlementValue entitlement.En
2629
HasAccess: ent.HasAccess(),
2730
}
2831

29-
// Config is now properly encoded (unwrapped at DB layer)
32+
// If config is not nil, unmarshal it
3033
if ent.Config != nil {
31-
jsonValue := string(ent.Config)
34+
var jsonValue string
35+
36+
// FIXME (pmarton): static config is double json encoded, we need to unmarshal before returning it
37+
if err := json.Unmarshal(ent.Config, &jsonValue); err != nil {
38+
return true, api.BillingEntitlementAccessResult{}, models.NewGenericValidationError(
39+
fmt.Errorf("failed to unmarshal static entitlement config: %w", err),
40+
)
41+
}
42+
3243
accessResult.Config = &jsonValue
3344
}
3445

e2e/entitlement_test.go

Lines changed: 0 additions & 282 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package e2e
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
76
"math"
87
"net/http"
9-
"os"
108
"testing"
119
"time"
1210

@@ -642,283 +640,3 @@ func TestEntitlementWithLatestAggregation(t *testing.T) {
642640
}, 2*time.Minute, time.Second)
643641
})
644642
}
645-
646-
func TestEntitlementStaticConfigEncoding(t *testing.T) {
647-
client := initClient(t)
648-
649-
ctx, cancel := context.WithCancel(context.Background())
650-
defer cancel()
651-
652-
subject := "ent_static_config_customer"
653-
customer := "ent_static_config_customer"
654-
var customerID string
655-
var featureId string
656-
var entitlementId string
657-
658-
c := CreateCustomerWithSubject(t, client, customer, subject)
659-
customerID = c.Id
660-
661-
t.Run("Create Feature", func(t *testing.T) {
662-
randKey := fmt.Sprintf("entitlement_static_config_feature_%d", time.Now().Unix())
663-
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
664-
Name: "Entitlement Static Config Feature",
665-
Key: randKey,
666-
})
667-
668-
require.NoError(t, err)
669-
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
670-
671-
featureId = resp.JSON201.Id
672-
})
673-
674-
expectedConfigJSON := `{"integrations":["github","gitlab"],"maxUsers":10}`
675-
// Config needs to be a JSON-encoded string (not raw JSON object)
676-
// because the API expects: {"config": "{...}"} not {"config": {...}}
677-
configAsJSONString, err := json.Marshal(expectedConfigJSON)
678-
require.NoError(t, err)
679-
680-
t.Run("Create Static Entitlement with Config (V1)", func(t *testing.T) {
681-
staticEntitlement := api.EntitlementStaticCreateInputs{
682-
Type: "static",
683-
FeatureId: &featureId,
684-
Config: configAsJSONString,
685-
}
686-
body := &api.CreateEntitlementJSONRequestBody{}
687-
err := body.FromEntitlementStaticCreateInputs(staticEntitlement)
688-
require.NoError(t, err)
689-
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
690-
691-
require.NoError(t, err)
692-
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
693-
694-
static, err := resp.JSON201.AsEntitlementStatic()
695-
require.NoError(t, err)
696-
entitlementId = static.Id
697-
698-
// The config comes as json.RawMessage which is a JSON-encoded string
699-
// Unmarshal it to get the actual JSON content
700-
var configStr string
701-
require.NoError(t, json.Unmarshal(static.Config, &configStr))
702-
require.JSONEq(t, expectedConfigJSON, configStr, "Config should be valid JSON, not double-encoded")
703-
})
704-
705-
t.Run("Get Entitlement by ID and Check Config (V1)", func(t *testing.T) {
706-
resp, err := client.GetEntitlementByIdWithResponse(ctx, entitlementId)
707-
require.NoError(t, err)
708-
require.Equal(t, http.StatusOK, resp.StatusCode())
709-
710-
static, err := resp.JSON200.AsEntitlementStatic()
711-
require.NoError(t, err)
712-
713-
// Verify config is not double-encoded when retrieved
714-
var configStr string
715-
require.NoError(t, json.Unmarshal(static.Config, &configStr))
716-
require.JSONEq(t, expectedConfigJSON, configStr, "Retrieved config should match and not be double-encoded")
717-
})
718-
719-
t.Run("Get Entitlement Value and Check Config (V1)", func(t *testing.T) {
720-
resp, err := client.GetEntitlementValueWithResponse(ctx, subject, entitlementId, nil)
721-
require.NoError(t, err)
722-
require.Equal(t, http.StatusOK, resp.StatusCode())
723-
724-
if resp.JSON200.Config != nil {
725-
// The config string should be valid JSON
726-
require.JSONEq(t, expectedConfigJSON, *resp.JSON200.Config, "EntitlementValue config should not be double-encoded")
727-
} else {
728-
t.Fatal("Config should not be nil in entitlement value")
729-
}
730-
})
731-
732-
// Test V2 API
733-
t.Run("Create Static Entitlement with Config (V2)", func(t *testing.T) {
734-
// Create a new feature for V2 test
735-
var v2FeatureId string
736-
{
737-
randKey := fmt.Sprintf("entitlement_static_config_v2_feature_%d", time.Now().Unix())
738-
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
739-
Name: "Entitlement Static Config V2 Feature",
740-
Key: randKey,
741-
})
742-
require.NoError(t, err)
743-
require.Equal(t, http.StatusCreated, resp.StatusCode())
744-
v2FeatureId = resp.JSON201.Id
745-
}
746-
747-
// Create static entitlement via V2 API using V1 static create inputs
748-
staticEntitlement := api.EntitlementStaticCreateInputs{
749-
Type: "static",
750-
FeatureId: &v2FeatureId,
751-
Config: configAsJSONString,
752-
}
753-
var body api.CreateCustomerEntitlementV2JSONRequestBody
754-
require.NoError(t, body.FromEntitlementStaticCreateInputs(staticEntitlement))
755-
756-
resp, err := client.CreateCustomerEntitlementV2WithResponse(ctx, customer, body)
757-
require.NoError(t, err)
758-
require.Equal(t, http.StatusCreated, resp.StatusCode(), "Invalid status code [response_body=%s]", string(resp.Body))
759-
760-
v2Static, err := resp.JSON201.AsEntitlementStaticV2()
761-
require.NoError(t, err)
762-
763-
// The config should be a valid JSON string, not double-encoded
764-
var configStr string
765-
require.NoError(t, json.Unmarshal(v2Static.Config, &configStr))
766-
require.JSONEq(t, expectedConfigJSON, configStr, "V2 Config should be valid JSON, not double-encoded")
767-
})
768-
769-
// Test V3 API (Entitlement Access)
770-
var v3FeatureKey string
771-
t.Run("Create Feature for V3", func(t *testing.T) {
772-
v3FeatureKey = fmt.Sprintf("entitlement_static_config_v3_feature_%d", time.Now().Unix())
773-
resp, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{
774-
Name: "Entitlement Static Config V3 Feature",
775-
Key: v3FeatureKey,
776-
})
777-
require.NoError(t, err)
778-
require.Equal(t, http.StatusCreated, resp.StatusCode())
779-
})
780-
781-
t.Run("Create Static Entitlement for V3", func(t *testing.T) {
782-
staticEntitlement := api.EntitlementStaticCreateInputs{
783-
Type: "static",
784-
FeatureKey: &v3FeatureKey,
785-
Config: configAsJSONString,
786-
}
787-
body := &api.CreateEntitlementJSONRequestBody{}
788-
require.NoError(t, body.FromEntitlementStaticCreateInputs(staticEntitlement))
789-
resp, err := client.CreateEntitlementWithResponse(ctx, subject, *body)
790-
require.NoError(t, err)
791-
require.Equal(t, http.StatusCreated, resp.StatusCode())
792-
})
793-
794-
// Raw HTTP tests to verify actual format (independent of Go client)
795-
baseURL := getBaseURL(t)
796-
httpClient := &http.Client{}
797-
798-
t.Run("Raw HTTP: Get Entitlement by ID (V1)", func(t *testing.T) {
799-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/api/v1/entitlements/%s", baseURL, entitlementId), nil)
800-
require.NoError(t, err)
801-
802-
resp, err := httpClient.Do(req)
803-
require.NoError(t, err)
804-
defer resp.Body.Close()
805-
806-
require.Equal(t, http.StatusOK, resp.StatusCode)
807-
808-
var result map[string]interface{}
809-
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
810-
811-
// Verify config is a string in the JSON response
812-
configValue, ok := result["config"]
813-
require.True(t, ok, "config field should exist")
814-
configStr, ok := configValue.(string)
815-
require.True(t, ok, "config should be a JSON string, not an object")
816-
require.JSONEq(t, expectedConfigJSON, configStr, "Raw HTTP: config should not be double-encoded")
817-
})
818-
819-
t.Run("Raw HTTP: Get Entitlement Value (V1)", func(t *testing.T) {
820-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/api/v1/subjects/%s/entitlements/%s/value", baseURL, subject, entitlementId), nil)
821-
require.NoError(t, err)
822-
823-
resp, err := httpClient.Do(req)
824-
require.NoError(t, err)
825-
defer resp.Body.Close()
826-
827-
require.Equal(t, http.StatusOK, resp.StatusCode)
828-
829-
var result map[string]interface{}
830-
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
831-
832-
// Verify config is a string in the JSON response
833-
configValue, ok := result["config"]
834-
require.True(t, ok, "config field should exist")
835-
configStr, ok := configValue.(string)
836-
require.True(t, ok, "config should be a JSON string, not an object")
837-
require.JSONEq(t, expectedConfigJSON, configStr, "Raw HTTP: EntitlementValue config should not be double-encoded")
838-
})
839-
840-
t.Run("Raw HTTP: List Customer Entitlements (V2)", func(t *testing.T) {
841-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/api/v2/customers/%s/entitlements", baseURL, customer), nil)
842-
require.NoError(t, err)
843-
844-
resp, err := httpClient.Do(req)
845-
require.NoError(t, err)
846-
defer resp.Body.Close()
847-
848-
require.Equal(t, http.StatusOK, resp.StatusCode)
849-
850-
var result map[string]interface{}
851-
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
852-
853-
items, ok := result["items"].([]interface{})
854-
require.True(t, ok, "items should be an array")
855-
require.NotEmpty(t, items, "should have at least one entitlement")
856-
857-
// Find a static entitlement with config
858-
var foundConfig bool
859-
for _, item := range items {
860-
entitlement, ok := item.(map[string]interface{})
861-
if !ok {
862-
continue
863-
}
864-
if entitlement["type"] == "static" {
865-
if configValue, ok := entitlement["config"]; ok {
866-
configStr, ok := configValue.(string)
867-
require.True(t, ok, "V2 config should be a JSON string, not an object")
868-
require.JSONEq(t, expectedConfigJSON, configStr, "Raw HTTP V2: config should not be double-encoded")
869-
foundConfig = true
870-
break
871-
}
872-
}
873-
}
874-
require.True(t, foundConfig, "should find a static entitlement with config")
875-
})
876-
877-
t.Run("Raw HTTP: Check Entitlement Access (V3)", func(t *testing.T) {
878-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/api/v3/openmeter/customers/%s/entitlement-access", baseURL, customerID), nil)
879-
require.NoError(t, err)
880-
881-
resp, err := httpClient.Do(req)
882-
require.NoError(t, err)
883-
defer resp.Body.Close()
884-
885-
require.Equal(t, http.StatusOK, resp.StatusCode, "V3 endpoint should be available")
886-
887-
var result map[string]interface{}
888-
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
889-
890-
data, ok := result["data"].([]interface{})
891-
require.True(t, ok, "data should be an array")
892-
require.NotEmpty(t, data, "should have at least one access result")
893-
894-
// Find the static entitlement we created
895-
var foundConfig bool
896-
for _, item := range data {
897-
access, ok := item.(map[string]interface{})
898-
if !ok {
899-
continue
900-
}
901-
// Check if this is our v3 feature
902-
if fk, ok := access["feature_key"].(string); ok && fk == v3FeatureKey {
903-
configValue, ok := access["config"]
904-
require.True(t, ok, "config field should exist for static entitlement")
905-
configStr, ok := configValue.(string)
906-
require.True(t, ok, "V3 config should be a JSON string, not an object")
907-
require.JSONEq(t, expectedConfigJSON, configStr, "Raw HTTP V3: config should not be double-encoded")
908-
foundConfig = true
909-
break
910-
}
911-
}
912-
require.True(t, foundConfig, "should find the v3 static entitlement with config")
913-
})
914-
}
915-
916-
// getBaseURL returns the base URL for raw HTTP requests
917-
func getBaseURL(t *testing.T) string {
918-
t.Helper()
919-
address := os.Getenv("OPENMETER_ADDRESS")
920-
if address == "" {
921-
t.Skip("OPENMETER_ADDRESS not set")
922-
}
923-
return address
924-
}

openmeter/entitlement/adapter/entitlement.go

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package adapter
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
76
"strings"
87
"time"
@@ -509,16 +508,7 @@ func (a *entitlementDBAdapter) mapEntitlementEntity(e *db.Entitlement) (*entitle
509508
}
510509

511510
if e.Config != nil {
512-
// Config in DB is double-encoded (stored as JSON string with quotes)
513-
// Unwrap one layer when reading from DB so the domain model has correct encoding
514-
var configStr string
515-
if err := json.Unmarshal(e.Config, &configStr); err == nil {
516-
// Successfully unmarshaled as string - store the unwrapped JSON
517-
ent.Config = []byte(configStr)
518-
} else {
519-
// Couldn't unmarshal as string - use as-is
520-
ent.Config = e.Config
521-
}
511+
ent.Config = e.Config
522512
}
523513

524514
if e.UsagePeriodAnchor != nil && e.UsagePeriodInterval != nil {

0 commit comments

Comments
 (0)