Files
at-container-registry/pkg/hold/pds/layer_test.go

711 lines
18 KiB
Go

package pds
import (
"os"
"path/filepath"
"testing"
"atcr.io/pkg/atproto"
"atcr.io/pkg/hold/quota"
)
func TestCreateLayerRecord(t *testing.T) {
// Setup test PDS
pds, ctx := setupTestPDS(t)
tests := []struct {
name string
record *atproto.LayerRecord
wantErr bool
errSubstr string
}{
{
name: "valid layer record",
record: atproto.NewLayerRecord(
"sha256:abc123def456",
1048576, // 1 MB
"application/vnd.oci.image.layer.v1.tar+gzip",
"did:plc:alice123",
"at://did:plc:alice123/io.atcr.manifest/abc123def456",
),
wantErr: false,
},
{
name: "valid layer record with large size",
record: atproto.NewLayerRecord(
"sha256:fedcba987654",
1073741824, // 1 GB
"application/vnd.docker.image.rootfs.diff.tar.gzip",
"did:plc:bob456",
"at://did:plc:bob456/io.atcr.manifest/fedcba987654",
),
wantErr: false,
},
{
name: "invalid record type",
record: &atproto.LayerRecord{
Type: "wrong.type",
Digest: "sha256:abc123",
Size: 1024,
MediaType: "application/vnd.oci.image.layer.v1.tar",
Manifest: "at://did:plc:test/io.atcr.manifest/abc123",
UserDID: "did:plc:test",
},
wantErr: true,
errSubstr: "invalid record type",
},
{
name: "missing digest",
record: &atproto.LayerRecord{
Type: atproto.LayerCollection,
Digest: "",
Size: 1024,
MediaType: "application/vnd.oci.image.layer.v1.tar",
Manifest: "at://did:plc:test/io.atcr.manifest/abc123",
UserDID: "did:plc:test",
},
wantErr: true,
errSubstr: "digest is required",
},
{
name: "zero size",
record: &atproto.LayerRecord{
Type: atproto.LayerCollection,
Digest: "sha256:abc123",
Size: 0,
MediaType: "application/vnd.oci.image.layer.v1.tar",
Manifest: "at://did:plc:test/io.atcr.manifest/abc123",
UserDID: "did:plc:test",
},
wantErr: true,
errSubstr: "size must be positive",
},
{
name: "negative size",
record: &atproto.LayerRecord{
Type: atproto.LayerCollection,
Digest: "sha256:abc123",
Size: -1,
MediaType: "application/vnd.oci.image.layer.v1.tar",
Manifest: "at://did:plc:test/io.atcr.manifest/abc123",
UserDID: "did:plc:test",
},
wantErr: true,
errSubstr: "size must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rkey, cid, err := pds.CreateLayerRecord(ctx, tt.record)
if tt.wantErr {
if err == nil {
t.Errorf("CreateLayerRecord() expected error containing %q, got nil", tt.errSubstr)
return
}
if tt.errSubstr != "" && !contains(err.Error(), tt.errSubstr) {
t.Errorf("CreateLayerRecord() error = %v, want error containing %q", err, tt.errSubstr)
}
return
}
if err != nil {
t.Errorf("CreateLayerRecord() unexpected error: %v", err)
return
}
if rkey == "" {
t.Error("CreateLayerRecord() returned empty rkey")
}
if cid == "" {
t.Error("CreateLayerRecord() returned empty CID")
}
t.Logf("Created layer record: rkey=%s, cid=%s", rkey, cid)
})
}
}
func TestCreateLayerRecord_MultipleRecords(t *testing.T) {
// Test creating multiple layer records for the same manifest
pds, ctx := setupTestPDS(t)
manifestURI := "at://did:plc:test123/io.atcr.manifest/manifestabc123"
layers := []struct {
digest string
size int64
}{
{"sha256:layer1abc123", 1024},
{"sha256:layer2def456", 2048},
{"sha256:layer3ghi789", 4096},
}
createdRKeys := make(map[string]bool)
for i, layer := range layers {
record := atproto.NewLayerRecord(
layer.digest,
layer.size,
"application/vnd.oci.image.layer.v1.tar+gzip",
"did:plc:test123",
manifestURI,
)
rkey, cid, err := pds.CreateLayerRecord(ctx, record)
if err != nil {
t.Fatalf("CreateLayerRecord() for layer %d failed: %v", i, err)
}
// Ensure unique rkeys
if createdRKeys[rkey] {
t.Errorf("CreateLayerRecord() returned duplicate rkey: %s", rkey)
}
createdRKeys[rkey] = true
t.Logf("Layer %d: rkey=%s, cid=%s", i, rkey, cid)
}
if len(createdRKeys) != len(layers) {
t.Errorf("Created %d unique rkeys, want %d", len(createdRKeys), len(layers))
}
}
func TestNewLayerRecord(t *testing.T) {
// Test the layer record constructor
digest := "sha256:abc123def456"
size := int64(1048576)
mediaType := "application/vnd.oci.image.layer.v1.tar+gzip"
userDID := "did:plc:alice123"
manifestURI := "at://did:plc:alice123/io.atcr.manifest/abc123def456"
record := atproto.NewLayerRecord(digest, size, mediaType, userDID, manifestURI)
if record == nil {
t.Fatal("NewLayerRecord() returned nil")
}
// Verify all fields are set correctly
if record.Type != atproto.LayerCollection {
t.Errorf("Type = %q, want %q", record.Type, atproto.LayerCollection)
}
if record.Digest != digest {
t.Errorf("Digest = %q, want %q", record.Digest, digest)
}
if record.Size != size {
t.Errorf("Size = %d, want %d", record.Size, size)
}
if record.MediaType != mediaType {
t.Errorf("MediaType = %q, want %q", record.MediaType, mediaType)
}
if record.Manifest != manifestURI {
t.Errorf("Manifest = %q, want %q", record.Manifest, manifestURI)
}
if record.UserDID != userDID {
t.Errorf("UserDID = %q, want %q", record.UserDID, userDID)
}
if record.CreatedAt == "" {
t.Error("CreatedAt is empty")
}
t.Logf("Created layer record: %+v", record)
}
func TestLayerRecord_FieldValidation(t *testing.T) {
// Test various field values
tests := []struct {
name string
digest string
size int64
mediaType string
userDID string
manifestURI string
}{
{
name: "typical OCI layer",
digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
size: 12582912, // 12 MB
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
userDID: "did:plc:evan123",
manifestURI: "at://did:plc:evan123/io.atcr.manifest/abc123",
},
{
name: "Docker layer format",
digest: "sha256:abc123",
size: 1024,
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
userDID: "did:plc:user456",
manifestURI: "at://did:plc:user456/io.atcr.manifest/def456",
},
{
name: "uncompressed layer",
digest: "sha256:def456",
size: 2048,
mediaType: "application/vnd.oci.image.layer.v1.tar",
userDID: "did:plc:user789",
manifestURI: "at://did:plc:user789/io.atcr.manifest/ghi789",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
record := atproto.NewLayerRecord(
tt.digest,
tt.size,
tt.mediaType,
tt.userDID,
tt.manifestURI,
)
if record == nil {
t.Fatal("NewLayerRecord() returned nil")
}
// Verify the record can be created
if record.Type != atproto.LayerCollection {
t.Errorf("Type = %q, want %q", record.Type, atproto.LayerCollection)
}
if record.Digest != tt.digest {
t.Errorf("Digest = %q, want %q", record.Digest, tt.digest)
}
if record.Manifest != tt.manifestURI {
t.Errorf("Manifest = %q, want %q", record.Manifest, tt.manifestURI)
}
})
}
}
// setupTestPDSWithIndex creates a PDS with file-based database (enables RecordsIndex)
// and bootstraps it with the given owner. Required for quota tests.
func setupTestPDSWithIndex(t *testing.T, ownerDID string) *HoldPDS {
t.Helper()
ctx := sharedCtx
tmpDir := t.TempDir()
// Use file-based database to enable RecordsIndex
dbPath := filepath.Join(tmpDir, "pds.db")
keyPath := filepath.Join(tmpDir, "signing-key")
// Copy shared signing key
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", "https://atcr.io", dbPath, keyPath, false)
if err != nil {
t.Fatalf("Failed to create test PDS: %v", err)
}
// Bootstrap with owner
if err := pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true}); err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Wire up records indexing
indexingHandler := pds.CreateRecordsIndexEventHandler(nil)
pds.RepomgrRef().SetEventHandler(indexingHandler, true)
// Backfill index from MST
if err := pds.BackfillRecordsIndex(ctx); err != nil {
t.Fatalf("Failed to backfill records index: %v", err)
}
t.Cleanup(func() { pds.Close() })
return pds
}
// addCrewMemberWithTier adds a crew member with a specific tier
func addCrewMemberWithTier(t *testing.T, pds *HoldPDS, memberDID, role string, permissions []string, tier string) {
t.Helper()
crewRecord := &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: memberDID,
Role: role,
Permissions: permissions,
Tier: tier,
AddedAt: "2026-01-04T12:00:00Z",
}
_, _, err := pds.repomgr.CreateRecord(sharedCtx, pds.uid, atproto.CrewCollection, crewRecord)
if err != nil {
t.Fatalf("Failed to add crew member with tier: %v", err)
}
}
func TestGetQuotaForUserWithTier_OwnerUnlimited(t *testing.T) {
ownerDID := "did:plc:owner123"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Create quota manager with config
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "quotas.yaml")
configContent := `
tiers:
- name: deckhand
quota: 5GB
- name: bosun
quota: 50GB
defaults:
new_crew_tier: deckhand
`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to write quota config: %v", err)
}
quotaMgr, err := quota.NewManager(configPath)
if err != nil {
t.Fatalf("Failed to create quota manager: %v", err)
}
// Create layer records for owner
for i := range 3 {
record := atproto.NewLayerRecord(
"sha256:owner"+string(rune('a'+i)),
1024*1024*100, // 100MB each
"application/vnd.oci.image.layer.v1.tar+gzip",
ownerDID,
"at://"+ownerDID+"/io.atcr.manifest/test123",
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record: %v", err)
}
}
// Get quota for owner
stats, err := pds.GetQuotaForUserWithTier(ctx, ownerDID, quotaMgr)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Owner should have unlimited quota (nil limit)
if stats.Limit != nil {
t.Errorf("Expected nil limit for owner, got %d", *stats.Limit)
}
// Tier should be "owner"
if stats.Tier != "owner" {
t.Errorf("Expected tier 'owner', got %q", stats.Tier)
}
// Should have 3 unique blobs
if stats.UniqueBlobs != 3 {
t.Errorf("Expected 3 unique blobs, got %d", stats.UniqueBlobs)
}
// Total size should be 300MB
expectedSize := int64(3 * 100 * 1024 * 1024)
if stats.TotalSize != expectedSize {
t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize)
}
t.Logf("Owner quota stats: %+v", stats)
}
func TestGetQuotaForUserWithTier_CrewWithDefaultTier(t *testing.T) {
ownerDID := "did:plc:owner456"
crewDID := "did:plc:crew123"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Create quota manager
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "quotas.yaml")
configContent := `
tiers:
- name: deckhand
quota: 5GB
- name: bosun
quota: 50GB
defaults:
new_crew_tier: deckhand
`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to write quota config: %v", err)
}
quotaMgr, err := quota.NewManager(configPath)
if err != nil {
t.Fatalf("Failed to create quota manager: %v", err)
}
// Add crew member with no tier (should use default)
addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "")
// Create layer records for crew member
for i := range 2 {
record := atproto.NewLayerRecord(
"sha256:crew"+string(rune('a'+i)),
1024*1024*50, // 50MB each
"application/vnd.oci.image.layer.v1.tar+gzip",
crewDID,
"at://"+crewDID+"/io.atcr.manifest/test456",
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record: %v", err)
}
}
// Get quota for crew member
stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Should have 5GB limit (deckhand tier)
expectedLimit := int64(5 * 1024 * 1024 * 1024)
if stats.Limit == nil {
t.Fatal("Expected non-nil limit for crew member")
}
if *stats.Limit != expectedLimit {
t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit)
}
// Tier should be "deckhand"
if stats.Tier != "deckhand" {
t.Errorf("Expected tier 'deckhand', got %q", stats.Tier)
}
// Should have 2 unique blobs
if stats.UniqueBlobs != 2 {
t.Errorf("Expected 2 unique blobs, got %d", stats.UniqueBlobs)
}
t.Logf("Crew (deckhand tier) quota stats: %+v", stats)
}
func TestGetQuotaForUserWithTier_CrewWithExplicitTier(t *testing.T) {
ownerDID := "did:plc:owner789"
crewDID := "did:plc:bosuncrew456"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Create quota manager
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "quotas.yaml")
configContent := `
tiers:
- name: deckhand
quota: 5GB
- name: bosun
quota: 50GB
defaults:
new_crew_tier: deckhand
`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to write quota config: %v", err)
}
quotaMgr, err := quota.NewManager(configPath)
if err != nil {
t.Fatalf("Failed to create quota manager: %v", err)
}
// Add crew member with explicit "bosun" tier
addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun")
// Create layer records for crew member
record := atproto.NewLayerRecord(
"sha256:bosunlayer1",
1024*1024*1024, // 1GB
"application/vnd.oci.image.layer.v1.tar+gzip",
crewDID,
"at://"+crewDID+"/io.atcr.manifest/test789",
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record: %v", err)
}
// Get quota for crew member
stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Should have 50GB limit (bosun tier)
expectedLimit := int64(50 * 1024 * 1024 * 1024)
if stats.Limit == nil {
t.Fatal("Expected non-nil limit for crew member")
}
if *stats.Limit != expectedLimit {
t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit)
}
// Tier should be "bosun"
if stats.Tier != "bosun" {
t.Errorf("Expected tier 'bosun', got %q", stats.Tier)
}
t.Logf("Crew (bosun tier) quota stats: %+v", stats)
}
func TestGetQuotaForUserWithTier_NoQuotaManager(t *testing.T) {
ownerDID := "did:plc:ownerabc"
crewDID := "did:plc:crewabc"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Add crew member
addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "deckhand")
// Create layer record
record := atproto.NewLayerRecord(
"sha256:noquotalayer1",
1024*1024*100,
"application/vnd.oci.image.layer.v1.tar+gzip",
crewDID,
"at://"+crewDID+"/io.atcr.manifest/testabc",
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record: %v", err)
}
// Get quota with nil quota manager (no enforcement)
stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, nil)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Should have nil limit (unlimited)
if stats.Limit != nil {
t.Errorf("Expected nil limit when quota manager is nil, got %d", *stats.Limit)
}
// Tier should be empty
if stats.Tier != "" {
t.Errorf("Expected empty tier, got %q", stats.Tier)
}
t.Logf("No quota manager stats: %+v", stats)
}
func TestGetQuotaForUserWithTier_DisabledQuotas(t *testing.T) {
ownerDID := "did:plc:ownerdef"
crewDID := "did:plc:crewdef"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Create quota manager with nonexistent config (disabled)
quotaMgr, err := quota.NewManager("/nonexistent/quotas.yaml")
if err != nil {
t.Fatalf("Failed to create quota manager: %v", err)
}
if quotaMgr.IsEnabled() {
t.Fatal("Expected quotas to be disabled")
}
// Add crew member
addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun")
// Create layer record
record := atproto.NewLayerRecord(
"sha256:disabledlayer1",
1024*1024*100,
"application/vnd.oci.image.layer.v1.tar+gzip",
crewDID,
"at://"+crewDID+"/io.atcr.manifest/testdef",
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record: %v", err)
}
// Get quota with disabled quota manager
stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Should have nil limit (unlimited when quotas disabled)
if stats.Limit != nil {
t.Errorf("Expected nil limit when quotas disabled, got %d", *stats.Limit)
}
t.Logf("Disabled quotas stats: %+v", stats)
}
func TestGetQuotaForUserWithTier_DeduplicatesBlobs(t *testing.T) {
ownerDID := "did:plc:ownerghi"
crewDID := "did:plc:crewghi"
pds := setupTestPDSWithIndex(t, ownerDID)
ctx := sharedCtx
// Create quota manager
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "quotas.yaml")
configContent := `
tiers:
- name: deckhand
quota: 5GB
defaults:
new_crew_tier: deckhand
`
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to write quota config: %v", err)
}
quotaMgr, err := quota.NewManager(configPath)
if err != nil {
t.Fatalf("Failed to create quota manager: %v", err)
}
// Add crew member
addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "")
// Create multiple layer records with same digest (should be deduplicated)
digest := "sha256:duplicatelayer"
for i := range 5 {
record := atproto.NewLayerRecord(
digest,
1024*1024*100, // 100MB
"application/vnd.oci.image.layer.v1.tar+gzip",
crewDID,
"at://"+crewDID+"/io.atcr.manifest/manifest"+string(rune('a'+i)),
)
if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil {
t.Fatalf("Failed to create layer record %d: %v", i, err)
}
}
// Get quota
stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr)
if err != nil {
t.Fatalf("GetQuotaForUserWithTier failed: %v", err)
}
// Should have 1 unique blob (deduplicated)
if stats.UniqueBlobs != 1 {
t.Errorf("Expected 1 unique blob (deduplicated), got %d", stats.UniqueBlobs)
}
// Total size should be 100MB (not 500MB)
expectedSize := int64(100 * 1024 * 1024)
if stats.TotalSize != expectedSize {
t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize)
}
t.Logf("Deduplicated quota stats: %+v", stats)
}