A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at codeberg-source 485 lines 12 kB view raw
1package auth 2 3import ( 4 "strings" 5 "testing" 6) 7 8func TestParseScope_Valid(t *testing.T) { 9 tests := []struct { 10 name string 11 scopes []string 12 expectedCount int 13 expectedType string 14 expectedName string 15 expectedActions []string 16 }{ 17 { 18 name: "repository with actions", 19 scopes: []string{"repository:alice/myapp:pull,push"}, 20 expectedCount: 1, 21 expectedType: "repository", 22 expectedName: "alice/myapp", 23 expectedActions: []string{"pull", "push"}, 24 }, 25 { 26 name: "repository without actions", 27 scopes: []string{"repository:alice/myapp"}, 28 expectedCount: 1, 29 expectedType: "repository", 30 expectedName: "alice/myapp", 31 expectedActions: nil, 32 }, 33 { 34 name: "wildcard repository", 35 scopes: []string{"repository:*:pull,push"}, 36 expectedCount: 1, 37 expectedType: "repository", 38 expectedName: "*", 39 expectedActions: []string{"pull", "push"}, 40 }, 41 { 42 name: "empty scope ignored", 43 scopes: []string{""}, 44 expectedCount: 0, 45 }, 46 { 47 name: "multiple scopes", 48 scopes: []string{"repository:alice/app1:pull", "repository:alice/app2:push"}, 49 expectedCount: 2, 50 expectedType: "repository", 51 expectedName: "alice/app1", 52 expectedActions: []string{"pull"}, 53 }, 54 { 55 name: "single action", 56 scopes: []string{"repository:alice/myapp:pull"}, 57 expectedCount: 1, 58 expectedType: "repository", 59 expectedName: "alice/myapp", 60 expectedActions: []string{"pull"}, 61 }, 62 { 63 name: "three actions", 64 scopes: []string{"repository:alice/myapp:pull,push,delete"}, 65 expectedCount: 1, 66 expectedType: "repository", 67 expectedName: "alice/myapp", 68 expectedActions: []string{"pull", "push", "delete"}, 69 }, 70 // Note: DIDs with colons cannot be used directly in scope strings due to 71 // the colon delimiter. This is a known limitation. 72 { 73 name: "empty actions string", 74 scopes: []string{"repository:alice/myapp:"}, 75 expectedCount: 1, 76 expectedType: "repository", 77 expectedName: "alice/myapp", 78 expectedActions: nil, 79 }, 80 } 81 82 for _, tt := range tests { 83 t.Run(tt.name, func(t *testing.T) { 84 access, err := ParseScope(tt.scopes) 85 if err != nil { 86 t.Fatalf("ParseScope() error = %v", err) 87 } 88 89 if len(access) != tt.expectedCount { 90 t.Errorf("Expected %d access entries, got %d", tt.expectedCount, len(access)) 91 return 92 } 93 94 if tt.expectedCount > 0 { 95 entry := access[0] 96 if entry.Type != tt.expectedType { 97 t.Errorf("Expected type %q, got %q", tt.expectedType, entry.Type) 98 } 99 if entry.Name != tt.expectedName { 100 t.Errorf("Expected name %q, got %q", tt.expectedName, entry.Name) 101 } 102 if len(entry.Actions) != len(tt.expectedActions) { 103 t.Errorf("Expected %d actions, got %d", len(tt.expectedActions), len(entry.Actions)) 104 } 105 for i, expectedAction := range tt.expectedActions { 106 if i < len(entry.Actions) && entry.Actions[i] != expectedAction { 107 t.Errorf("Expected action[%d] = %q, got %q", i, expectedAction, entry.Actions[i]) 108 } 109 } 110 } 111 }) 112 } 113} 114 115func TestParseScope_Invalid(t *testing.T) { 116 tests := []struct { 117 name string 118 scopes []string 119 }{ 120 { 121 name: "missing colon", 122 scopes: []string{"repository"}, 123 }, 124 { 125 name: "too many parts", 126 scopes: []string{"repository:name:actions:extra"}, 127 }, 128 { 129 name: "single part only", 130 scopes: []string{"invalid"}, 131 }, 132 { 133 name: "four colons", 134 scopes: []string{"a:b:c:d:e"}, 135 }, 136 } 137 138 for _, tt := range tests { 139 t.Run(tt.name, func(t *testing.T) { 140 _, err := ParseScope(tt.scopes) 141 if err == nil { 142 t.Error("Expected error for invalid scope format") 143 } 144 if !strings.Contains(err.Error(), "invalid scope") { 145 t.Errorf("Expected error message to contain 'invalid scope', got: %v", err) 146 } 147 }) 148 } 149} 150 151func TestParseScope_SpecialCharacters(t *testing.T) { 152 tests := []struct { 153 name string 154 scope string 155 expectedName string 156 }{ 157 { 158 name: "hyphen in name", 159 scope: "repository:alice-bob/my-app:pull", 160 expectedName: "alice-bob/my-app", 161 }, 162 { 163 name: "underscore in name", 164 scope: "repository:alice_bob/my_app:pull", 165 expectedName: "alice_bob/my_app", 166 }, 167 { 168 name: "dot in name", 169 scope: "repository:alice.bsky.social/myapp:pull", 170 expectedName: "alice.bsky.social/myapp", 171 }, 172 { 173 name: "numbers in name", 174 scope: "repository:user123/app456:pull", 175 expectedName: "user123/app456", 176 }, 177 } 178 179 for _, tt := range tests { 180 t.Run(tt.name, func(t *testing.T) { 181 access, err := ParseScope([]string{tt.scope}) 182 if err != nil { 183 t.Fatalf("ParseScope() error = %v", err) 184 } 185 186 if len(access) != 1 { 187 t.Fatalf("Expected 1 access entry, got %d", len(access)) 188 } 189 190 if access[0].Name != tt.expectedName { 191 t.Errorf("Expected name %q, got %q", tt.expectedName, access[0].Name) 192 } 193 }) 194 } 195} 196 197func TestParseScope_MultipleScopes(t *testing.T) { 198 scopes := []string{ 199 "repository:alice/app1:pull", 200 "repository:alice/app2:push", 201 "repository:bob/app3:pull,push", 202 } 203 204 access, err := ParseScope(scopes) 205 if err != nil { 206 t.Fatalf("ParseScope() error = %v", err) 207 } 208 209 if len(access) != 3 { 210 t.Fatalf("Expected 3 access entries, got %d", len(access)) 211 } 212 213 // Verify first entry 214 if access[0].Name != "alice/app1" { 215 t.Errorf("Expected first name %q, got %q", "alice/app1", access[0].Name) 216 } 217 if len(access[0].Actions) != 1 || access[0].Actions[0] != "pull" { 218 t.Errorf("Expected first actions [pull], got %v", access[0].Actions) 219 } 220 221 // Verify second entry 222 if access[1].Name != "alice/app2" { 223 t.Errorf("Expected second name %q, got %q", "alice/app2", access[1].Name) 224 } 225 if len(access[1].Actions) != 1 || access[1].Actions[0] != "push" { 226 t.Errorf("Expected second actions [push], got %v", access[1].Actions) 227 } 228 229 // Verify third entry 230 if access[2].Name != "bob/app3" { 231 t.Errorf("Expected third name %q, got %q", "bob/app3", access[2].Name) 232 } 233 if len(access[2].Actions) != 2 { 234 t.Errorf("Expected third entry to have 2 actions, got %d", len(access[2].Actions)) 235 } 236} 237 238func TestValidateAccess_Owner(t *testing.T) { 239 userDID := "did:plc:alice123" 240 userHandle := "alice.bsky.social" 241 242 tests := []struct { 243 name string 244 repoName string 245 actions []string 246 shouldErr bool 247 errorMsg string 248 }{ 249 { 250 name: "owner can push to own repo (by handle)", 251 repoName: "alice.bsky.social/myapp", 252 actions: []string{"push"}, 253 shouldErr: false, 254 }, 255 { 256 name: "owner can push to own repo (by DID)", 257 repoName: "did:plc:alice123/myapp", 258 actions: []string{"push"}, 259 shouldErr: false, 260 }, 261 { 262 name: "owner cannot push to others repo", 263 repoName: "bob.bsky.social/myapp", 264 actions: []string{"push"}, 265 shouldErr: true, 266 errorMsg: "cannot push", 267 }, 268 { 269 name: "wildcard scope allowed", 270 repoName: "*", 271 actions: []string{"push", "pull"}, 272 shouldErr: false, 273 }, 274 { 275 name: "owner can pull from others repo", 276 repoName: "bob.bsky.social/myapp", 277 actions: []string{"pull"}, 278 shouldErr: false, 279 }, 280 { 281 name: "owner cannot delete others repo", 282 repoName: "bob.bsky.social/myapp", 283 actions: []string{"delete"}, 284 shouldErr: true, 285 errorMsg: "cannot delete", 286 }, 287 { 288 name: "multiple actions with push fails for others", 289 repoName: "bob.bsky.social/myapp", 290 actions: []string{"pull", "push"}, 291 shouldErr: true, 292 }, 293 { 294 name: "empty repository name", 295 repoName: "", 296 actions: []string{"push"}, 297 shouldErr: true, 298 }, 299 } 300 301 for _, tt := range tests { 302 t.Run(tt.name, func(t *testing.T) { 303 access := []AccessEntry{ 304 { 305 Type: "repository", 306 Name: tt.repoName, 307 Actions: tt.actions, 308 }, 309 } 310 311 err := ValidateAccess(userDID, userHandle, access) 312 if tt.shouldErr && err == nil { 313 t.Error("Expected error but got none") 314 } 315 if !tt.shouldErr && err != nil { 316 t.Errorf("Expected no error but got: %v", err) 317 } 318 if tt.shouldErr && err != nil && tt.errorMsg != "" { 319 if !strings.Contains(err.Error(), tt.errorMsg) { 320 t.Errorf("Expected error to contain %q, got: %v", tt.errorMsg, err) 321 } 322 } 323 }) 324 } 325} 326 327func TestValidateAccess_NonRepositoryType(t *testing.T) { 328 userDID := "did:plc:alice123" 329 userHandle := "alice.bsky.social" 330 331 // Non-repository types should be ignored 332 access := []AccessEntry{ 333 { 334 Type: "registry", 335 Name: "something", 336 Actions: []string{"admin"}, 337 }, 338 } 339 340 err := ValidateAccess(userDID, userHandle, access) 341 if err != nil { 342 t.Errorf("Expected non-repository types to be ignored, got error: %v", err) 343 } 344} 345 346func TestValidateAccess_EmptyAccess(t *testing.T) { 347 userDID := "did:plc:alice123" 348 userHandle := "alice.bsky.social" 349 350 err := ValidateAccess(userDID, userHandle, nil) 351 if err != nil { 352 t.Errorf("Expected no error for empty access, got: %v", err) 353 } 354 355 err = ValidateAccess(userDID, userHandle, []AccessEntry{}) 356 if err != nil { 357 t.Errorf("Expected no error for empty access slice, got: %v", err) 358 } 359} 360 361func TestValidateAccess_InvalidRepositoryName(t *testing.T) { 362 userDID := "did:plc:alice123" 363 userHandle := "alice.bsky.social" 364 365 // Repository name without slash - invalid format 366 access := []AccessEntry{ 367 { 368 Type: "repository", 369 Name: "justareponame", 370 Actions: []string{"push"}, 371 }, 372 } 373 374 err := ValidateAccess(userDID, userHandle, access) 375 if err != nil { 376 // Should fail because can't extract owner from name without slash 377 // and it's not "*", so it will try to access [0] which is the whole string 378 // This is expected behavior - validate that owner check happens 379 t.Logf("Got expected validation error: %v", err) 380 } 381} 382 383func TestValidateAccess_DIDAndHandleBothWork(t *testing.T) { 384 userDID := "did:plc:alice123" 385 userHandle := "alice.bsky.social" 386 387 // Test with handle as owner 388 accessByHandle := []AccessEntry{ 389 { 390 Type: "repository", 391 Name: "alice.bsky.social/myapp", 392 Actions: []string{"push"}, 393 }, 394 } 395 396 err := ValidateAccess(userDID, userHandle, accessByHandle) 397 if err != nil { 398 t.Errorf("Expected no error for handle match, got: %v", err) 399 } 400 401 // Test with DID as owner 402 accessByDID := []AccessEntry{ 403 { 404 Type: "repository", 405 Name: "did:plc:alice123/myapp", 406 Actions: []string{"push"}, 407 }, 408 } 409 410 err = ValidateAccess(userDID, userHandle, accessByDID) 411 if err != nil { 412 t.Errorf("Expected no error for DID match, got: %v", err) 413 } 414} 415 416func TestValidateAccess_MixedActionsAndOwnership(t *testing.T) { 417 userDID := "did:plc:alice123" 418 userHandle := "alice.bsky.social" 419 420 // Mix of own and others' repositories 421 access := []AccessEntry{ 422 { 423 Type: "repository", 424 Name: "alice.bsky.social/myapp", 425 Actions: []string{"push", "pull"}, 426 }, 427 { 428 Type: "repository", 429 Name: "bob.bsky.social/bobapp", 430 Actions: []string{"pull"}, // OK - just pull 431 }, 432 } 433 434 err := ValidateAccess(userDID, userHandle, access) 435 if err != nil { 436 t.Errorf("Expected no error for valid mixed access, got: %v", err) 437 } 438 439 // Now add push to someone else's repo - should fail 440 access = []AccessEntry{ 441 { 442 Type: "repository", 443 Name: "alice.bsky.social/myapp", 444 Actions: []string{"push"}, 445 }, 446 { 447 Type: "repository", 448 Name: "bob.bsky.social/bobapp", 449 Actions: []string{"push"}, // FAIL - can't push to others 450 }, 451 } 452 453 err = ValidateAccess(userDID, userHandle, access) 454 if err == nil { 455 t.Error("Expected error when trying to push to others' repository") 456 } 457} 458 459func TestParseScope_EmptyActionsArray(t *testing.T) { 460 // Test with empty actions (colon present but no actions after it) 461 access, err := ParseScope([]string{"repository:alice/myapp:"}) 462 if err != nil { 463 t.Fatalf("ParseScope() error = %v", err) 464 } 465 466 if len(access) != 1 { 467 t.Fatalf("Expected 1 entry, got %d", len(access)) 468 } 469 470 // Actions should be nil or empty when actions string is empty 471 if len(access[0].Actions) > 0 { 472 t.Errorf("Expected nil or empty actions, got %v", access[0].Actions) 473 } 474} 475 476func TestParseScope_NilInput(t *testing.T) { 477 access, err := ParseScope(nil) 478 if err != nil { 479 t.Fatalf("ParseScope() with nil input error = %v", err) 480 } 481 482 if len(access) != 0 { 483 t.Errorf("Expected empty access for nil input, got %d entries", len(access)) 484 } 485}