Files
at-container-registry/pkg/hold/pds/captain_test.go
T

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)
}
}