Files
at-container-registry/pkg/hold/pds/status_test.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)
}