mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-27 20:54:20 +00:00
1370 lines
34 KiB
Go
1370 lines
34 KiB
Go
package atproto
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewManifestRecord(t *testing.T) {
|
|
validOCIManifest := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"config": {
|
|
"mediaType": "application/vnd.oci.image.config.v1+json",
|
|
"digest": "sha256:config123",
|
|
"size": 1234
|
|
},
|
|
"layers": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
"digest": "sha256:layer1",
|
|
"size": 5678
|
|
},
|
|
{
|
|
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
"digest": "sha256:layer2",
|
|
"size": 9012
|
|
}
|
|
],
|
|
"annotations": {
|
|
"org.opencontainers.image.created": "2025-01-01T00:00:00Z"
|
|
}
|
|
}`
|
|
|
|
manifestWithSubject := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"config": {
|
|
"mediaType": "application/vnd.oci.image.config.v1+json",
|
|
"digest": "sha256:config123",
|
|
"size": 1234
|
|
},
|
|
"layers": [],
|
|
"subject": {
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"digest": "sha256:subject123",
|
|
"size": 4321
|
|
}
|
|
}`
|
|
|
|
manifestList := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
|
"manifests": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"digest": "sha256:amd64manifest",
|
|
"size": 1000,
|
|
"platform": {
|
|
"architecture": "amd64",
|
|
"os": "linux"
|
|
}
|
|
},
|
|
{
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"digest": "sha256:arm64manifest",
|
|
"size": 1100,
|
|
"platform": {
|
|
"architecture": "arm64",
|
|
"os": "linux",
|
|
"variant": "v8"
|
|
}
|
|
}
|
|
]
|
|
}`
|
|
|
|
invalidBothFields := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
|
"config": {
|
|
"mediaType": "application/vnd.oci.image.config.v1+json",
|
|
"digest": "sha256:config123",
|
|
"size": 1234
|
|
},
|
|
"layers": [],
|
|
"manifests": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"digest": "sha256:amd64manifest",
|
|
"size": 1000
|
|
}
|
|
]
|
|
}`
|
|
|
|
invalidNoFields := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json"
|
|
}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
repository string
|
|
digest string
|
|
ociManifest string
|
|
wantErr bool
|
|
checkFunc func(*testing.T, *ManifestRecord)
|
|
}{
|
|
{
|
|
name: "valid OCI manifest",
|
|
repository: "myapp",
|
|
digest: "sha256:abc123",
|
|
ociManifest: validOCIManifest,
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, record *ManifestRecord) {
|
|
if record.Type != ManifestCollection {
|
|
t.Errorf("Type = %v, want %v", record.Type, ManifestCollection)
|
|
}
|
|
if record.Repository != "myapp" {
|
|
t.Errorf("Repository = %v, want myapp", record.Repository)
|
|
}
|
|
if record.Digest != "sha256:abc123" {
|
|
t.Errorf("Digest = %v, want sha256:abc123", record.Digest)
|
|
}
|
|
if record.SchemaVersion != 2 {
|
|
t.Errorf("SchemaVersion = %v, want 2", record.SchemaVersion)
|
|
}
|
|
if record.MediaType != "application/vnd.oci.image.manifest.v1+json" {
|
|
t.Errorf("MediaType = %v, want application/vnd.oci.image.manifest.v1+json", record.MediaType)
|
|
}
|
|
if record.Config.Digest != "sha256:config123" {
|
|
t.Errorf("Config.Digest = %v, want sha256:config123", record.Config.Digest)
|
|
}
|
|
if record.Config.Size != 1234 {
|
|
t.Errorf("Config.Size = %v, want 1234", record.Config.Size)
|
|
}
|
|
if len(record.Layers) != 2 {
|
|
t.Fatalf("len(Layers) = %v, want 2", len(record.Layers))
|
|
}
|
|
if record.Layers[0].Digest != "sha256:layer1" {
|
|
t.Errorf("Layers[0].Digest = %v, want sha256:layer1", record.Layers[0].Digest)
|
|
}
|
|
if record.Layers[1].Digest != "sha256:layer2" {
|
|
t.Errorf("Layers[1].Digest = %v, want sha256:layer2", record.Layers[1].Digest)
|
|
}
|
|
if record.Annotations["org.opencontainers.image.created"] != "2025-01-01T00:00:00Z" {
|
|
t.Errorf("Annotations missing expected key")
|
|
}
|
|
if record.CreatedAt.IsZero() {
|
|
t.Error("CreatedAt should not be zero")
|
|
}
|
|
if record.Subject != nil {
|
|
t.Error("Subject should be nil")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "manifest with subject",
|
|
repository: "myapp",
|
|
digest: "sha256:abc123",
|
|
ociManifest: manifestWithSubject,
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, record *ManifestRecord) {
|
|
if record.Subject == nil {
|
|
t.Fatal("Subject should not be nil")
|
|
}
|
|
if record.Subject.Digest != "sha256:subject123" {
|
|
t.Errorf("Subject.Digest = %v, want sha256:subject123", record.Subject.Digest)
|
|
}
|
|
if record.Subject.Size != 4321 {
|
|
t.Errorf("Subject.Size = %v, want 4321", record.Subject.Size)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
repository: "myapp",
|
|
digest: "sha256:abc123",
|
|
ociManifest: "not valid json",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid config JSON",
|
|
repository: "myapp",
|
|
digest: "sha256:abc123",
|
|
ociManifest: `{"schemaVersion": 2, "mediaType": "test", "config": "not-an-object", "layers": []}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid manifest list (multi-arch)",
|
|
repository: "myapp",
|
|
digest: "sha256:multiarch",
|
|
ociManifest: manifestList,
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, record *ManifestRecord) {
|
|
if record.MediaType != "application/vnd.oci.image.index.v1+json" {
|
|
t.Errorf("MediaType = %v, want application/vnd.oci.image.index.v1+json", record.MediaType)
|
|
}
|
|
if record.Config != nil {
|
|
t.Error("Config should be nil for manifest list")
|
|
}
|
|
if len(record.Layers) != 0 {
|
|
t.Errorf("Layers should be empty for manifest list, got %d", len(record.Layers))
|
|
}
|
|
if len(record.Manifests) != 2 {
|
|
t.Fatalf("Manifests should have 2 entries, got %d", len(record.Manifests))
|
|
}
|
|
|
|
// Check first manifest (amd64)
|
|
if record.Manifests[0].Digest != "sha256:amd64manifest" {
|
|
t.Errorf("Manifests[0].Digest = %v, want sha256:amd64manifest", record.Manifests[0].Digest)
|
|
}
|
|
if record.Manifests[0].Size != 1000 {
|
|
t.Errorf("Manifests[0].Size = %v, want 1000", record.Manifests[0].Size)
|
|
}
|
|
if record.Manifests[0].Platform == nil {
|
|
t.Fatal("Manifests[0].Platform should not be nil")
|
|
}
|
|
if record.Manifests[0].Platform.Architecture != "amd64" {
|
|
t.Errorf("Platform.Architecture = %v, want amd64", record.Manifests[0].Platform.Architecture)
|
|
}
|
|
if record.Manifests[0].Platform.OS != "linux" {
|
|
t.Errorf("Platform.OS = %v, want linux", record.Manifests[0].Platform.OS)
|
|
}
|
|
|
|
// Check second manifest (arm64)
|
|
if record.Manifests[1].Digest != "sha256:arm64manifest" {
|
|
t.Errorf("Manifests[1].Digest = %v, want sha256:arm64manifest", record.Manifests[1].Digest)
|
|
}
|
|
if record.Manifests[1].Platform.Architecture != "arm64" {
|
|
t.Errorf("Platform.Architecture = %v, want arm64", record.Manifests[1].Platform.Architecture)
|
|
}
|
|
if record.Manifests[1].Platform.Variant != "v8" {
|
|
t.Errorf("Platform.Variant = %v, want v8", record.Manifests[1].Platform.Variant)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid: both image and index fields",
|
|
repository: "myapp",
|
|
digest: "sha256:invalid",
|
|
ociManifest: invalidBothFields,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid: neither image nor index fields",
|
|
repository: "myapp",
|
|
digest: "sha256:invalid",
|
|
ociManifest: invalidNoFields,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NewManifestRecord(tt.repository, tt.digest, []byte(tt.ociManifest))
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("NewManifestRecord() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if !tt.wantErr && tt.checkFunc != nil {
|
|
tt.checkFunc(t, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewTagRecord(t *testing.T) {
|
|
did := "did:plc:test123"
|
|
before := time.Now()
|
|
record := NewTagRecord(did, "myapp", "latest", "sha256:abc123")
|
|
after := time.Now()
|
|
|
|
if record.Type != TagCollection {
|
|
t.Errorf("Type = %v, want %v", record.Type, TagCollection)
|
|
}
|
|
|
|
if record.Repository != "myapp" {
|
|
t.Errorf("Repository = %v, want myapp", record.Repository)
|
|
}
|
|
|
|
if record.Tag != "latest" {
|
|
t.Errorf("Tag = %v, want latest", record.Tag)
|
|
}
|
|
|
|
// New records should have manifest field (AT-URI)
|
|
expectedURI := "at://did:plc:test123/io.atcr.manifest/abc123"
|
|
if record.Manifest != expectedURI {
|
|
t.Errorf("Manifest = %v, want %v", record.Manifest, expectedURI)
|
|
}
|
|
|
|
// New records should NOT have manifestDigest field
|
|
if record.ManifestDigest != "" {
|
|
t.Errorf("ManifestDigest should be empty for new records, got %v", record.ManifestDigest)
|
|
}
|
|
|
|
if record.UpdatedAt.Before(before) || record.UpdatedAt.After(after) {
|
|
t.Errorf("UpdatedAt = %v, want between %v and %v", record.UpdatedAt, before, after)
|
|
}
|
|
}
|
|
|
|
func TestBuildManifestURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
did string
|
|
manifestDigest string
|
|
want string
|
|
}{
|
|
{
|
|
name: "standard digest",
|
|
did: "did:plc:abc123",
|
|
manifestDigest: "sha256:def456",
|
|
want: "at://did:plc:abc123/io.atcr.manifest/def456",
|
|
},
|
|
{
|
|
name: "long digest",
|
|
did: "did:web:hold.example.com",
|
|
manifestDigest: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
want: "at://did:web:hold.example.com/io.atcr.manifest/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := BuildManifestURI(tt.did, tt.manifestDigest)
|
|
if got != tt.want {
|
|
t.Errorf("BuildManifestURI() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseManifestURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
manifestURI string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid URI",
|
|
manifestURI: "at://did:plc:abc123/io.atcr.manifest/def456",
|
|
want: "sha256:def456",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid URI with did:web",
|
|
manifestURI: "at://did:web:hold.example.com/io.atcr.manifest/xyz789",
|
|
want: "sha256:xyz789",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid prefix",
|
|
manifestURI: "https://example.com/manifest",
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong collection",
|
|
manifestURI: "at://did:plc:abc123/io.atcr.tag/def456",
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "too few parts",
|
|
manifestURI: "at://did:plc:abc123/io.atcr.manifest",
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "too many parts",
|
|
manifestURI: "at://did:plc:abc123/io.atcr.manifest/def456/extra",
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseManifestURI(tt.manifestURI)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseManifestURI() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("ParseManifestURI() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTagRecord_GetManifestDigest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record TagRecord
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "new record with manifest field",
|
|
record: TagRecord{
|
|
Manifest: "at://did:plc:test123/io.atcr.manifest/abc123",
|
|
},
|
|
want: "sha256:abc123",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "old record with manifestDigest field",
|
|
record: TagRecord{
|
|
ManifestDigest: "sha256:def456",
|
|
},
|
|
want: "sha256:def456",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "prefers manifest over manifestDigest",
|
|
record: TagRecord{
|
|
Manifest: "at://did:plc:test123/io.atcr.manifest/abc123",
|
|
ManifestDigest: "sha256:def456",
|
|
},
|
|
want: "sha256:abc123",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "no fields set",
|
|
record: TagRecord{},
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid manifest URI",
|
|
record: TagRecord{
|
|
Manifest: "invalid-uri",
|
|
},
|
|
want: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.record.GetManifestDigest()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("GetManifestDigest() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("GetManifestDigest() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSailorProfileRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
defaultHold string
|
|
}{
|
|
{
|
|
name: "with default hold DID",
|
|
defaultHold: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "with default hold URL",
|
|
defaultHold: "https://hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "empty default hold",
|
|
defaultHold: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
before := time.Now()
|
|
record := NewSailorProfileRecord(tt.defaultHold)
|
|
after := time.Now()
|
|
|
|
if record.Type != SailorProfileCollection {
|
|
t.Errorf("Type = %v, want %v", record.Type, SailorProfileCollection)
|
|
}
|
|
|
|
if record.DefaultHold != tt.defaultHold {
|
|
t.Errorf("DefaultHold = %v, want %v", record.DefaultHold, tt.defaultHold)
|
|
}
|
|
|
|
if record.CreatedAt.Before(before) || record.CreatedAt.After(after) {
|
|
t.Errorf("CreatedAt = %v, want between %v and %v", record.CreatedAt, before, after)
|
|
}
|
|
|
|
if record.UpdatedAt.Before(before) || record.UpdatedAt.After(after) {
|
|
t.Errorf("UpdatedAt = %v, want between %v and %v", record.UpdatedAt, before, after)
|
|
}
|
|
|
|
// CreatedAt and UpdatedAt should be equal for new records
|
|
if !record.CreatedAt.Equal(record.UpdatedAt) {
|
|
t.Errorf("CreatedAt (%v) != UpdatedAt (%v)", record.CreatedAt, record.UpdatedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewStarRecord(t *testing.T) {
|
|
before := time.Now()
|
|
record := NewStarRecord("did:plc:alice123", "myapp")
|
|
after := time.Now()
|
|
|
|
if record.Type != StarCollection {
|
|
t.Errorf("Type = %v, want %v", record.Type, StarCollection)
|
|
}
|
|
|
|
if record.Subject.DID != "did:plc:alice123" {
|
|
t.Errorf("Subject.DID = %v, want did:plc:alice123", record.Subject.DID)
|
|
}
|
|
|
|
if record.Subject.Repository != "myapp" {
|
|
t.Errorf("Subject.Repository = %v, want myapp", record.Subject.Repository)
|
|
}
|
|
|
|
if record.CreatedAt.Before(before) || record.CreatedAt.After(after) {
|
|
t.Errorf("CreatedAt = %v, want between %v and %v", record.CreatedAt, before, after)
|
|
}
|
|
}
|
|
|
|
func TestStarRecordKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ownerDID string
|
|
repository string
|
|
wantPrefix string // Expected prefix for validation
|
|
}{
|
|
{
|
|
name: "simple key",
|
|
ownerDID: "did:plc:alice123",
|
|
repository: "myapp",
|
|
},
|
|
{
|
|
name: "long DID and repo",
|
|
ownerDID: "did:plc:abcdefghijklmnopqrstuvwxyz123456",
|
|
repository: "my-very-long-repository-name",
|
|
},
|
|
{
|
|
name: "special characters in repo",
|
|
ownerDID: "did:plc:alice123",
|
|
repository: "my-app_test.v1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
key := StarRecordKey(tt.ownerDID, tt.repository)
|
|
|
|
// Key should be non-empty
|
|
if key == "" {
|
|
t.Error("StarRecordKey() returned empty string")
|
|
}
|
|
|
|
// Key should be base64 URL-encoded (no padding)
|
|
if strings.Contains(key, "=") {
|
|
t.Errorf("StarRecordKey() = %v, should not contain padding", key)
|
|
}
|
|
|
|
// Should be deterministic
|
|
key2 := StarRecordKey(tt.ownerDID, tt.repository)
|
|
if key != key2 {
|
|
t.Errorf("StarRecordKey() not deterministic: %v != %v", key, key2)
|
|
}
|
|
|
|
// Should be different for different inputs
|
|
differentKey := StarRecordKey(tt.ownerDID, tt.repository+"different")
|
|
if key == differentKey {
|
|
t.Error("StarRecordKey() should be different for different inputs")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseStarRecordKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ownerDID string
|
|
repository string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid key",
|
|
ownerDID: "did:plc:alice123",
|
|
repository: "myapp",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "key with special characters",
|
|
ownerDID: "did:plc:alice123",
|
|
repository: "my-app_test.v1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "long values",
|
|
ownerDID: "did:plc:abcdefghijklmnopqrstuvwxyz123456",
|
|
repository: "my-very-long-repository-name",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Generate key
|
|
key := StarRecordKey(tt.ownerDID, tt.repository)
|
|
|
|
// Parse it back
|
|
gotDID, gotRepo, err := ParseStarRecordKey(key)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseStarRecordKey() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if !tt.wantErr {
|
|
if gotDID != tt.ownerDID {
|
|
t.Errorf("ParseStarRecordKey() DID = %v, want %v", gotDID, tt.ownerDID)
|
|
}
|
|
if gotRepo != tt.repository {
|
|
t.Errorf("ParseStarRecordKey() repository = %v, want %v", gotRepo, tt.repository)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseStarRecordKey_Invalid(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rkey string
|
|
}{
|
|
{
|
|
name: "invalid base64",
|
|
rkey: "not!!!valid!!!base64",
|
|
},
|
|
{
|
|
name: "no separator - base64 encoded text without slash",
|
|
rkey: "bm9zZXBhcmF0b3I", // base64 of "noseparator" (no "/" in the decoded value)
|
|
},
|
|
{
|
|
name: "empty string",
|
|
rkey: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, _, err := ParseStarRecordKey(tt.rkey)
|
|
if err == nil {
|
|
t.Error("ParseStarRecordKey() expected error for invalid input")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveHoldDIDFromURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
holdURL string
|
|
want string
|
|
}{
|
|
{
|
|
name: "https URL",
|
|
holdURL: "https://hold01.atcr.io",
|
|
want: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "http URL",
|
|
holdURL: "http://hold01.atcr.io",
|
|
want: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "URL with trailing slash",
|
|
holdURL: "https://hold01.atcr.io/",
|
|
want: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "URL with path",
|
|
holdURL: "https://hold01.atcr.io/some/path",
|
|
want: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "URL with port",
|
|
holdURL: "https://hold01.atcr.io:8080",
|
|
want: "did:web:hold01.atcr.io:8080",
|
|
},
|
|
{
|
|
name: "already a did:web",
|
|
holdURL: "did:web:hold01.atcr.io",
|
|
want: "did:web:hold01.atcr.io",
|
|
},
|
|
{
|
|
name: "already a did:plc",
|
|
holdURL: "did:plc:abc123",
|
|
want: "did:plc:abc123",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
holdURL: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "localhost",
|
|
holdURL: "http://localhost:8080",
|
|
want: "did:web:localhost:8080",
|
|
},
|
|
{
|
|
name: "IP address",
|
|
holdURL: "http://192.168.1.1:8080",
|
|
want: "did:web:192.168.1.1:8080",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ResolveHoldDIDFromURL(tt.holdURL)
|
|
if got != tt.want {
|
|
t.Errorf("ResolveHoldDIDFromURL() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsDID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
s string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid did:web",
|
|
s: "did:web:example.com",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid did:plc",
|
|
s: "did:plc:abc123",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid did:key",
|
|
s: "did:key:z6Mkfriq",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "not a DID - URL",
|
|
s: "https://example.com",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not a DID - short string",
|
|
s: "did",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not a DID - empty",
|
|
s: "",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not a DID - almost",
|
|
s: "did:",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not a DID - plain text",
|
|
s: "hello world",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsDID(tt.s)
|
|
if got != tt.want {
|
|
t.Errorf("IsDID() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManifestRecord_JSONSerialization(t *testing.T) {
|
|
// Create a manifest record
|
|
ociManifest := `{
|
|
"schemaVersion": 2,
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"config": {
|
|
"mediaType": "application/vnd.oci.image.config.v1+json",
|
|
"digest": "sha256:config123",
|
|
"size": 1234
|
|
},
|
|
"layers": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
"digest": "sha256:layer1",
|
|
"size": 5678
|
|
}
|
|
]
|
|
}`
|
|
|
|
record, err := NewManifestRecord("myapp", "sha256:abc123", []byte(ociManifest))
|
|
if err != nil {
|
|
t.Fatalf("NewManifestRecord() error = %v", err)
|
|
}
|
|
|
|
// Add hold DID
|
|
record.HoldDID = "did:web:hold01.atcr.io"
|
|
|
|
// Serialize to JSON
|
|
jsonData, err := json.Marshal(record)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() error = %v", err)
|
|
}
|
|
|
|
// Deserialize from JSON
|
|
var decoded ManifestRecord
|
|
if err := json.Unmarshal(jsonData, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
|
|
// Verify fields
|
|
if decoded.Type != record.Type {
|
|
t.Errorf("Type = %v, want %v", decoded.Type, record.Type)
|
|
}
|
|
if decoded.Repository != record.Repository {
|
|
t.Errorf("Repository = %v, want %v", decoded.Repository, record.Repository)
|
|
}
|
|
if decoded.Digest != record.Digest {
|
|
t.Errorf("Digest = %v, want %v", decoded.Digest, record.Digest)
|
|
}
|
|
if decoded.HoldDID != record.HoldDID {
|
|
t.Errorf("HoldDID = %v, want %v", decoded.HoldDID, record.HoldDID)
|
|
}
|
|
if decoded.Config.Digest != record.Config.Digest {
|
|
t.Errorf("Config.Digest = %v, want %v", decoded.Config.Digest, record.Config.Digest)
|
|
}
|
|
if len(decoded.Layers) != len(record.Layers) {
|
|
t.Errorf("len(Layers) = %v, want %v", len(decoded.Layers), len(record.Layers))
|
|
}
|
|
}
|
|
|
|
func TestBlobReference_JSONSerialization(t *testing.T) {
|
|
blob := BlobReference{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
Digest: "sha256:abc123",
|
|
Size: 12345,
|
|
URLs: []string{"https://s3.example.com/blob"},
|
|
Annotations: map[string]string{
|
|
"key": "value",
|
|
},
|
|
}
|
|
|
|
// Serialize
|
|
jsonData, err := json.Marshal(blob)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() error = %v", err)
|
|
}
|
|
|
|
// Deserialize
|
|
var decoded BlobReference
|
|
if err := json.Unmarshal(jsonData, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
|
|
// Verify
|
|
if decoded.MediaType != blob.MediaType {
|
|
t.Errorf("MediaType = %v, want %v", decoded.MediaType, blob.MediaType)
|
|
}
|
|
if decoded.Digest != blob.Digest {
|
|
t.Errorf("Digest = %v, want %v", decoded.Digest, blob.Digest)
|
|
}
|
|
if decoded.Size != blob.Size {
|
|
t.Errorf("Size = %v, want %v", decoded.Size, blob.Size)
|
|
}
|
|
}
|
|
|
|
func TestStarSubject_JSONSerialization(t *testing.T) {
|
|
subject := StarSubject{
|
|
DID: "did:plc:alice123",
|
|
Repository: "myapp",
|
|
}
|
|
|
|
// Serialize
|
|
jsonData, err := json.Marshal(subject)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() error = %v", err)
|
|
}
|
|
|
|
// Deserialize
|
|
var decoded StarSubject
|
|
if err := json.Unmarshal(jsonData, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
|
|
// Verify
|
|
if decoded.DID != subject.DID {
|
|
t.Errorf("DID = %v, want %v", decoded.DID, subject.DID)
|
|
}
|
|
if decoded.Repository != subject.Repository {
|
|
t.Errorf("Repository = %v, want %v", decoded.Repository, subject.Repository)
|
|
}
|
|
}
|
|
|
|
func TestRepositoryTagToRKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
repository string
|
|
tag string
|
|
want string
|
|
}{
|
|
{
|
|
name: "simple repository and tag",
|
|
repository: "myapp",
|
|
tag: "latest",
|
|
want: "myapp_latest",
|
|
},
|
|
{
|
|
name: "repository with slash",
|
|
repository: "org/myapp",
|
|
tag: "v1.0.0",
|
|
want: "org~myapp_v1.0.0",
|
|
},
|
|
{
|
|
name: "multiple slashes in repository",
|
|
repository: "github.com/user/repo",
|
|
tag: "main",
|
|
want: "github.com~user~repo_main",
|
|
},
|
|
{
|
|
name: "tag with version",
|
|
repository: "app",
|
|
tag: "v1.2.3",
|
|
want: "app_v1.2.3",
|
|
},
|
|
{
|
|
name: "repository with hyphen",
|
|
repository: "my-app",
|
|
tag: "prod",
|
|
want: "my-app_prod",
|
|
},
|
|
{
|
|
name: "empty repository",
|
|
repository: "",
|
|
tag: "latest",
|
|
want: "_latest",
|
|
},
|
|
{
|
|
name: "empty tag",
|
|
repository: "myapp",
|
|
tag: "",
|
|
want: "myapp_",
|
|
},
|
|
{
|
|
name: "both empty",
|
|
repository: "",
|
|
tag: "",
|
|
want: "_",
|
|
},
|
|
{
|
|
name: "complex repository with slash",
|
|
repository: "namespace/app",
|
|
tag: "v2.0",
|
|
want: "namespace~app_v2.0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := RepositoryTagToRKey(tt.repository, tt.tag)
|
|
if got != tt.want {
|
|
t.Errorf("RepositoryTagToRKey(%q, %q) = %q, want %q", tt.repository, tt.tag, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRKeyToRepositoryTag(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rkey string
|
|
wantRepository string
|
|
wantTag string
|
|
}{
|
|
{
|
|
name: "simple rkey",
|
|
rkey: "myapp_latest",
|
|
wantRepository: "myapp",
|
|
wantTag: "latest",
|
|
},
|
|
{
|
|
name: "repository with tilde (encoded slash)",
|
|
rkey: "org~myapp_v1.0.0",
|
|
wantRepository: "org/myapp",
|
|
wantTag: "v1.0.0",
|
|
},
|
|
{
|
|
name: "multiple tildes",
|
|
rkey: "github.com~user~repo_main",
|
|
wantRepository: "github.com/user/repo",
|
|
wantTag: "main",
|
|
},
|
|
{
|
|
name: "tag with underscore (splits on last underscore)",
|
|
rkey: "app_tag_with_underscore",
|
|
wantRepository: "app_tag_with",
|
|
wantTag: "underscore",
|
|
},
|
|
{
|
|
name: "repository with hyphen",
|
|
rkey: "my-app_prod",
|
|
wantRepository: "my-app",
|
|
wantTag: "prod",
|
|
},
|
|
{
|
|
name: "no underscore (treats as tag)",
|
|
rkey: "justtext",
|
|
wantRepository: "",
|
|
wantTag: "justtext",
|
|
},
|
|
{
|
|
name: "empty repository",
|
|
rkey: "_latest",
|
|
wantRepository: "",
|
|
wantTag: "latest",
|
|
},
|
|
{
|
|
name: "empty tag",
|
|
rkey: "myapp_",
|
|
wantRepository: "myapp",
|
|
wantTag: "",
|
|
},
|
|
{
|
|
name: "complex with tilde and multiple underscores",
|
|
rkey: "namespace~app_tag_with_underscore",
|
|
wantRepository: "namespace/app_tag_with",
|
|
wantTag: "underscore",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotRepository, gotTag := RKeyToRepositoryTag(tt.rkey)
|
|
if gotRepository != tt.wantRepository {
|
|
t.Errorf("RKeyToRepositoryTag(%q) repository = %q, want %q", tt.rkey, gotRepository, tt.wantRepository)
|
|
}
|
|
if gotTag != tt.wantTag {
|
|
t.Errorf("RKeyToRepositoryTag(%q) tag = %q, want %q", tt.rkey, gotTag, tt.wantTag)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepositoryTagRoundTrip(t *testing.T) {
|
|
// Test that converting to rkey and back gives original values
|
|
tests := []struct {
|
|
name string
|
|
repository string
|
|
tag string
|
|
}{
|
|
{"simple", "myapp", "latest"},
|
|
{"with slash", "org/myapp", "v1.0.0"},
|
|
{"multiple slashes", "github.com/user/repo", "main"},
|
|
{"with hyphen", "my-app", "prod"},
|
|
{"empty repository", "", "latest"},
|
|
{"empty tag", "myapp", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Convert to rkey
|
|
rkey := RepositoryTagToRKey(tt.repository, tt.tag)
|
|
|
|
// Convert back
|
|
gotRepository, gotTag := RKeyToRepositoryTag(rkey)
|
|
|
|
// Verify round-trip
|
|
if gotRepository != tt.repository {
|
|
t.Errorf("Round-trip repository = %q, want %q (via rkey %q)", gotRepository, tt.repository, rkey)
|
|
}
|
|
if gotTag != tt.tag {
|
|
t.Errorf("Round-trip tag = %q, want %q (via rkey %q)", gotTag, tt.tag, rkey)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewLayerRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
digest string
|
|
size int64
|
|
mediaType string
|
|
repository string
|
|
userDID string
|
|
userHandle string
|
|
}{
|
|
{
|
|
name: "standard layer",
|
|
digest: "sha256:abc123",
|
|
size: 1024,
|
|
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
repository: "myapp",
|
|
userDID: "did:plc:user123",
|
|
userHandle: "alice.bsky.social",
|
|
},
|
|
{
|
|
name: "large layer",
|
|
digest: "sha256:def456",
|
|
size: 1073741824, // 1GB
|
|
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
repository: "largeapp",
|
|
userDID: "did:plc:user456",
|
|
userHandle: "bob.example.com",
|
|
},
|
|
{
|
|
name: "empty values",
|
|
digest: "",
|
|
size: 0,
|
|
mediaType: "",
|
|
repository: "",
|
|
userDID: "",
|
|
userHandle: "",
|
|
},
|
|
{
|
|
name: "config layer",
|
|
digest: "sha256:config123",
|
|
size: 512,
|
|
mediaType: "application/vnd.oci.image.config.v1+json",
|
|
repository: "app/subapp",
|
|
userDID: "did:web:example.com",
|
|
userHandle: "charlie.tangled.io",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
record := NewLayerRecord(tt.digest, tt.size, tt.mediaType, tt.repository, tt.userDID, tt.userHandle)
|
|
|
|
// Verify all fields
|
|
if record == nil {
|
|
t.Fatal("NewLayerRecord() returned nil")
|
|
}
|
|
|
|
if record.Type != LayerCollection {
|
|
t.Errorf("Type = %q, want %q", record.Type, LayerCollection)
|
|
}
|
|
|
|
if record.Digest != tt.digest {
|
|
t.Errorf("Digest = %q, want %q", record.Digest, tt.digest)
|
|
}
|
|
|
|
if record.Size != tt.size {
|
|
t.Errorf("Size = %d, want %d", record.Size, tt.size)
|
|
}
|
|
|
|
if record.MediaType != tt.mediaType {
|
|
t.Errorf("MediaType = %q, want %q", record.MediaType, tt.mediaType)
|
|
}
|
|
|
|
if record.Repository != tt.repository {
|
|
t.Errorf("Repository = %q, want %q", record.Repository, tt.repository)
|
|
}
|
|
|
|
if record.UserDID != tt.userDID {
|
|
t.Errorf("UserDID = %q, want %q", record.UserDID, tt.userDID)
|
|
}
|
|
|
|
if record.UserHandle != tt.userHandle {
|
|
t.Errorf("UserHandle = %q, want %q", record.UserHandle, tt.userHandle)
|
|
}
|
|
|
|
// Verify CreatedAt is set and is a valid RFC3339 timestamp
|
|
if record.CreatedAt == "" {
|
|
t.Error("CreatedAt is empty")
|
|
}
|
|
|
|
// Parse to verify it's a valid timestamp
|
|
_, err := time.Parse(time.RFC3339, record.CreatedAt)
|
|
if err != nil {
|
|
t.Errorf("CreatedAt %q is not a valid RFC3339 timestamp: %v", record.CreatedAt, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewLayerRecordJSON(t *testing.T) {
|
|
// Test that LayerRecord can be marshaled/unmarshaled to/from JSON
|
|
record := NewLayerRecord(
|
|
"sha256:abc123",
|
|
1024,
|
|
"application/vnd.oci.image.layer.v1.tar+gzip",
|
|
"myapp",
|
|
"did:plc:user123",
|
|
"alice.bsky.social",
|
|
)
|
|
|
|
// Marshal to JSON
|
|
jsonData, err := json.Marshal(record)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() error = %v", err)
|
|
}
|
|
|
|
// Unmarshal back
|
|
var decoded LayerRecord
|
|
if err := json.Unmarshal(jsonData, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
|
|
// Verify fields match
|
|
if decoded.Type != record.Type {
|
|
t.Errorf("Type = %q, want %q", decoded.Type, record.Type)
|
|
}
|
|
if decoded.Digest != record.Digest {
|
|
t.Errorf("Digest = %q, want %q", decoded.Digest, record.Digest)
|
|
}
|
|
if decoded.Size != record.Size {
|
|
t.Errorf("Size = %d, want %d", decoded.Size, record.Size)
|
|
}
|
|
if decoded.MediaType != record.MediaType {
|
|
t.Errorf("MediaType = %q, want %q", decoded.MediaType, record.MediaType)
|
|
}
|
|
if decoded.Repository != record.Repository {
|
|
t.Errorf("Repository = %q, want %q", decoded.Repository, record.Repository)
|
|
}
|
|
if decoded.UserDID != record.UserDID {
|
|
t.Errorf("UserDID = %q, want %q", decoded.UserDID, record.UserDID)
|
|
}
|
|
if decoded.UserHandle != record.UserHandle {
|
|
t.Errorf("UserHandle = %q, want %q", decoded.UserHandle, record.UserHandle)
|
|
}
|
|
if decoded.CreatedAt != record.CreatedAt {
|
|
t.Errorf("CreatedAt = %q, want %q", decoded.CreatedAt, record.CreatedAt)
|
|
}
|
|
}
|
|
|
|
func TestNewRepoPageRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
repository string
|
|
description string
|
|
avatar *ATProtoBlobRef
|
|
}{
|
|
{
|
|
name: "with description only",
|
|
repository: "myapp",
|
|
description: "# My App\n\nA cool container image.",
|
|
avatar: nil,
|
|
},
|
|
{
|
|
name: "with avatar only",
|
|
repository: "another-app",
|
|
description: "",
|
|
avatar: &ATProtoBlobRef{
|
|
Type: "blob",
|
|
Ref: Link{Link: "bafyreiabc123"},
|
|
MimeType: "image/png",
|
|
Size: 1024,
|
|
},
|
|
},
|
|
{
|
|
name: "with both description and avatar",
|
|
repository: "full-app",
|
|
description: "This is a full description.",
|
|
avatar: &ATProtoBlobRef{
|
|
Type: "blob",
|
|
Ref: Link{Link: "bafyreiabc456"},
|
|
MimeType: "image/jpeg",
|
|
Size: 2048,
|
|
},
|
|
},
|
|
{
|
|
name: "empty values",
|
|
repository: "",
|
|
description: "",
|
|
avatar: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
before := time.Now()
|
|
record := NewRepoPageRecord(tt.repository, tt.description, tt.avatar)
|
|
after := time.Now()
|
|
|
|
if record.Type != RepoPageCollection {
|
|
t.Errorf("Type = %v, want %v", record.Type, RepoPageCollection)
|
|
}
|
|
|
|
if record.Repository != tt.repository {
|
|
t.Errorf("Repository = %v, want %v", record.Repository, tt.repository)
|
|
}
|
|
|
|
if record.Description != tt.description {
|
|
t.Errorf("Description = %v, want %v", record.Description, tt.description)
|
|
}
|
|
|
|
if tt.avatar == nil && record.Avatar != nil {
|
|
t.Error("Avatar should be nil")
|
|
}
|
|
|
|
if tt.avatar != nil {
|
|
if record.Avatar == nil {
|
|
t.Fatal("Avatar should not be nil")
|
|
}
|
|
if record.Avatar.Ref.Link != tt.avatar.Ref.Link {
|
|
t.Errorf("Avatar.Ref.Link = %v, want %v", record.Avatar.Ref.Link, tt.avatar.Ref.Link)
|
|
}
|
|
}
|
|
|
|
if record.CreatedAt.Before(before) || record.CreatedAt.After(after) {
|
|
t.Errorf("CreatedAt = %v, want between %v and %v", record.CreatedAt, before, after)
|
|
}
|
|
|
|
if record.UpdatedAt.Before(before) || record.UpdatedAt.After(after) {
|
|
t.Errorf("UpdatedAt = %v, want between %v and %v", record.UpdatedAt, before, after)
|
|
}
|
|
|
|
// CreatedAt and UpdatedAt should be equal for new records
|
|
if !record.CreatedAt.Equal(record.UpdatedAt) {
|
|
t.Errorf("CreatedAt (%v) != UpdatedAt (%v)", record.CreatedAt, record.UpdatedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepoPageRecord_JSONSerialization(t *testing.T) {
|
|
record := NewRepoPageRecord(
|
|
"myapp",
|
|
"# My App\n\nA description with **markdown**.",
|
|
&ATProtoBlobRef{
|
|
Type: "blob",
|
|
Ref: Link{Link: "bafyreiabc123"},
|
|
MimeType: "image/png",
|
|
Size: 1024,
|
|
},
|
|
)
|
|
|
|
// Serialize to JSON
|
|
jsonData, err := json.Marshal(record)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() error = %v", err)
|
|
}
|
|
|
|
// Deserialize from JSON
|
|
var decoded RepoPageRecord
|
|
if err := json.Unmarshal(jsonData, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
|
|
// Verify fields
|
|
if decoded.Type != record.Type {
|
|
t.Errorf("Type = %v, want %v", decoded.Type, record.Type)
|
|
}
|
|
if decoded.Repository != record.Repository {
|
|
t.Errorf("Repository = %v, want %v", decoded.Repository, record.Repository)
|
|
}
|
|
if decoded.Description != record.Description {
|
|
t.Errorf("Description = %v, want %v", decoded.Description, record.Description)
|
|
}
|
|
if decoded.Avatar == nil {
|
|
t.Fatal("Avatar should not be nil")
|
|
}
|
|
if decoded.Avatar.Ref.Link != record.Avatar.Ref.Link {
|
|
t.Errorf("Avatar.Ref.Link = %v, want %v", decoded.Avatar.Ref.Link, record.Avatar.Ref.Link)
|
|
}
|
|
}
|