implement com.atproto.sync.getRepoStatus

This commit is contained in:
Evan Jarrett
2025-10-25 09:13:47 -05:00
parent 8201d9977d
commit 771cd4390a
4 changed files with 154 additions and 0 deletions
+1
View File
@@ -405,6 +405,7 @@ Write access:
Standard ATProto sync endpoints:
- `GET /xrpc/com.atproto.sync.getRepo?did={did}` - Download full repository as CAR file
- `GET /xrpc/com.atproto.sync.getRepo?did={did}&since={rev}` - Download repository diff since revision
- `GET /xrpc/com.atproto.sync.getRepoStatus?did={did}` - Get repository hosting status and current revision
- `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events
- `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS)
- `GET /xrpc/com.atproto.sync.getBlob?did={did}&cid={digest}` - Get blob or presigned download URL
+6
View File
@@ -98,6 +98,12 @@ const (
// Response: Stream of #commit events
SyncSubscribeRepos = "/xrpc/com.atproto.sync.subscribeRepos"
// SyncGetRepoStatus gets the hosting status for a repository.
// Method: GET
// Query: did={did}
// Response: {"did": "...", "active": true, "rev": "..."}
SyncGetRepoStatus = "/xrpc/com.atproto.sync.getRepoStatus"
// SyncRequestCrawl requests a relay to crawl a PDS.
// Method: POST
// Request: {"hostname": "hold01.atcr.io"}
+42
View File
@@ -160,6 +160,7 @@ func (h *XRPCHandler) RegisterHandlers(r chi.Router) {
r.Get(atproto.SyncListRepos, h.HandleListRepos)
r.Get(atproto.SyncGetRecord, h.HandleSyncGetRecord)
r.Get(atproto.SyncGetRepo, h.HandleGetRepo)
r.Get(atproto.SyncGetRepoStatus, h.HandleGetRepoStatus)
r.Get(atproto.SyncSubscribeRepos, h.HandleSubscribeRepos)
// DID document and handle resolution
@@ -1106,6 +1107,47 @@ func (h *XRPCHandler) HandleListRepos(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}
// HandleGetRepoStatus returns the hosting status for a repository
// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
func (h *XRPCHandler) HandleGetRepoStatus(w http.ResponseWriter, r *http.Request) {
// Get required 'did' parameter
did := r.URL.Query().Get("did")
if did == "" {
http.Error(w, "missing required parameter: did", http.StatusBadRequest)
return
}
// Validate DID matches this PDS (single-user PDS only hosts one repo)
if did != h.pds.DID() {
http.Error(w, "repo not found", http.StatusNotFound)
return
}
// Get current repo revision to verify repo is initialized
rev, err := h.pds.repomgr.GetRepoRev(r.Context(), h.pds.uid)
if err != nil || rev == "" {
// Repo exists (DID matches) but no commits yet
// Per ATProto spec, return active=true even if empty
response := map[string]any{
"did": did,
"active": true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
// Return status with revision
response := map[string]any{
"did": did,
"active": true,
"rev": rev,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleDIDDocument returns the DID document
func (h *XRPCHandler) HandleDIDDocument(w http.ResponseWriter, r *http.Request) {
doc, err := h.pds.GenerateDIDDocument(h.pds.PublicURL)
+105
View File
@@ -981,6 +981,111 @@ func TestHandleListRepos_MethodNotAllowed(t *testing.T) {
t.Skip("Method validation is now handled by chi router, not individual handlers")
}
// Tests for HandleGetRepoStatus
// TestHandleGetRepoStatus tests com.atproto.sync.getRepoStatus with a valid DID
// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
func TestHandleGetRepoStatus(t *testing.T) {
handler, _ := setupTestXRPCHandler(t)
holdDID := "did:web:hold.example.com"
req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
"did": holdDID,
})
w := httptest.NewRecorder()
handler.HandleGetRepoStatus(w, req)
result := assertJSONResponse(t, w, http.StatusOK)
// Verify required fields per spec
if did, ok := result["did"].(string); !ok || did != holdDID {
t.Errorf("Expected did=%s, got %v", holdDID, result["did"])
}
if active, ok := result["active"].(bool); !ok {
t.Error("Expected active boolean")
} else if !active {
t.Error("Expected active to be true")
}
// rev is optional but should be present for initialized repo
if rev, ok := result["rev"].(string); ok && rev == "" {
t.Error("Expected non-empty rev string when present")
}
}
// TestHandleGetRepoStatus_EmptyRepo tests getRepoStatus with no commits
// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
func TestHandleGetRepoStatus_EmptyRepo(t *testing.T) {
pds, ctx := setupTestPDS(t) // Don't bootstrap
mockClient := &mockPDSClient{}
mockS3 := s3.S3Service{}
handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient)
holdDID := "did:web:hold.example.com"
// Initialize repo but don't add any records
err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "")
if err != nil {
t.Fatalf("Failed to initialize repo: %v", err)
}
req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
"did": holdDID,
})
w := httptest.NewRecorder()
handler.HandleGetRepoStatus(w, req)
result := assertJSONResponse(t, w, http.StatusOK)
// Even with no commits, repo is active
if did, ok := result["did"].(string); !ok || did != holdDID {
t.Errorf("Expected did=%s, got %v", holdDID, result["did"])
}
if active, ok := result["active"].(bool); !ok || !active {
t.Error("Expected active=true even for empty repo")
}
// rev may not be present for empty repo (no commits)
if rev, ok := result["rev"].(string); ok && rev != "" {
t.Logf("Note: Empty repo has rev=%s (acceptable)", rev)
}
}
// TestHandleGetRepoStatus_MissingDID tests missing did parameter
// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
func TestHandleGetRepoStatus_MissingDID(t *testing.T) {
handler, _ := setupTestXRPCHandler(t)
req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, nil)
w := httptest.NewRecorder()
handler.HandleGetRepoStatus(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status 400, got %d", w.Code)
}
}
// TestHandleGetRepoStatus_InvalidDID tests invalid DID
// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
func TestHandleGetRepoStatus_InvalidDID(t *testing.T) {
handler, _ := setupTestXRPCHandler(t)
req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
"did": "did:plc:wrongdid",
})
w := httptest.NewRecorder()
handler.HandleGetRepoStatus(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("Expected status 404, got %d", w.Code)
}
}
// Tests for HandleSyncGetRecord
// TestHandleSyncGetRecord tests com.atproto.sync.getRecord