A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
72
fork

Configure Feed

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

fix ListCrewMembers

+206 -3
+7 -1
pkg/appview/storage/crew.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "io" 6 7 "log/slog" 7 8 "net/http" 8 9 ··· 75 76 defer resp.Body.Close() 76 77 77 78 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { 78 - return fmt.Errorf("requestCrew failed with status %d", resp.StatusCode) 79 + // Read response body to capture actual error message from hold 80 + body, readErr := io.ReadAll(resp.Body) 81 + if readErr != nil { 82 + return fmt.Errorf("requestCrew failed with status %d (failed to read error body: %w)", resp.StatusCode, readErr) 83 + } 84 + return fmt.Errorf("requestCrew failed with status %d: %s", resp.StatusCode, string(body)) 79 85 } 80 86 81 87 return nil
+4 -2
pkg/hold/pds/crew.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 6 7 "fmt" 7 8 "strings" 8 9 "time" ··· 122 123 123 124 if err != nil { 124 125 // ErrDoneIterating is expected when we stop walking early 125 - if err == repo.ErrDoneIterating { 126 + // Use errors.Is to handle wrapped errors (indigo wraps with %w in MST walk) 127 + if errors.Is(err, repo.ErrDoneIterating) { 126 128 // Successfully stopped at collection boundary 127 - } else if err.Error() == "mst: not found" || strings.Contains(err.Error(), "not found") { 129 + } else if strings.Contains(err.Error(), "not found") { 128 130 // If the collection doesn't exist yet (empty repo or no records created), 129 131 // return empty list instead of error 130 132 return []*CrewMemberWithKey{}, nil
+195
pkg/hold/pds/xrpc_test.go
··· 1289 1289 t.Skip("Method validation is now handled by chi router, not individual handlers") 1290 1290 } 1291 1291 1292 + // TestHandleRequestCrew_WithAuth tests successful crew membership request with authenticated user 1293 + // This test exercises the complete flow including the ListCrewMembers path with empty crew list, 1294 + // which previously caused a 500 error due to improper handling of wrapped ErrDoneIterating 1295 + func TestHandleRequestCrew_WithAuth(t *testing.T) { 1296 + handler, ctx := setupTestXRPCHandler(t) 1297 + 1298 + // Update captain record to allow all crew 1299 + _, err := handler.pds.UpdateCaptainRecord(ctx, true, true, false) // public=true, allowAllCrew=true 1300 + if err != nil { 1301 + t.Fatalf("Failed to update captain record: %v", err) 1302 + } 1303 + 1304 + // Remove the bootstrap-created owner crew member to get an empty crew list 1305 + // (Bootstrap automatically adds the owner as a crew admin) 1306 + crewBefore, err := handler.pds.ListCrewMembers(ctx) 1307 + if err != nil { 1308 + t.Fatalf("Failed to list crew members before test: %v", err) 1309 + } 1310 + for _, member := range crewBefore { 1311 + if err := handler.pds.RemoveCrewMember(ctx, member.Rkey); err != nil { 1312 + t.Fatalf("Failed to remove bootstrap crew member: %v", err) 1313 + } 1314 + } 1315 + 1316 + // Verify crew list is now empty (this is the critical condition that triggers the bug) 1317 + crewAfterCleanup, err := handler.pds.ListCrewMembers(ctx) 1318 + if err != nil { 1319 + t.Fatalf("Failed to list crew members after cleanup: %v", err) 1320 + } 1321 + if len(crewAfterCleanup) != 0 { 1322 + t.Fatalf("Expected empty crew list after cleanup, got %d members", len(crewAfterCleanup)) 1323 + } 1324 + 1325 + // Create authenticated user (injected into context to bypass auth middleware) 1326 + testUserDID := "did:plc:newuser123" 1327 + user := &ValidatedUser{ 1328 + DID: testUserDID, 1329 + Handle: "newuser.test", 1330 + PDS: "https://pds.test", 1331 + Authorized: true, 1332 + } 1333 + 1334 + // Create request with user in context 1335 + body := map[string]any{ 1336 + "role": "member", 1337 + "permissions": []string{"blob:read", "blob:write"}, 1338 + } 1339 + bodyBytes, _ := json.Marshal(body) 1340 + req := httptest.NewRequest(http.MethodPost, atproto.HoldRequestCrew, bytes.NewReader(bodyBytes)) 1341 + req.Header.Set("Content-Type", "application/json") 1342 + 1343 + // Inject user into request context 1344 + reqCtx := context.WithValue(req.Context(), contextKeyUser, user) 1345 + req = req.WithContext(reqCtx) 1346 + 1347 + w := httptest.NewRecorder() 1348 + 1349 + // Call handler - this should NOT return 500 even with empty crew list 1350 + handler.HandleRequestCrew(w, req) 1351 + 1352 + // Verify successful response 1353 + if w.Code != http.StatusCreated { 1354 + t.Errorf("Expected status 201 Created, got %d", w.Code) 1355 + t.Logf("Response body: %s", w.Body.String()) 1356 + } 1357 + 1358 + // Verify response structure 1359 + var response map[string]any 1360 + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { 1361 + t.Fatalf("Failed to parse response JSON: %v", err) 1362 + } 1363 + 1364 + // Check response fields 1365 + if cid, ok := response["cid"].(string); !ok || cid == "" { 1366 + t.Error("Expected cid string in response") 1367 + } 1368 + 1369 + if status, ok := response["status"].(string); !ok || status != "created" { 1370 + t.Errorf("Expected status='created', got %v", response["status"]) 1371 + } 1372 + 1373 + // Verify the crew member was actually added 1374 + crewAfter, err := handler.pds.ListCrewMembers(ctx) 1375 + if err != nil { 1376 + t.Fatalf("Failed to list crew members after request: %v", err) 1377 + } 1378 + 1379 + if len(crewAfter) != 1 { 1380 + t.Fatalf("Expected 1 crew member after request, got %d", len(crewAfter)) 1381 + } 1382 + 1383 + // Verify it's the correct user 1384 + if crewAfter[0].Record.Member != testUserDID { 1385 + t.Errorf("Expected crew member DID %s, got %s", testUserDID, crewAfter[0].Record.Member) 1386 + } 1387 + } 1388 + 1389 + // TestHandleRequestCrew_AlreadyMember tests requesting crew membership when already a member 1390 + // Should return success without creating duplicate records 1391 + func TestHandleRequestCrew_AlreadyMember(t *testing.T) { 1392 + handler, ctx := setupTestXRPCHandler(t) 1393 + 1394 + // Update captain record to allow all crew 1395 + _, err := handler.pds.UpdateCaptainRecord(ctx, true, true, false) 1396 + if err != nil { 1397 + t.Fatalf("Failed to update captain record: %v", err) 1398 + } 1399 + 1400 + // Pre-add the user as a crew member 1401 + testUserDID := "did:plc:existinguser123" 1402 + _, err = handler.pds.AddCrewMember(ctx, testUserDID, "member", []string{"blob:read", "blob:write"}) 1403 + if err != nil { 1404 + t.Fatalf("Failed to pre-add crew member: %v", err) 1405 + } 1406 + 1407 + // Verify crew list has our test user (+ the bootstrap owner) 1408 + crewBefore, err := handler.pds.ListCrewMembers(ctx) 1409 + if err != nil { 1410 + t.Fatalf("Failed to list crew members: %v", err) 1411 + } 1412 + 1413 + // Find our test user in the crew list 1414 + var foundTestUser bool 1415 + var testUserCountBefore int 1416 + for _, member := range crewBefore { 1417 + if member.Record.Member == testUserDID { 1418 + foundTestUser = true 1419 + testUserCountBefore++ 1420 + } 1421 + } 1422 + if !foundTestUser { 1423 + t.Fatalf("Expected to find test user %s in crew list", testUserDID) 1424 + } 1425 + if testUserCountBefore != 1 { 1426 + t.Fatalf("Expected test user to appear once in crew list, got %d times", testUserCountBefore) 1427 + } 1428 + 1429 + // Create authenticated user (same DID as pre-added member) 1430 + user := &ValidatedUser{ 1431 + DID: testUserDID, 1432 + Handle: "existinguser.test", 1433 + PDS: "https://pds.test", 1434 + Authorized: true, 1435 + } 1436 + 1437 + // Create request 1438 + req := httptest.NewRequest(http.MethodPost, atproto.HoldRequestCrew, nil) 1439 + reqCtx := context.WithValue(req.Context(), contextKeyUser, user) 1440 + req = req.WithContext(reqCtx) 1441 + 1442 + w := httptest.NewRecorder() 1443 + 1444 + // Call handler - should return success with "already_member" status 1445 + handler.HandleRequestCrew(w, req) 1446 + 1447 + // Verify successful response (200 OK for already member) 1448 + if w.Code != http.StatusOK { 1449 + t.Errorf("Expected status 200 OK, got %d", w.Code) 1450 + t.Logf("Response body: %s", w.Body.String()) 1451 + } 1452 + 1453 + // Verify response indicates already a member 1454 + var response map[string]any 1455 + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { 1456 + t.Fatalf("Failed to parse response JSON: %v", err) 1457 + } 1458 + 1459 + if status, ok := response["status"].(string); !ok || status != "already_member" { 1460 + t.Errorf("Expected status='already_member', got %v", response["status"]) 1461 + } 1462 + 1463 + // Verify no duplicate was created 1464 + crewAfter, err := handler.pds.ListCrewMembers(ctx) 1465 + if err != nil { 1466 + t.Fatalf("Failed to list crew members after request: %v", err) 1467 + } 1468 + 1469 + // Count how many times our test user appears 1470 + var testUserCountAfter int 1471 + for _, member := range crewAfter { 1472 + if member.Record.Member == testUserDID { 1473 + testUserCountAfter++ 1474 + } 1475 + } 1476 + 1477 + if testUserCountAfter != 1 { 1478 + t.Errorf("Expected test user to appear exactly once (no duplicate), got %d times", testUserCountAfter) 1479 + } 1480 + 1481 + // Verify crew list size didn't change (no duplicates added) 1482 + if len(crewAfter) != len(crewBefore) { 1483 + t.Errorf("Expected crew list size to stay the same (%d), got %d", len(crewBefore), len(crewAfter)) 1484 + } 1485 + } 1486 + 1292 1487 // Tests for DID document endpoints 1293 1488 1294 1489 // TestHandleDIDDocument tests /.well-known/did.json endpoint