@@ -56,6 +56,9 @@ type corpusTest struct {
5656//go:embed corpus-tests.tar.gz
5757var corpusArchive []byte
5858
59+ //go:embed corpus-tests-json-schemas.tar.gz
60+ var corpusJSONSchemasArchive []byte
61+
5962type tarFileDataPointer struct {
6063 Position int64
6164 Size int64
@@ -91,39 +94,52 @@ func (fdm TarFileMap) GetFileData(path string) ([]byte, error) {
9194 return content , nil
9295}
9396
94- //nolint:revive // due to test cognitive complexity
95- func TestCorpus (t * testing.T ) {
96- t .Parallel ()
97+ func loadTarGz (t testing.TB , archive []byte ) TarFileMap {
98+ t .Helper ()
9799
98- gzipReader , err := gzip .NewReader (bytes .NewReader (corpusArchive ))
100+ gzipReader , err := gzip .NewReader (bytes .NewReader (archive ))
99101 if err != nil {
100- t .Fatal ("error reading corpus compressed archive header" , err )
102+ t .Fatal ("error reading compressed archive header" , err )
101103 }
102104 defer gzipReader .Close () //nolint:errcheck
103105
104106 buf , err := io .ReadAll (gzipReader )
105107 if err != nil {
106- t .Fatal ("error reading corpus compressed archive" , err )
108+ t .Fatal ("error reading compressed archive" , err )
107109 }
108110
109111 bufReader := bytes .NewReader (buf )
110112 archiveReader := tar .NewReader (bufReader )
111113
112114 fdm := NewTarFileMap (bufReader )
113- var testFiles []string
114115 for file , err := archiveReader .Next (); err == nil ; file , err = archiveReader .Next () {
115116 if file .Typeflag != tar .TypeReg {
116117 continue
117118 }
118119
119120 cursor , _ := bufReader .Seek (0 , io .SeekCurrent )
120121 fdm .AddFileData (file .Name , cursor , file .Size )
122+ }
123+
124+ return fdm
125+ }
121126
122- if strings .HasSuffix (file .Name , ".json" ) && ! strings .HasSuffix (file .Name , ".entities.json" ) {
123- testFiles = append (testFiles , file .Name )
127+ //nolint:revive // due to test cognitive complexity
128+ func TestCorpus (t * testing.T ) {
129+ t .Parallel ()
130+
131+ // Load corpus test files
132+ fdm := loadTarGz (t , corpusArchive )
133+ var testFiles []string
134+ for fileName := range fdm .files {
135+ if strings .HasSuffix (fileName , ".json" ) && ! strings .HasSuffix (fileName , ".entities.json" ) {
136+ testFiles = append (testFiles , fileName )
124137 }
125138 }
126139
140+ // Load JSON schemas for validation
141+ jsonSchemasFdm := loadTarGz (t , corpusJSONSchemasArchive )
142+
127143 for _ , testFile := range testFiles {
128144 testFile := testFile
129145 t .Run (testFile , func (t * testing.T ) {
@@ -152,12 +168,70 @@ func TestCorpus(t *testing.T) {
152168 if err != nil {
153169 t .Fatal ("error reading schema content" , err )
154170 }
171+ // Rust converted JSON never contains the empty context record
172+ schemaContent = bytes .ReplaceAll (schemaContent , []byte ("context: {}\n " ), nil )
173+
155174 var s schema.Schema
156175 s .SetFilename ("test.schema" )
157176 if err := s .UnmarshalCedar (schemaContent ); err != nil {
158177 t .Fatal ("error parsing schema" , err , "\n ===\n " , string (schemaContent ))
159178 }
160179
180+ // Validate schema round-trip
181+ t .Run ("schema-round-trip" , func (t * testing.T ) {
182+ t .Parallel ()
183+
184+ js , err := s .MarshalJSON ()
185+ testutil .OK (t , err )
186+
187+ var s2 schema.Schema
188+ err = s2 .UnmarshalJSON (js )
189+ testutil .OK (t , err )
190+
191+ sb , err := s2 .MarshalCedar ()
192+ testutil .OK (t , err )
193+
194+ var s3 schema.Schema
195+ err = s3 .UnmarshalCedar (sb )
196+ testutil .OK (t , err )
197+
198+ j2 , err := s3 .MarshalJSON ()
199+ testutil .OK (t , err )
200+
201+ testutil .Equals (t , string (j2 ), string (js ))
202+ })
203+
204+ // Validate schema matches Rust Cedar CLI output
205+ t .Run ("schema-vs-rust" , func (t * testing.T ) {
206+ t .Parallel ()
207+
208+ // Extract schema filename from path (e.g., "corpus-tests/abc123.cedarschema" -> "abc123")
209+ schemaFilename := strings .TrimSuffix (strings .TrimPrefix (tt .Schema , "corpus-tests/" ), ".cedarschema" )
210+ jsonSchemaPath := fmt .Sprintf ("corpus-tests-json-schemas/%s.cedarschema.json" , schemaFilename )
211+
212+ rustJSON , err := jsonSchemasFdm .GetFileData (jsonSchemaPath )
213+ testutil .OK (t , err )
214+
215+ // Normalize Rust JSON: appliesTo is optional - match testdata_test.go pattern
216+ // Need to handle trailing comma to avoid creating invalid JSON like {,"other":...}
217+ rustJSON = bytes .ReplaceAll (rustJSON , []byte (`"appliesTo":{"resourceTypes":[],"principalTypes":[]},` ), nil )
218+ rustJSON = bytes .ReplaceAll (rustJSON , []byte (`"appliesTo":{"resourceTypes":[],"principalTypes":[]}` ), nil )
219+
220+ // Unmarshal Rust JSON to handle any syntax issues from replacement
221+ var rustSchema schema.Schema
222+ err = rustSchema .UnmarshalJSON (rustJSON )
223+ testutil .OK (t , err )
224+
225+ // Marshal both schemas to JSON for comparison
226+ goJSON , err := s .MarshalJSON ()
227+ testutil .OK (t , err )
228+ rustJSON2 , err := rustSchema .MarshalJSON ()
229+ testutil .OK (t , err )
230+
231+ // Normalize and compare
232+ stringEquals (t , string (normalizeJSON (t , goJSON )), string (normalizeJSON (t , rustJSON2 )))
233+ })
234+
161235 policyContent , err := fdm .GetFileData (tt .Policies )
162236 if err != nil {
163237 t .Fatal ("error reading policy content" , err )
@@ -250,6 +324,21 @@ func TestCorpus(t *testing.T) {
250324 }
251325}
252326
327+ func normalizeJSON (t * testing.T , in []byte ) []byte {
328+ t .Helper ()
329+ var out any
330+ err := json .Unmarshal (in , & out )
331+ testutil .OK (t , err )
332+ b , err := json .MarshalIndent (out , "" , " " )
333+ testutil .OK (t , err )
334+ return b
335+ }
336+
337+ func stringEquals (t * testing.T , got , want string ) {
338+ t .Helper ()
339+ testutil .Equals (t , strings .TrimSpace (got ), strings .TrimSpace (want ))
340+ }
341+
253342// Specific corpus tests that have been extracted for easy regression testing purposes
254343func TestCorpusRelated (t * testing.T ) {
255344 t .Parallel ()
0 commit comments