mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 08:44:14 +00:00
301 lines
8.2 KiB
Go
301 lines
8.2 KiB
Go
package pds
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
"atcr.io/pkg/auth/oauth"
|
|
"atcr.io/pkg/s3"
|
|
bsky "github.com/bluesky-social/indigo/api/bsky"
|
|
)
|
|
|
|
// Shared test resources (used across all test files in package)
|
|
var (
|
|
sharedTestKeyPath string
|
|
sharedTestKey []byte
|
|
sharedPDS *HoldPDS // Shared bootstrapped PDS for read-only tests
|
|
sharedHandler *XRPCHandler // Shared handler for read-only tests
|
|
sharedCtx context.Context // Shared context
|
|
)
|
|
|
|
func TestStatusPost(t *testing.T) {
|
|
// Create temporary directory for test
|
|
tmpDir := t.TempDir()
|
|
// Use in-memory database for speed
|
|
dbPath := ":memory:"
|
|
keyPath := filepath.Join(tmpDir, "test.key")
|
|
|
|
// Copy shared signing key
|
|
if err := os.WriteFile(keyPath, sharedTestKey, 0600); err != nil {
|
|
t.Fatalf("Failed to copy shared signing key: %v", err)
|
|
}
|
|
|
|
// Create test PDS
|
|
ctx := context.Background()
|
|
did := "did:web:test.example.com"
|
|
publicURL := "https://test.example.com"
|
|
|
|
holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, true)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test PDS: %v", err)
|
|
}
|
|
|
|
// Initialize empty repo (required before creating records)
|
|
err = holdPDS.repomgr.InitNewActor(ctx, holdPDS.uid, "", did, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize repo: %v", err)
|
|
}
|
|
|
|
// Create handler for XRPC endpoints
|
|
handler := NewXRPCHandler(holdPDS, s3.S3Service{}, nil, nil, &mockPDSClient{})
|
|
|
|
// Helper function to list posts via XRPC
|
|
listPosts := func() ([]map[string]any, error) {
|
|
req := makeXRPCGetRequest(atproto.RepoListRecords, map[string]string{
|
|
"repo": did,
|
|
"collection": StatusPostCollection,
|
|
"limit": "100",
|
|
"reverse": "true", // Most recent first
|
|
})
|
|
w := httptest.NewRecorder()
|
|
handler.HandleListRecords(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
records, ok := result["records"].([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected records array, got %T", result["records"])
|
|
}
|
|
|
|
posts := make([]map[string]any, len(records))
|
|
for i, rec := range records {
|
|
post, ok := rec.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected record map, got %T", rec)
|
|
}
|
|
posts[i] = post
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
t.Run("CreateStatusPost", func(t *testing.T) {
|
|
// Set status to online (creates new post)
|
|
err := holdPDS.SetStatus(ctx, "online")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set status to online: %v", err)
|
|
}
|
|
|
|
// List posts
|
|
posts, err := listPosts()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list posts: %v", err)
|
|
}
|
|
|
|
if len(posts) == 0 {
|
|
t.Fatal("Expected at least one status post, got 0")
|
|
}
|
|
|
|
// Get the latest post
|
|
post := posts[0]
|
|
|
|
value, ok := post["value"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("Expected value map, got %T", post["value"])
|
|
}
|
|
|
|
text, ok := value["text"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected text string, got %T", value["text"])
|
|
}
|
|
|
|
if text != "🟢 Current status: online" {
|
|
t.Errorf("Expected text '🟢 Current status: online', got '%s'", text)
|
|
}
|
|
|
|
// Verify TID-based rkey (extract from URI)
|
|
uri, ok := post["uri"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected uri string, got %T", post["uri"])
|
|
}
|
|
// URI format: at://did:web:test.example.com/app.bsky.feed.post/3m3c4...
|
|
// We just check that it contains the collection
|
|
if !contains(uri, StatusPostCollection) {
|
|
t.Errorf("Expected URI to contain collection %s, got %s", StatusPostCollection, uri)
|
|
}
|
|
})
|
|
|
|
t.Run("CreateMultiplePosts", func(t *testing.T) {
|
|
// Create multiple status posts
|
|
err := holdPDS.SetStatus(ctx, "offline")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set status to offline: %v", err)
|
|
}
|
|
|
|
// Wait a moment to ensure different timestamp
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
err = holdPDS.SetStatus(ctx, "online")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set status to online again: %v", err)
|
|
}
|
|
|
|
// List all posts - should have at least 3 now (1 from previous test + 2 from this test)
|
|
posts, err := listPosts()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list posts: %v", err)
|
|
}
|
|
|
|
if len(posts) < 3 {
|
|
t.Errorf("Expected at least 3 status posts, got %d", len(posts))
|
|
}
|
|
|
|
// Verify each post has a unique URI
|
|
uris := make(map[string]bool)
|
|
for _, post := range posts {
|
|
uri, ok := post["uri"].(string)
|
|
if !ok {
|
|
t.Errorf("Expected uri string, got %T", post["uri"])
|
|
continue
|
|
}
|
|
if uris[uri] {
|
|
t.Errorf("Duplicate URI found: %s", uri)
|
|
}
|
|
uris[uri] = true
|
|
}
|
|
|
|
// Verify the latest post is online
|
|
latestPost := posts[0]
|
|
value, ok := latestPost["value"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("Expected value map, got %T", latestPost["value"])
|
|
}
|
|
text, ok := value["text"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected text string, got %T", value["text"])
|
|
}
|
|
if text != "🟢 Current status: online" {
|
|
t.Errorf("Expected latest post text '🟢 Current status: online', got '%s'", text)
|
|
}
|
|
})
|
|
|
|
t.Run("OfflineStatus", func(t *testing.T) {
|
|
// Create offline status post
|
|
err := holdPDS.SetStatus(ctx, "offline")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set status to offline: %v", err)
|
|
}
|
|
|
|
// Get the latest post
|
|
posts, err := listPosts()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list posts: %v", err)
|
|
}
|
|
|
|
if len(posts) == 0 {
|
|
t.Fatal("Expected at least one status post, got 0")
|
|
}
|
|
|
|
latestPost := posts[0]
|
|
value, ok := latestPost["value"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("Expected value map, got %T", latestPost["value"])
|
|
}
|
|
text, ok := value["text"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected text string, got %T", value["text"])
|
|
}
|
|
|
|
if text != "🔴 Current status: offline" {
|
|
t.Errorf("Expected text '🔴 Current status: offline', got '%s'", text)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestStatusPostCollection(t *testing.T) {
|
|
// Verify constant
|
|
if StatusPostCollection != "app.bsky.feed.post" {
|
|
t.Errorf("Expected StatusPostCollection 'app.bsky.feed.post', got '%s'", StatusPostCollection)
|
|
}
|
|
}
|
|
|
|
// Helper function to check if a string contains a substring
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && findSubstring(s, substr))
|
|
}
|
|
|
|
func findSubstring(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func init() {
|
|
// Register FeedPost type for testing
|
|
// This is normally done by the bsky package init(), but we need to ensure it's done
|
|
// The actual registration happens in the bsky package, so this is a no-op
|
|
// but kept for clarity
|
|
_ = &bsky.FeedPost{}
|
|
}
|
|
|
|
// Cleanup function to remove test files
|
|
func TestMain(m *testing.M) {
|
|
// Create a temporary directory for shared test key
|
|
tmpDir, err := os.MkdirTemp("", "pds-test-shared-*")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to create temp dir: %v", err))
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Generate one signing key to be reused across all tests in the package
|
|
sharedTestKeyPath = filepath.Join(tmpDir, "shared-signing-key")
|
|
privateKey, err := oauth.GenerateOrLoadPDSKey(sharedTestKeyPath)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to generate shared signing key: %v", err))
|
|
}
|
|
|
|
// Store the key bytes so tests can copy them
|
|
sharedTestKey = privateKey.Bytes()
|
|
|
|
// Create one shared, bootstrapped PDS for read-only tests
|
|
// Use in-memory database for speed
|
|
sharedCtx = context.Background()
|
|
sharedPDS, err = NewHoldPDS(sharedCtx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", sharedTestKeyPath, true)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to create shared PDS: %v", err))
|
|
}
|
|
|
|
// Bootstrap once
|
|
ownerDID := "did:plc:testowner123"
|
|
err = sharedPDS.Bootstrap(sharedCtx, nil, ownerDID, true, false, "")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to bootstrap shared PDS: %v", err))
|
|
}
|
|
|
|
// Create shared handler
|
|
sharedHandler = NewXRPCHandler(sharedPDS, s3.S3Service{}, nil, nil, &mockPDSClient{})
|
|
|
|
// Run tests
|
|
code := m.Run()
|
|
|
|
// Cleanup is automatic with t.TempDir()
|
|
os.Exit(code)
|
|
}
|