@@ -2,9 +2,11 @@ package e2e
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "math"
78 "net/http"
9+ "os"
810 "testing"
911 "time"
1012
@@ -640,3 +642,283 @@ func TestEntitlementWithLatestAggregation(t *testing.T) {
640642 }, 2 * time .Minute , time .Second )
641643 })
642644}
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+ }
0 commit comments