mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
711 lines
18 KiB
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)
|
|
}
|