mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
implement com.atproto.sync.getRepoStatus
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user