mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 23:16:53 +00:00
421 lines
12 KiB
Go
421 lines
12 KiB
Go
package pds
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
)
|
|
|
|
// setupTestPDS creates a test PDS instance in a temporary directory
|
|
// It initializes the repo but does NOT create captain/crew records
|
|
// Tests should call Bootstrap or create records as needed
|
|
func setupTestPDS(t *testing.T) (*HoldPDS, context.Context) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
// Use in-memory database for speed
|
|
dbPath := ":memory:"
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Copy shared signing key instead of generating a new one
|
|
if err := os.WriteFile(keyPath, sharedTestKey, 0600); err != nil {
|
|
t.Fatalf("Failed to copy shared signing key: %v", err)
|
|
}
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test PDS: %v", err)
|
|
}
|
|
|
|
// Initialize repo so tests can create records
|
|
// Use a dummy DID to initialize, then tests can create actual captain records
|
|
err = pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize test repo: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() { pds.Close() })
|
|
return pds, ctx
|
|
}
|
|
|
|
// setupTestPDSWithBootstrap creates a test PDS and bootstraps it with suppressed output
|
|
// This is a convenience function for tests that need a fully initialized PDS
|
|
func setupTestPDSWithBootstrap(t *testing.T, ownerDID string, public, allowAllCrew bool) (*HoldPDS, context.Context) {
|
|
t.Helper()
|
|
|
|
pds, ctx := setupTestPDS(t)
|
|
|
|
// Bootstrap with suppressed output
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
err := pds.Bootstrap(ctx, nil, ownerDID, public, allowAllCrew, "", "")
|
|
|
|
w.Close()
|
|
os.Stdout = oldStdout
|
|
io.ReadAll(r) // Drain the pipe
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to bootstrap PDS: %v", err)
|
|
}
|
|
|
|
return pds, ctx
|
|
}
|
|
|
|
// TestCreateCaptainRecord tests creating a captain record with various settings
|
|
func TestCreateCaptainRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ownerDID string
|
|
public bool
|
|
allowAllCrew bool
|
|
enableBlueskyPosts bool
|
|
}{
|
|
{
|
|
name: "Private hold, no all-crew",
|
|
ownerDID: "did:plc:alice123",
|
|
public: false,
|
|
allowAllCrew: false,
|
|
enableBlueskyPosts: false,
|
|
},
|
|
{
|
|
name: "Public hold, no all-crew",
|
|
ownerDID: "did:plc:bob456",
|
|
public: true,
|
|
allowAllCrew: false,
|
|
enableBlueskyPosts: true,
|
|
},
|
|
{
|
|
name: "Public hold, allow all crew",
|
|
ownerDID: "did:plc:charlie789",
|
|
public: true,
|
|
allowAllCrew: true,
|
|
enableBlueskyPosts: false,
|
|
},
|
|
{
|
|
name: "Private hold, allow all crew",
|
|
ownerDID: "did:plc:dave012",
|
|
public: false,
|
|
allowAllCrew: true,
|
|
enableBlueskyPosts: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Each subtest gets its own PDS instance
|
|
pds, ctx := setupTestPDS(t)
|
|
defer pds.Close()
|
|
|
|
// Create captain record
|
|
recordCID, err := pds.CreateCaptainRecord(ctx, tt.ownerDID, tt.public, tt.allowAllCrew, tt.enableBlueskyPosts, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify CID is defined
|
|
if !recordCID.Defined() {
|
|
t.Error("Expected defined CID")
|
|
}
|
|
|
|
// Retrieve and verify
|
|
retrievedCID, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
if !recordCID.Equals(retrievedCID) {
|
|
t.Error("Expected retrieved CID to match created CID")
|
|
}
|
|
|
|
if captain.Owner != tt.ownerDID {
|
|
t.Errorf("Expected owner %s, got %s", tt.ownerDID, captain.Owner)
|
|
}
|
|
if captain.Public != tt.public {
|
|
t.Errorf("Expected public=%v, got %v", tt.public, captain.Public)
|
|
}
|
|
if captain.AllowAllCrew != tt.allowAllCrew {
|
|
t.Errorf("Expected allowAllCrew=%v, got %v", tt.allowAllCrew, captain.AllowAllCrew)
|
|
}
|
|
if captain.EnableBlueskyPosts != tt.enableBlueskyPosts {
|
|
t.Errorf("Expected enableBlueskyPosts=%v, got %v", tt.enableBlueskyPosts, captain.EnableBlueskyPosts)
|
|
}
|
|
if captain.Type != atproto.CaptainCollection {
|
|
t.Errorf("Expected type %s, got %s", atproto.CaptainCollection, captain.Type)
|
|
}
|
|
if captain.DeployedAt == "" {
|
|
t.Error("Expected deployedAt to be set")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetCaptainRecord tests retrieving a captain record
|
|
func TestGetCaptainRecord(t *testing.T) {
|
|
pds, ctx := setupTestPDS(t)
|
|
defer pds.Close()
|
|
|
|
ownerDID := "did:plc:alice123"
|
|
|
|
// Create captain record
|
|
createdCID, err := pds.CreateCaptainRecord(ctx, ownerDID, true, false, false, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Get captain record
|
|
retrievedCID, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify CIDs match
|
|
if !createdCID.Equals(retrievedCID) {
|
|
t.Error("Expected retrieved CID to match created CID")
|
|
}
|
|
|
|
// Verify captain data
|
|
if captain == nil {
|
|
t.Fatal("Expected non-nil captain record")
|
|
}
|
|
if captain.Owner != ownerDID {
|
|
t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner)
|
|
}
|
|
if !captain.Public {
|
|
t.Error("Expected public=true")
|
|
}
|
|
if captain.AllowAllCrew {
|
|
t.Error("Expected allowAllCrew=false")
|
|
}
|
|
}
|
|
|
|
// TestGetCaptainRecord_NotFound tests error handling for missing captain
|
|
func TestGetCaptainRecord_NotFound(t *testing.T) {
|
|
pds, ctx := setupTestPDS(t)
|
|
defer pds.Close()
|
|
|
|
// Try to get captain record before creating one
|
|
_, _, err := pds.GetCaptainRecord(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error when getting non-existent captain record")
|
|
}
|
|
|
|
// Verify error message indicates not found
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, "not found") && !strings.Contains(errMsg, "failed to get captain record") {
|
|
t.Errorf("Expected 'not found' in error message, got: %s", errMsg)
|
|
}
|
|
}
|
|
|
|
// TestUpdateCaptainRecord tests updating captain settings
|
|
func TestUpdateCaptainRecord(t *testing.T) {
|
|
pds, ctx := setupTestPDS(t)
|
|
defer pds.Close()
|
|
|
|
ownerDID := "did:plc:alice123"
|
|
|
|
// Create initial captain record (public=false, allowAllCrew=false, enableBlueskyPosts=false)
|
|
_, err := pds.CreateCaptainRecord(ctx, ownerDID, false, false, false, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Get initial record
|
|
_, captain1, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify initial state
|
|
if captain1.Public {
|
|
t.Error("Expected initial public=false")
|
|
}
|
|
if captain1.AllowAllCrew {
|
|
t.Error("Expected initial allowAllCrew=false")
|
|
}
|
|
if captain1.EnableBlueskyPosts {
|
|
t.Error("Expected initial enableBlueskyPosts=false")
|
|
}
|
|
|
|
// Update to public=true, allowAllCrew=true, enableBlueskyPosts=true
|
|
captain1.Public = true
|
|
captain1.AllowAllCrew = true
|
|
captain1.EnableBlueskyPosts = true
|
|
updatedCID, err := pds.UpdateCaptainRecord(ctx, captain1)
|
|
if err != nil {
|
|
t.Fatalf("UpdateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
if !updatedCID.Defined() {
|
|
t.Error("Expected defined CID after update")
|
|
}
|
|
|
|
// Get updated record
|
|
retrievedCID, captain2, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after update: %v", err)
|
|
}
|
|
|
|
// Verify CID changed
|
|
if !updatedCID.Equals(retrievedCID) {
|
|
t.Error("Expected retrieved CID to match updated CID")
|
|
}
|
|
|
|
// Verify updated values
|
|
if !captain2.Public {
|
|
t.Error("Expected public=true after update")
|
|
}
|
|
if !captain2.AllowAllCrew {
|
|
t.Error("Expected allowAllCrew=true after update")
|
|
}
|
|
if !captain2.EnableBlueskyPosts {
|
|
t.Error("Expected enableBlueskyPosts=true after update")
|
|
}
|
|
|
|
// Verify owner didn't change
|
|
if captain2.Owner != ownerDID {
|
|
t.Errorf("Expected owner to remain %s, got %s", ownerDID, captain2.Owner)
|
|
}
|
|
|
|
// Update again to different values (public=true, allowAllCrew=false, enableBlueskyPosts=false)
|
|
captain2.AllowAllCrew = false
|
|
captain2.EnableBlueskyPosts = false
|
|
_, err = pds.UpdateCaptainRecord(ctx, captain2)
|
|
if err != nil {
|
|
t.Fatalf("Second UpdateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify second update
|
|
_, captain3, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after second update: %v", err)
|
|
}
|
|
|
|
if !captain3.Public {
|
|
t.Error("Expected public=true after second update")
|
|
}
|
|
if captain3.AllowAllCrew {
|
|
t.Error("Expected allowAllCrew=false after second update")
|
|
}
|
|
}
|
|
|
|
// TestUpdateCaptainRecord_NotFound tests updating non-existent captain
|
|
func TestUpdateCaptainRecord_NotFound(t *testing.T) {
|
|
pds, ctx := setupTestPDS(t)
|
|
defer pds.Close()
|
|
|
|
// Try to update captain record before creating one
|
|
record := &atproto.CaptainRecord{
|
|
Type: atproto.CaptainCollection,
|
|
Public: true,
|
|
}
|
|
_, err := pds.UpdateCaptainRecord(ctx, record)
|
|
if err == nil {
|
|
t.Fatal("Expected error when updating non-existent captain record")
|
|
}
|
|
|
|
// Verify error message
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, "failed to update captain record") {
|
|
t.Errorf("Expected 'failed to update captain record' in error, got: %s", errMsg)
|
|
}
|
|
}
|
|
|
|
// TestCaptainRecord_CBORRoundtrip tests CBOR marshal/unmarshal integrity
|
|
func TestCaptainRecord_CBORRoundtrip(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record *atproto.CaptainRecord
|
|
}{
|
|
{
|
|
name: "Basic captain",
|
|
record: &atproto.CaptainRecord{
|
|
Type: atproto.CaptainCollection,
|
|
Owner: "did:plc:alice123",
|
|
Public: true,
|
|
AllowAllCrew: false,
|
|
DeployedAt: "2025-10-16T12:00:00Z",
|
|
},
|
|
},
|
|
{
|
|
name: "Captain with optional fields",
|
|
record: &atproto.CaptainRecord{
|
|
Type: atproto.CaptainCollection,
|
|
Owner: "did:plc:bob456",
|
|
Public: false,
|
|
AllowAllCrew: true,
|
|
DeployedAt: "2025-10-16T12:00:00Z",
|
|
Region: "us-west-2",
|
|
},
|
|
},
|
|
{
|
|
name: "Captain with empty optional fields",
|
|
record: &atproto.CaptainRecord{
|
|
Type: atproto.CaptainCollection,
|
|
Owner: "did:plc:charlie789",
|
|
Public: true,
|
|
AllowAllCrew: true,
|
|
DeployedAt: "2025-10-16T12:00:00Z",
|
|
Region: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Marshal to CBOR
|
|
var buf bytes.Buffer
|
|
err := tt.record.MarshalCBOR(&buf)
|
|
if err != nil {
|
|
t.Fatalf("MarshalCBOR failed: %v", err)
|
|
}
|
|
|
|
cborBytes := buf.Bytes()
|
|
if len(cborBytes) == 0 {
|
|
t.Fatal("Expected non-empty CBOR bytes")
|
|
}
|
|
|
|
// Unmarshal from CBOR
|
|
var decoded atproto.CaptainRecord
|
|
err = decoded.UnmarshalCBOR(bytes.NewReader(cborBytes))
|
|
if err != nil {
|
|
t.Fatalf("UnmarshalCBOR failed: %v", err)
|
|
}
|
|
|
|
// Verify all fields match
|
|
if decoded.Type != tt.record.Type {
|
|
t.Errorf("Type mismatch: expected %s, got %s", tt.record.Type, decoded.Type)
|
|
}
|
|
if decoded.Owner != tt.record.Owner {
|
|
t.Errorf("Owner mismatch: expected %s, got %s", tt.record.Owner, decoded.Owner)
|
|
}
|
|
if decoded.Public != tt.record.Public {
|
|
t.Errorf("Public mismatch: expected %v, got %v", tt.record.Public, decoded.Public)
|
|
}
|
|
if decoded.AllowAllCrew != tt.record.AllowAllCrew {
|
|
t.Errorf("AllowAllCrew mismatch: expected %v, got %v", tt.record.AllowAllCrew, decoded.AllowAllCrew)
|
|
}
|
|
if decoded.DeployedAt != tt.record.DeployedAt {
|
|
t.Errorf("DeployedAt mismatch: expected %s, got %s", tt.record.DeployedAt, decoded.DeployedAt)
|
|
}
|
|
if decoded.Region != tt.record.Region {
|
|
t.Errorf("Region mismatch: expected %s, got %s", tt.record.Region, decoded.Region)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCaptainRkey tests that captain record uses the fixed "self" rkey
|
|
func TestCaptainRkey(t *testing.T) {
|
|
if CaptainRkey != "self" {
|
|
t.Errorf("Expected CaptainRkey to be 'self', got '%s'", CaptainRkey)
|
|
}
|
|
}
|