11package confidence
22
33import (
4+ "context"
45 "errors"
6+ "os"
7+ "path/filepath"
8+ "strings"
59 "testing"
10+
11+ adminv1 "github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence/proto/confidence/flags/admin/v1"
12+ iamv1 "github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence/proto/confidence/iam/v1"
13+ "github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence/proto/resolver"
14+ "github.com/tetratelabs/wazero"
15+ "google.golang.org/protobuf/proto"
16+ "google.golang.org/protobuf/types/known/structpb"
617)
718
8- // Note: SwapWasmResolverApi requires a real WASM runtime and ResolverApi instances
9- // which are complex to mock. The critical logic is tested through integration tests.
10- // These unit tests verify the error handling and basic structure.
19+ // Helper to load test data from the data directory
20+ func loadTestResolverState (t * testing.T ) []byte {
21+ dataPath := filepath .Join (".." , ".." , "data" , "resolver_state_current.pb" )
22+ data , err := os .ReadFile (dataPath )
23+ if err != nil {
24+ t .Skipf ("Skipping test - could not load test resolver state: %v" , err )
25+ }
26+ return data
27+ }
28+
29+ func loadTestAccountID (t * testing.T ) string {
30+ dataPath := filepath .Join (".." , ".." , "data" , "account_id" )
31+ data , err := os .ReadFile (dataPath )
32+ if err != nil {
33+ t .Skipf ("Skipping test - could not load test account ID: %v" , err )
34+ }
35+ return strings .TrimSpace (string (data ))
36+ }
37+
38+ // Helper function to create minimal valid resolver state for testing
39+ func createMinimalResolverState () []byte {
40+ state := & adminv1.ResolverState {
41+ Flags : []* adminv1.Flag {},
42+ ClientCredentials : []* iamv1.ClientCredential {
43+ {
44+ Credential : & iamv1.ClientCredential_ClientSecret_ {
45+ ClientSecret : & iamv1.ClientCredential_ClientSecret {
46+ Secret : "test-secret" ,
47+ },
48+ },
49+ },
50+ },
51+ }
52+ data , err := proto .Marshal (state )
53+ if err != nil {
54+ panic ("Failed to create minimal state: " + err .Error ())
55+ }
56+ return data
57+ }
58+
59+ func TestSwapWasmResolverApi_NewSwapWasmResolverApi (t * testing.T ) {
60+ ctx := context .Background ()
61+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
62+ defer runtime .Close (ctx )
63+
64+ flagLogger := NewNoOpWasmFlagLogger ()
65+ initialState := createMinimalResolverState ()
66+ accountId := "test-account"
67+
68+ swap , err := NewSwapWasmResolverApi (ctx , runtime , defaultWasmBytes , flagLogger , initialState , accountId )
69+ if err != nil {
70+ t .Fatalf ("Failed to create SwapWasmResolverApi: %v" , err )
71+ }
72+ defer swap .Close (ctx )
73+
74+ if swap == nil {
75+ t .Fatal ("Expected non-nil SwapWasmResolverApi" )
76+ }
77+
78+ if swap .runtime == nil {
79+ t .Error ("Expected runtime to be set" )
80+ }
81+
82+ if swap .compiledModule == nil {
83+ t .Error ("Expected compiled module to be set" )
84+ }
85+
86+ if swap .flagLogger == nil {
87+ t .Error ("Expected flag logger to be set" )
88+ }
89+ }
90+
91+ func TestSwapWasmResolverApi_NewSwapWasmResolverApi_InvalidWasm (t * testing.T ) {
92+ ctx := context .Background ()
93+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
94+ defer runtime .Close (ctx )
95+
96+ flagLogger := NewNoOpWasmFlagLogger ()
97+ initialState := createMinimalResolverState ()
98+ accountId := "test-account"
99+
100+ // Use invalid WASM bytes
101+ invalidWasmBytes := []byte ("not valid wasm" )
102+
103+ _ , err := NewSwapWasmResolverApi (ctx , runtime , invalidWasmBytes , flagLogger , initialState , accountId )
104+ if err == nil {
105+ t .Fatal ("Expected error when creating SwapWasmResolverApi with invalid WASM" )
106+ }
107+ }
108+
109+ func TestSwapWasmResolverApi_WithRealState (t * testing.T ) {
110+ ctx := context .Background ()
111+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
112+ defer runtime .Close (ctx )
113+
114+ flagLogger := NewNoOpWasmFlagLogger ()
115+
116+ // Load real test state from data directory
117+ testState := loadTestResolverState (t )
118+ testAcctID := loadTestAccountID (t )
119+
120+ swap , err := NewSwapWasmResolverApi (ctx , runtime , defaultWasmBytes , flagLogger , testState , testAcctID )
121+ if err != nil {
122+ t .Fatalf ("Failed to create SwapWasmResolverApi with real state: %v" , err )
123+ }
124+ defer swap .Close (ctx )
125+
126+ // Try to resolve a flag with the real state
127+ // Note: We need a valid client secret from the state
128+ request := & resolver.ResolveFlagsRequest {
129+ Flags : []string {"flags/test-flag" },
130+ Apply : false ,
131+ ClientSecret : "CLIENT_SECRET" , // This needs to match a credential in the state
132+ EvaluationContext : & structpb.Struct {
133+ Fields : map [string ]* structpb.Value {
134+ "targeting_key" : structpb .NewStringValue ("user-123" ),
135+ },
136+ },
137+ }
138+
139+ response , err := swap .Resolve (request )
140+ // It's ok if resolution fails due to client secret mismatch or missing flags
141+ // The important part is that the WASM module loaded, state was set, and didn't crash
142+ if err != nil {
143+ t .Logf ("Resolve returned error (expected with CLIENT_SECRET placeholder): %v" , err )
144+ // When there's an error from WASM, response may be nil
145+ return
146+ }
147+
148+ if response != nil {
149+ t .Logf ("Successfully resolved with real state, got %d flags" , len (response .ResolvedFlags ))
150+ }
151+ }
152+
153+ func TestSwapWasmResolverApi_UpdateStateAndFlushLogs (t * testing.T ) {
154+ ctx := context .Background ()
155+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
156+ defer runtime .Close (ctx )
157+
158+ flagLogger := NewNoOpWasmFlagLogger ()
159+ initialState := createMinimalResolverState ()
160+ accountId := "test-account"
161+
162+ swap , err := NewSwapWasmResolverApi (ctx , runtime , defaultWasmBytes , flagLogger , initialState , accountId )
163+ if err != nil {
164+ t .Fatalf ("Failed to create SwapWasmResolverApi: %v" , err )
165+ }
166+ defer swap .Close (ctx )
167+
168+ // Update with new state
169+ newState := createMinimalResolverState ()
170+ err = swap .UpdateStateAndFlushLogs (newState , accountId )
171+ if err != nil {
172+ t .Fatalf ("UpdateStateAndFlushLogs failed: %v" , err )
173+ }
174+
175+ // Verify that we can call Resolve after update (even if it returns an error due to client secret)
176+ request := & resolver.ResolveFlagsRequest {
177+ Flags : []string {"flags/test-flag" },
178+ Apply : false ,
179+ ClientSecret : "test-secret" ,
180+ EvaluationContext : & structpb.Struct {
181+ Fields : map [string ]* structpb.Value {
182+ "targeting_key" : structpb .NewStringValue ("user-123" ),
183+ },
184+ },
185+ }
186+
187+ // Call Resolve - it may fail due to client secret not being in state, which is OK
188+ _ , err = swap .Resolve (request )
189+ // The key point is that UpdateStateAndFlushLogs completed and the swap happened
190+ // Resolution errors are expected with minimal test state
191+ t .Logf ("Resolve after update result: %v" , err )
192+ }
193+
194+ func TestSwapWasmResolverApi_MultipleUpdates (t * testing.T ) {
195+ ctx := context .Background ()
196+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
197+ defer runtime .Close (ctx )
198+
199+ flagLogger := NewNoOpWasmFlagLogger ()
200+ initialState := createMinimalResolverState ()
201+ accountId := "test-account"
202+
203+ swap , err := NewSwapWasmResolverApi (ctx , runtime , defaultWasmBytes , flagLogger , initialState , accountId )
204+ if err != nil {
205+ t .Fatalf ("Failed to create SwapWasmResolverApi: %v" , err )
206+ }
207+ defer swap .Close (ctx )
208+
209+ // Perform multiple state updates to verify the swap mechanism works correctly
210+ for i := 0 ; i < 3 ; i ++ {
211+ newState := createMinimalResolverState ()
212+ err := swap .UpdateStateAndFlushLogs (newState , accountId )
213+ if err != nil {
214+ t .Fatalf ("Update %d failed: %v" , i , err )
215+ }
216+
217+ // Verify that Resolve can be called after each update
218+ request := & resolver.ResolveFlagsRequest {
219+ Flags : []string {"flags/test-flag" },
220+ Apply : false ,
221+ ClientSecret : "test-secret" ,
222+ EvaluationContext : & structpb.Struct {
223+ Fields : map [string ]* structpb.Value {
224+ "targeting_key" : structpb .NewStringValue ("user-123" ),
225+ },
226+ },
227+ }
228+
229+ // The key verification is that the swap completed successfully
230+ // Resolution may fail with minimal test state, which is expected
231+ _ , resolveErr := swap .Resolve (request )
232+ t .Logf ("Update %d: Resolve result: %v" , i , resolveErr )
233+ }
234+ }
235+
236+ func TestSwapWasmResolverApi_Close (t * testing.T ) {
237+ ctx := context .Background ()
238+ runtime := wazero .NewRuntimeWithConfig (ctx , wazero .NewRuntimeConfig ())
239+ defer runtime .Close (ctx )
240+
241+ flagLogger := NewNoOpWasmFlagLogger ()
242+ initialState := createMinimalResolverState ()
243+ accountId := "test-account"
244+
245+ swap , err := NewSwapWasmResolverApi (ctx , runtime , defaultWasmBytes , flagLogger , initialState , accountId )
246+ if err != nil {
247+ t .Fatalf ("Failed to create SwapWasmResolverApi: %v" , err )
248+ }
249+
250+ // Close should not panic
251+ swap .Close (ctx )
252+
253+ // Note: Closing again may cause issues with WASM module, so we don't test double-close
254+ }
11255
12256func TestErrInstanceClosed (t * testing.T ) {
13257 err := ErrInstanceClosed
@@ -20,5 +264,4 @@ func TestErrInstanceClosed(t *testing.T) {
20264 if ! errors .Is (testErr , ErrInstanceClosed ) {
21265 t .Error ("Expected errors.Is to work with ErrInstanceClosed" )
22266 }
23- }
24-
267+ }
0 commit comments