@@ -93,8 +93,15 @@ definition group {
9393 require .NotEmpty (t , tracker .visitedRelations , "Should have visited at least one relation" )
9494 require .Contains (t , tracker .visitedRelations , "group#member" , "Should have visited the member relation" )
9595
96+ // Verify each relation/permission is visited exactly once (proper cycle detection)
97+ memberCount := countOccurrences (tracker .visitedRelations , "group#member" )
98+ isMemberCount := countOccurrences (tracker .visitedPermissions , "group#is_member" )
99+
100+ require .Equal (t , 1 , memberCount , "group#member should be visited exactly once" )
101+ require .Equal (t , 1 , isMemberCount , "group#is_member should be visited exactly once" )
102+
96103 // Check that we didn't get stuck in infinite recursion
97- require .Equal (t , 2 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
104+ require .Equal (t , 1 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
98105}
99106
100107// TestWalkPostOrder_MutuallyRecursivePermissions tests that the walker can handle
@@ -142,18 +149,17 @@ definition document {
142149 require .True (t , containsPermission (tracker .visitedPermissions , "view" ), "Should have visited view permission" )
143150 require .True (t , containsPermission (tracker .visitedPermissions , "edit" ), "Should have visited edit permission" )
144151
145- // Check depth - this will reveal if there's cycle detection or if it walks the cycle multiple times
146- require .Equal (t , 4 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
152+ // Verify each permission is visited exactly once (proper cycle detection)
153+ ownerCount := countOccurrences (tracker .visitedRelations , "document#owner" )
154+ viewCount := countOccurrences (tracker .visitedPermissions , "document#view" )
155+ editCount := countOccurrences (tracker .visitedPermissions , "document#edit" )
147156
148- // Count how many times each permission was visited
149- viewCount := countOccurrences ( tracker . visitedPermissions , "view" )
150- editCount := countOccurrences ( tracker . visitedPermissions , "edit" )
157+ require . Equal ( t , 1 , ownerCount , "document#owner should be visited exactly once" )
158+ require . Equal ( t , 1 , viewCount , "document# view should be visited exactly once " )
159+ require . Equal ( t , 1 , editCount , "document# edit should be visited exactly once " )
151160
152- // If there's proper cycle detection, each should be visited once
153- // If not, they might be visited multiple times (but should still terminate)
154- // The key is that it should NOT infinite loop
155- require .Positive (t , viewCount , "view should be visited at least once" )
156- require .Positive (t , editCount , "edit should be visited at least once" )
161+ // Check depth - this will reveal if there's cycle detection or if it walks the cycle multiple times
162+ require .Equal (t , 2 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
157163}
158164
159165// TestWalkPreOrder_RecursiveGroupRelation tests pre-order with recursive group relation
@@ -321,12 +327,20 @@ definition document {
321327 // Verify no infinite recursion
322328 require .Less (t , 2 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
323329
324- // The arrow cycle detection should prevent folder#view from being visited multiple times
325- // when following the parent->view arrow
326- viewCount := countOccurrences (tracker .visitedPermissions , "folder#view" )
327-
328- // With proper cycle detection, it should be visited a reasonable number of times
329- require .Equal (t , 2 , viewCount , "folder#view should not be visited excessive times due to cycle detection" )
330+ // Verify each relation/permission is visited exactly once (proper cycle detection)
331+ folderParentCount := countOccurrences (tracker .visitedRelations , "folder#parent" )
332+ folderViewerCount := countOccurrences (tracker .visitedRelations , "folder#viewer" )
333+ folderViewCount := countOccurrences (tracker .visitedPermissions , "folder#view" )
334+ documentParentCount := countOccurrences (tracker .visitedRelations , "document#parent" )
335+ documentOwnerCount := countOccurrences (tracker .visitedRelations , "document#owner" )
336+ documentViewCount := countOccurrences (tracker .visitedPermissions , "document#view" )
337+
338+ require .Equal (t , 1 , folderParentCount , "folder#parent should be visited exactly once" )
339+ require .Equal (t , 1 , folderViewerCount , "folder#viewer should be visited exactly once" )
340+ require .Equal (t , 1 , folderViewCount , "folder#view should be visited exactly once" )
341+ require .Equal (t , 1 , documentParentCount , "document#parent should be visited exactly once" )
342+ require .Equal (t , 1 , documentOwnerCount , "document#owner should be visited exactly once" )
343+ require .Equal (t , 1 , documentViewCount , "document#view should be visited exactly once" )
330344}
331345
332346// TestWalkPostOrder_DeeplyNestedRecursion tests a more complex scenario with multiple levels
@@ -380,8 +394,79 @@ definition resource {
380394 require .True (t , containsPermission (tracker .visitedPermissions , "edit" ), "Should have visited edit" )
381395 require .True (t , containsPermission (tracker .visitedPermissions , "delete" ), "Should have visited delete" )
382396
397+ // Verify each permission is visited at most once (proper cycle detection)
398+ viewCount := countOccurrences (tracker .visitedPermissions , "resource#view" )
399+ editCount := countOccurrences (tracker .visitedPermissions , "resource#edit" )
400+ deleteCount := countOccurrences (tracker .visitedPermissions , "resource#delete" )
401+
402+ require .Equal (t , 1 , viewCount , "resource#view should be visited exactly once" )
403+ require .Equal (t , 1 , editCount , "resource#edit should be visited exactly once" )
404+ require .Equal (t , 1 , deleteCount , "resource#delete should be visited exactly once" )
405+
383406 // Check for reasonable depth
384- require .Equal (t , 9 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
407+ require .Equal (t , 5 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
408+ }
409+
410+ // TestWalkPostOrder_RecursiveResourceWithTeamMembers tests a schema with both
411+ // recursive relation non-terminal types and recursive permission on the same resource
412+ func TestWalkPostOrder_RecursiveRelationAndPermission (t * testing.T ) {
413+ schemaString := `definition user {}
414+
415+ definition team {
416+ relation members: user | team#members
417+ }
418+
419+ definition resource {
420+ relation parent: resource
421+ relation reader: user | team#members
422+
423+ permission read = reader + parent->read
424+ }`
425+
426+ // Compile the schema
427+ compiled , err := compiler .Compile (compiler.InputSchema {
428+ Source : input .Source ("test" ),
429+ SchemaString : schemaString ,
430+ }, compiler .AllowUnprefixedObjectType ())
431+ require .NoError (t , err )
432+
433+ // Convert to *Schema
434+ schema , err := BuildSchemaFromCompiledSchema (* compiled )
435+ require .NoError (t , err )
436+ require .NotNil (t , schema )
437+
438+ // Resolve the schema (required for arrow traversal)
439+ resolved , err := ResolveSchema (schema )
440+ require .NoError (t , err )
441+ require .NotNil (t , resolved )
442+
443+ // Track what gets visited
444+ tracker := & recursionTracker {}
445+
446+ // Walk with post-order traversal with arrow traversal
447+ opts := NewWalkOptions ().
448+ WithStrategy (WalkPostOrder ).
449+ WithTraverseArrowTargets (resolved .Schema ()).MustBuild ()
450+
451+ _ , err = WalkSchemaWithOptions (resolved .Schema (), tracker , 0 , opts )
452+ require .NoError (t , err )
453+
454+ // The walker should have completed without infinite recursion
455+ require .NotEmpty (t , tracker .visitedPermissions , "Should have visited at least one permission" )
456+ require .NotEmpty (t , tracker .visitedRelations , "Should have visited at least one relation" )
457+
458+ teamMembersCount := countOccurrences (tracker .visitedRelations , "team#members" )
459+ resourceReaderCount := countOccurrences (tracker .visitedRelations , "resource#reader" )
460+ resourceParentCount := countOccurrences (tracker .visitedRelations , "resource#parent" )
461+ resourceReadCount := countOccurrences (tracker .visitedPermissions , "resource#read" )
462+
463+ require .Equal (t , 1 , teamMembersCount , "team#members should be visited exactly once" )
464+ require .Equal (t , 1 , resourceReaderCount , "resource#reader should be visited exactly once" )
465+ require .Equal (t , 1 , resourceParentCount , "resource#parent should be visited exactly once" )
466+ require .Equal (t , 1 , resourceReadCount , "resource#read should be visited exactly once" )
467+
468+ // Check for reasonable depth (should not indicate infinite recursion)
469+ require .Equal (t , 3 , tracker .maxDepth , "Depth should not indicate infinite recursion" )
385470}
386471
387472// Helper functions
0 commit comments