Skip to content

Commit 4ca1b61

Browse files
committed
adds test to verify grant sources (and grants) agaisnt an reference sdk
1 parent ac3ba04 commit 4ca1b61

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package expand
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
"sort"
10+
"testing"
11+
"time"
12+
13+
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
14+
"github.com/conductorone/baton-sdk/pkg/dotc1z"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func getTestdataPathWithSuffix(syncID, suffix string) string {
19+
_, filename, _, _ := runtime.Caller(0)
20+
return filepath.Join(filepath.Dir(filename), "testdata", fmt.Sprintf("sync.%s.%s", syncID, suffix))
21+
}
22+
23+
// grantKey creates a unique key for a grant based on entitlement and principal.
24+
func grantKey(g *v2.Grant) string {
25+
return fmt.Sprintf("%s|%s|%s",
26+
g.GetEntitlement().GetId(),
27+
g.GetPrincipal().GetId().GetResourceType(),
28+
g.GetPrincipal().GetId().GetResource(),
29+
)
30+
}
31+
32+
// grantInfo holds grant data for comparison.
33+
type grantInfo struct {
34+
ID string
35+
Sources map[string]bool // Set of source entitlement IDs
36+
}
37+
38+
// loadAllGrants loads all grants from a c1z file into a map keyed by grantKey.
39+
func loadAllGrants(ctx context.Context, c1f *dotc1z.C1File) (map[string]*grantInfo, error) {
40+
grants := make(map[string]*grantInfo)
41+
42+
pageToken := ""
43+
for {
44+
resp, err := c1f.ListGrants(ctx, v2.GrantsServiceListGrantsRequest_builder{PageToken: pageToken}.Build())
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
for _, g := range resp.GetList() {
50+
key := grantKey(g)
51+
sources := make(map[string]bool)
52+
for sourceID := range g.GetSources().GetSources() {
53+
sources[sourceID] = true
54+
}
55+
grants[key] = &grantInfo{
56+
ID: g.GetId(),
57+
Sources: sources,
58+
}
59+
}
60+
61+
pageToken = resp.GetNextPageToken()
62+
if pageToken == "" {
63+
break
64+
}
65+
}
66+
67+
return grants, nil
68+
}
69+
70+
// TestExpandCorrectness runs expansion on an unexpanded c1z and compares against a reference (baton-sdk v0.5.25).
71+
// This test takes ~60 seconds to run. Use: go test -timeout 2m -run TestExpandCorrectness.
72+
func TestExpandCorrectness(t *testing.T) {
73+
if testing.Short() {
74+
t.Skip("skipping slow expansion correctness test in short mode")
75+
}
76+
77+
testCases := []struct {
78+
name string
79+
syncID string
80+
}{
81+
{
82+
name: "Small",
83+
syncID: "36zGvJw3uxU1QMJKU2yPVQ1hBOC",
84+
},
85+
// {
86+
// name: "SmallMedium",
87+
// syncID: "36zM46KKuaBq0wjSSvKh5o0350y",
88+
// },
89+
}
90+
91+
for _, tc := range testCases {
92+
t.Run(tc.name, func(t *testing.T) {
93+
unexpandedPath := getTestdataPathWithSuffix(tc.syncID, "unexpanded")
94+
expectedPath := getTestdataPathWithSuffix(tc.syncID, "expanded")
95+
96+
if _, err := os.Stat(unexpandedPath); os.IsNotExist(err) {
97+
t.Skipf("unexpanded testdata file not found: %s", unexpandedPath)
98+
}
99+
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
100+
t.Skipf("expanded testdata file not found: %s", expectedPath)
101+
}
102+
103+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
104+
defer cancel()
105+
106+
// Copy unexpanded file to temp location
107+
tmpFile, err := os.CreateTemp("", "test-expand-*.c1z")
108+
require.NoError(t, err)
109+
tmpPath := tmpFile.Name()
110+
tmpFile.Close()
111+
defer os.Remove(tmpPath)
112+
113+
srcData, err := os.ReadFile(unexpandedPath)
114+
require.NoError(t, err)
115+
err = os.WriteFile(tmpPath, srcData, 0600)
116+
require.NoError(t, err)
117+
118+
// Open the temp file and run expansion
119+
actualC1f, err := dotc1z.NewC1ZFile(ctx, tmpPath)
120+
require.NoError(t, err)
121+
defer actualC1f.Close()
122+
123+
err = actualC1f.SetSyncID(ctx, tc.syncID)
124+
require.NoError(t, err)
125+
126+
// Load graph and run expansion
127+
graph, err := loadEntitlementGraphFromC1Z(ctx, actualC1f)
128+
require.NoError(t, err)
129+
130+
t.Logf("Graph loaded: %d nodes, %d edges", len(graph.Nodes), len(graph.Edges))
131+
132+
expander := NewExpander(actualC1f, graph)
133+
err = expander.Run(ctx)
134+
require.NoError(t, err)
135+
136+
// Open expected file
137+
expectedC1f, err := dotc1z.NewC1ZFile(ctx, expectedPath, dotc1z.WithReadOnly(true))
138+
require.NoError(t, err)
139+
defer expectedC1f.Close()
140+
141+
err = expectedC1f.SetSyncID(ctx, tc.syncID)
142+
require.NoError(t, err)
143+
144+
// Load grants from both
145+
actualGrants, err := loadAllGrants(ctx, actualC1f)
146+
require.NoError(t, err)
147+
148+
expectedGrants, err := loadAllGrants(ctx, expectedC1f)
149+
require.NoError(t, err)
150+
151+
t.Logf("Actual grants: %d, Expected grants: %d", len(actualGrants), len(expectedGrants))
152+
153+
// Compare grant counts
154+
require.Equal(t, len(expectedGrants), len(actualGrants), "grant count mismatch")
155+
156+
// Find missing and extra grants
157+
var missingGrants []string
158+
var extraGrants []string
159+
160+
for key := range expectedGrants {
161+
if _, ok := actualGrants[key]; !ok {
162+
missingGrants = append(missingGrants, key)
163+
}
164+
}
165+
166+
for key := range actualGrants {
167+
if _, ok := expectedGrants[key]; !ok {
168+
extraGrants = append(extraGrants, key)
169+
}
170+
}
171+
172+
sort.Strings(missingGrants)
173+
sort.Strings(extraGrants)
174+
175+
if len(missingGrants) > 0 {
176+
t.Errorf("Missing %d grants (first 10):", len(missingGrants))
177+
for i, key := range missingGrants {
178+
if i >= 10 {
179+
break
180+
}
181+
t.Errorf(" - %s", key)
182+
}
183+
}
184+
185+
if len(extraGrants) > 0 {
186+
t.Errorf("Extra %d grants (first 10):", len(extraGrants))
187+
for i, key := range extraGrants {
188+
if i >= 10 {
189+
break
190+
}
191+
t.Errorf(" - %s", key)
192+
}
193+
}
194+
195+
// Compare sources for matching grants
196+
var sourceMismatches []string
197+
for key, expected := range expectedGrants {
198+
actual, ok := actualGrants[key]
199+
if !ok {
200+
continue // Already reported as missing
201+
}
202+
203+
// Compare sources
204+
if len(expected.Sources) != len(actual.Sources) {
205+
sourceMismatches = append(sourceMismatches,
206+
fmt.Sprintf("%s: expected %d sources, got %d", key, len(expected.Sources), len(actual.Sources)))
207+
continue
208+
}
209+
210+
for sourceID := range expected.Sources {
211+
if !actual.Sources[sourceID] {
212+
sourceMismatches = append(sourceMismatches,
213+
fmt.Sprintf("%s: missing source %s", key, sourceID))
214+
}
215+
}
216+
217+
for sourceID := range actual.Sources {
218+
if !expected.Sources[sourceID] {
219+
sourceMismatches = append(sourceMismatches,
220+
fmt.Sprintf("%s: extra source %s", key, sourceID))
221+
}
222+
}
223+
}
224+
225+
sort.Strings(sourceMismatches)
226+
227+
if len(sourceMismatches) > 0 {
228+
t.Errorf("Source mismatches (%d total, first 20):", len(sourceMismatches))
229+
for i, mismatch := range sourceMismatches {
230+
if i >= 20 {
231+
break
232+
}
233+
t.Errorf(" - %s", mismatch)
234+
}
235+
}
236+
237+
require.Empty(t, missingGrants, "should have no missing grants")
238+
require.Empty(t, extraGrants, "should have no extra grants")
239+
require.Empty(t, sourceMismatches, "should have no source mismatches")
240+
})
241+
}
242+
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)