Skip to content

Commit 4ef5333

Browse files
test(go): Add tests using sample state
1 parent 7ca1167 commit 4ef5333

File tree

1 file changed

+247
-4
lines changed

1 file changed

+247
-4
lines changed

openfeature-provider/go/swap_wasm_resolver_api_test.go

Lines changed: 247 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,257 @@
11
package confidence
22

33
import (
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

12256
func TestErrInstanceClosed(t *testing.T) {
13257
err := ErrInstanceClosed
@@ -21,4 +265,3 @@ func TestErrInstanceClosed(t *testing.T) {
21265
t.Error("Expected errors.Is to work with ErrInstanceClosed")
22266
}
23267
}
24-

0 commit comments

Comments
 (0)