github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/watch_test.go (about) 1 package v1_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "sort" 9 "strings" 10 "testing" 11 "time" 12 13 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 14 "github.com/authzed/grpcutil" 15 "github.com/stretchr/testify/require" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/grpc/status" 18 19 "github.com/authzed/spicedb/internal/datastore/memdb" 20 "github.com/authzed/spicedb/internal/testfixtures" 21 "github.com/authzed/spicedb/internal/testserver" 22 "github.com/authzed/spicedb/pkg/tuple" 23 "github.com/authzed/spicedb/pkg/zedtoken" 24 ) 25 26 func update( 27 op v1.RelationshipUpdate_Operation, 28 resourceObjType, 29 resourceObjID, 30 relation, 31 subObjType, 32 subObjectID string, 33 ) *v1.RelationshipUpdate { 34 return &v1.RelationshipUpdate{ 35 Operation: op, 36 Relationship: &v1.Relationship{ 37 Resource: &v1.ObjectReference{ 38 ObjectType: resourceObjType, 39 ObjectId: resourceObjID, 40 }, 41 Relation: relation, 42 Subject: &v1.SubjectReference{ 43 Object: &v1.ObjectReference{ 44 ObjectType: subObjType, 45 ObjectId: subObjectID, 46 }, 47 }, 48 }, 49 } 50 } 51 52 func TestWatch(t *testing.T) { 53 testCases := []struct { 54 name string 55 objectTypesFilter []string 56 relationshipFilters []*v1.RelationshipFilter 57 startCursor *v1.ZedToken 58 mutations []*v1.RelationshipUpdate 59 expectedCode codes.Code 60 expectedUpdates []*v1.RelationshipUpdate 61 }{ 62 { 63 name: "unfiltered watch", 64 expectedCode: codes.OK, 65 mutations: []*v1.RelationshipUpdate{ 66 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 67 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 68 update(v1.RelationshipUpdate_OPERATION_TOUCH, "folder", "folder2", "viewer", "user", "user1"), 69 }, 70 expectedUpdates: []*v1.RelationshipUpdate{ 71 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document1", "viewer", "user", "user1"), 72 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 73 update(v1.RelationshipUpdate_OPERATION_TOUCH, "folder", "folder2", "viewer", "user", "user1"), 74 }, 75 }, 76 { 77 name: "watch with objectType filter", 78 expectedCode: codes.OK, 79 objectTypesFilter: []string{"document"}, 80 mutations: []*v1.RelationshipUpdate{ 81 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 82 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 83 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 84 }, 85 expectedUpdates: []*v1.RelationshipUpdate{ 86 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document1", "viewer", "user", "user1"), 87 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 88 }, 89 }, 90 { 91 name: "watch with relationship filters", 92 expectedCode: codes.OK, 93 relationshipFilters: []*v1.RelationshipFilter{ 94 { 95 ResourceType: "document", 96 }, 97 { 98 OptionalResourceIdPrefix: "d", 99 }, 100 }, 101 mutations: []*v1.RelationshipUpdate{ 102 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 103 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 104 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 105 }, 106 expectedUpdates: []*v1.RelationshipUpdate{ 107 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document1", "viewer", "user", "user1"), 108 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 109 }, 110 }, 111 { 112 name: "watch with modified relationship filters", 113 expectedCode: codes.OK, 114 relationshipFilters: []*v1.RelationshipFilter{ 115 { 116 ResourceType: "folder", 117 }, 118 }, 119 mutations: []*v1.RelationshipUpdate{ 120 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 121 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 122 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 123 }, 124 expectedUpdates: []*v1.RelationshipUpdate{ 125 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 126 }, 127 }, 128 { 129 name: "watch with resource ID prefix", 130 expectedCode: codes.OK, 131 relationshipFilters: []*v1.RelationshipFilter{ 132 { 133 OptionalResourceIdPrefix: "document1", 134 }, 135 }, 136 mutations: []*v1.RelationshipUpdate{ 137 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 138 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 139 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 140 }, 141 expectedUpdates: []*v1.RelationshipUpdate{ 142 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document1", "viewer", "user", "user1"), 143 }, 144 }, 145 { 146 name: "watch with shorter resource ID prefix", 147 expectedCode: codes.OK, 148 relationshipFilters: []*v1.RelationshipFilter{ 149 { 150 OptionalResourceIdPrefix: "doc", 151 }, 152 }, 153 mutations: []*v1.RelationshipUpdate{ 154 update(v1.RelationshipUpdate_OPERATION_CREATE, "document", "document1", "viewer", "user", "user1"), 155 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 156 update(v1.RelationshipUpdate_OPERATION_DELETE, "folder", "auditors", "viewer", "user", "auditor"), 157 }, 158 expectedUpdates: []*v1.RelationshipUpdate{ 159 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document1", "viewer", "user", "user1"), 160 update(v1.RelationshipUpdate_OPERATION_TOUCH, "document", "document2", "viewer", "user", "user1"), 161 }, 162 }, 163 { 164 name: "invalid zedtoken", 165 startCursor: &v1.ZedToken{Token: "bad-token"}, 166 expectedCode: codes.InvalidArgument, 167 }, 168 { 169 name: "empty zedtoken fails validation", 170 startCursor: &v1.ZedToken{Token: ""}, 171 expectedCode: codes.InvalidArgument, 172 }, 173 { 174 name: "watch with both kinds of filters", 175 relationshipFilters: []*v1.RelationshipFilter{ 176 { 177 OptionalResourceIdPrefix: "doc", 178 }, 179 }, 180 objectTypesFilter: []string{"document"}, 181 expectedCode: codes.InvalidArgument, 182 }, 183 { 184 name: "watch with both fields of filter", 185 relationshipFilters: []*v1.RelationshipFilter{ 186 { 187 OptionalResourceIdPrefix: "doc", 188 OptionalResourceId: "document1", 189 }, 190 }, 191 expectedCode: codes.InvalidArgument, 192 }, 193 { 194 name: "watch with invalid filter resource type", 195 relationshipFilters: []*v1.RelationshipFilter{ 196 { 197 ResourceType: "invalid", 198 }, 199 }, 200 expectedCode: codes.FailedPrecondition, 201 }, 202 } 203 204 for _, tc := range testCases { 205 tc := tc 206 t.Run(tc.name, func(t *testing.T) { 207 require := require.New(t) 208 209 conn, cleanup, _, revision := testserver.NewTestServer(require, 0, memdb.DisableGC, true, testfixtures.StandardDatastoreWithData) 210 t.Cleanup(cleanup) 211 client := v1.NewWatchServiceClient(conn) 212 213 cursor := zedtoken.MustNewFromRevision(revision) 214 if tc.startCursor != nil { 215 cursor = tc.startCursor 216 } 217 218 ctx, cancel := context.WithCancel(context.Background()) 219 defer cancel() 220 221 stream, err := client.Watch(ctx, &v1.WatchRequest{ 222 OptionalObjectTypes: tc.objectTypesFilter, 223 OptionalRelationshipFilters: tc.relationshipFilters, 224 OptionalStartCursor: cursor, 225 }) 226 require.NoError(err) 227 228 if tc.expectedCode == codes.OK { 229 updatesChan := make(chan []*v1.RelationshipUpdate, len(tc.mutations)) 230 231 go func() { 232 defer close(updatesChan) 233 234 for { 235 select { 236 case <-ctx.Done(): 237 return 238 case <-time.After(3 * time.Second): 239 panic(fmt.Errorf("timed out waiting for stream updates")) 240 default: 241 resp, err := stream.Recv() 242 if err != nil { 243 errStatus, ok := status.FromError(err) 244 if (ok && (errStatus.Code() == codes.Canceled || errStatus.Code() == codes.Unavailable)) || errors.Is(err, io.EOF) { 245 break 246 } 247 248 panic(fmt.Errorf("received a stream read error: %w", err)) 249 } 250 251 updatesChan <- resp.Updates 252 } 253 } 254 }() 255 256 _, err := v1.NewPermissionsServiceClient(conn).WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 257 Updates: tc.mutations, 258 }) 259 require.NoError(err) 260 261 var receivedUpdates []*v1.RelationshipUpdate 262 263 for len(receivedUpdates) < len(tc.expectedUpdates) { 264 select { 265 case updates := <-updatesChan: 266 receivedUpdates = append(receivedUpdates, updates...) 267 case <-time.After(1 * time.Second): 268 require.FailNow("timed out waiting for updates") 269 return 270 } 271 } 272 273 require.Equal(sortUpdates(tc.expectedUpdates), sortUpdates(receivedUpdates)) 274 } else { 275 _, err := stream.Recv() 276 grpcutil.RequireStatus(t, tc.expectedCode, err) 277 } 278 }) 279 } 280 } 281 282 func sortUpdates(in []*v1.RelationshipUpdate) []*v1.RelationshipUpdate { 283 out := make([]*v1.RelationshipUpdate, 0, len(in)) 284 out = append(out, in...) 285 sort.Slice(out, func(i, j int) bool { 286 left, right := out[i], out[j] 287 compareResult := strings.Compare(tuple.MustRelString(left.Relationship), tuple.MustRelString(right.Relationship)) 288 if compareResult < 0 { 289 return true 290 } 291 if compareResult > 0 { 292 return false 293 } 294 295 return left.Operation < right.Operation 296 }) 297 298 return out 299 }