package oci_test import ( "bytes" "os" "path/filepath" "strings" "testing" "github.com/forgeo/forgebucket/internal/domain/oci" ) func TestParseOCIPath(t *testing.T) { tests := []struct { path string wantName string wantKind string wantRef string }{ {"/v2/", "", "", ""}, {"/v2", "", "", ""}, {"/v2/alice/myapp/tags/list", "alice/myapp", "tags", ""}, {"/v2/alice/myapp/manifests/latest", "alice/myapp", "manifest", "latest"}, {"/v2/alice/myapp/manifests/sha256:abc123", "alice/myapp", "manifest", "sha256:abc123"}, {"/v2/alice/myapp/blobs/sha256:def456", "alice/myapp", "blob", "sha256:def456"}, {"/v2/alice/myapp/blobs/uploads/", "alice/myapp", "upload", ""}, {"/v2/alice/myapp/blobs/uploads/uuid123", "alice/myapp", "upload", "uuid123"}, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { name, kind, ref := oci.ParseOCIPath(tt.path) if name != tt.wantName { t.Errorf("name = %q, want %q", name, tt.wantName) } if kind != tt.wantKind { t.Errorf("kind = %q, want %q", kind, tt.wantKind) } if ref != tt.wantRef { t.Errorf("ref = %q, want %q", ref, tt.wantRef) } }) } } func TestValidateName(t *testing.T) { if err := oci.ValidateName("alice/myapp"); err != nil { t.Errorf("valid name got error: %v", err) } if err := oci.ValidateName(""); err == nil { t.Error("empty name should error") } if err := oci.ValidateName("alice/my app"); err == nil { t.Error("name with spaces should error") } } func TestBlobPath(t *testing.T) { dir := t.TempDir() reg, err := oci.New(dir) if err != nil { t.Fatal(err) } p, err := reg.BlobPath("sha256:" + strings.Repeat("a", 64)) if err != nil { t.Fatal(err) } expectedSuffix := filepath.Join("blobs", "sha256", strings.Repeat("a", 64)) if !strings.HasSuffix(p, expectedSuffix) { t.Errorf("path %q does not end with %q", p, expectedSuffix) } if _, err := reg.BlobPath("sha256:bad"); err == nil { t.Error("expected error for short hex") } if _, err := reg.BlobPath("md5:abc"); err == nil { t.Error("expected error for non-sha256 algorithm") } } func TestWriteAndReadBlob(t *testing.T) { dir := t.TempDir() reg, err := oci.New(dir) if err != nil { t.Fatal(err) } content := []byte("hello oci blob") digest, size, err := reg.WriteBlob(bytes.NewReader(content)) if err != nil { t.Fatalf("WriteBlob: %v", err) } if !strings.HasPrefix(digest, "sha256:") { t.Errorf("digest should start with sha256:, got %s", digest) } if size != int64(len(content)) { t.Errorf("size = %d, want %d", size, len(content)) } if !reg.BlobExists(digest) { t.Error("blob should exist after write") } // Deduplication test: writing same content again should succeed without error. d2, s2, err := reg.WriteBlob(bytes.NewReader(content)) if err != nil { t.Fatalf("WriteBlob duplicate: %v", err) } if d2 != digest { t.Errorf("digest mismatch: %s vs %s", d2, digest) } if s2 != size { t.Errorf("size mismatch: %d vs %d", s2, size) } f, err := reg.ReadBlob(digest) if err != nil { t.Fatalf("ReadBlob: %v", err) } defer f.Close() buf := new(bytes.Buffer) buf.ReadFrom(f) if buf.String() != string(content) { t.Errorf("content mismatch: got %s", buf.String()) } } func TestUploadSession(t *testing.T) { dir := t.TempDir() reg, _ := oci.New(dir) uploadID := "test-upload-001" // Append content in chunks. off, err := reg.AppendUpload(uploadID, strings.NewReader("chunk1")) if err != nil { t.Fatalf("AppendUpload: %v", err) } if off != 6 { t.Errorf("expected offset 6, got %d", off) } off, err = reg.AppendUpload(uploadID, strings.NewReader("-chunk2")) if err != nil { t.Fatalf("AppendUpload second: %v", err) } if off != 13 { t.Errorf("expected offset 13 after chunk2, got %d", off) } if reg.UploadOffset(uploadID) != 13 { t.Errorf("UploadOffset = %d, want 13", reg.UploadOffset(uploadID)) } // Finish upload with digest. digest, size, err := reg.FinishUpload(uploadID, "") if err != nil { t.Fatalf("FinishUpload: %v", err) } if !strings.HasPrefix(digest, "sha256:") { t.Errorf("expected sha256 digest, got %s", digest) } if size != 13 { t.Errorf("expected size 13, got %d", size) } if !reg.BlobExists(digest) { t.Error("blob should exist after finish upload") } // Verify content. f, _ := reg.ReadBlob(digest) buf := new(bytes.Buffer) buf.ReadFrom(f) f.Close() if buf.String() != "chunk1-chunk2" { t.Errorf("content = %q, want %q", buf.String(), "chunk1-chunk2") } } func TestFinishUploadDigestMismatch(t *testing.T) { dir := t.TempDir() reg, _ := oci.New(dir) uploadID := "mismatch-upload" reg.AppendUpload(uploadID, strings.NewReader("some data")) _, _, err := reg.FinishUpload(uploadID, "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") if err == nil { t.Fatal("expected digest mismatch error") } if !strings.Contains(err.Error(), "digest mismatch") { t.Errorf("expected 'digest mismatch', got: %v", err) } } func TestManifestDescriptor(t *testing.T) { body := []byte(`{"schemaVersion":2}`) digest, size := oci.ManifestDescriptor(body) if !strings.HasPrefix(digest, "sha256:") { t.Errorf("digest should be sha256, got %s", digest) } if size != int64(len(body)) { t.Errorf("size = %d, want %d", size, len(body)) } } func TestIsDigestRef(t *testing.T) { if !oci.IsDigestRef("sha256:abc") { t.Error("sha256:abc should be a digest ref") } if oci.IsDigestRef("latest") { t.Error("latest should NOT be a digest ref") } } func TestDeleteBlob(t *testing.T) { dir := t.TempDir() reg, _ := oci.New(dir) content := []byte("delete me") digest, _, _ := reg.WriteBlob(bytes.NewReader(content)) if !reg.BlobExists(digest) { t.Fatal("blob should exist after write") } if err := reg.DeleteBlob(digest); err != nil { t.Fatalf("DeleteBlob: %v", err) } if reg.BlobExists(digest) { t.Error("blob should not exist after delete") } } func TestNewCreatesDirectories(t *testing.T) { dir := filepath.Join(t.TempDir(), "oci-storage") reg, err := oci.New(dir) if err != nil { t.Fatal(err) } for _, sub := range []string{"blobs/sha256", "uploads"} { p := filepath.Join(dir, sub) if _, err := os.Stat(p); os.IsNotExist(err) { t.Errorf("directory not created: %s", p) } } _ = reg }