package dynamodb_test import ( "context" "errors" "fmt" "os" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" guuid "github.com/gofrs/uuid" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" zerr "zotregistry.dev/zot/v2/errors" "zotregistry.dev/zot/v2/pkg/extensions/imagetrust" "zotregistry.dev/zot/v2/pkg/log" mdynamodb "zotregistry.dev/zot/v2/pkg/meta/dynamodb" proto_go "zotregistry.dev/zot/v2/pkg/meta/proto/gen" mTypes "zotregistry.dev/zot/v2/pkg/meta/types" reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext" . "zotregistry.dev/zot/v2/pkg/test/image-utils" tskip "zotregistry.dev/zot/v2/pkg/test/skip" ) const badTablename = "bad tablename" func TestIterator(t *testing.T) { tskip.SkipDynamo(t) const region = "us-east-2" endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT") uuid, err := guuid.NewV4() if err != nil { panic(err) } repoMetaTablename := "RepoMetadataTable" + uuid.String() versionTablename := "Version" + uuid.String() imageMetaTablename := "ImageMeta" + uuid.String() repoBlobsTablename := "RepoBlobs" + uuid.String() userDataTablename := "UserDataTable" + uuid.String() apiKeyTablename := "ApiKeyTable" + uuid.String() log := log.NewTestLogger() Convey("TestIterator", t, func() { params := mdynamodb.DBDriverParameters{ Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, VersionTablename: versionTablename, APIKeyTablename: apiKeyTablename, UserDataTablename: userDataTablename, } client, err := mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) dynamoWrapper, err := mdynamodb.New(client, params, log) So(err, ShouldBeNil) So(dynamoWrapper.ResetTable(dynamoWrapper.ImageMetaTablename), ShouldBeNil) So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil) err = dynamoWrapper.SetRepoReference(context.Background(), "repo1", "tag1", CreateRandomImage().AsImageMeta()) So(err, ShouldBeNil) err = dynamoWrapper.SetRepoReference(context.Background(), "repo2", "tag2", CreateRandomImage().AsImageMeta()) So(err, ShouldBeNil) err = dynamoWrapper.SetRepoReference(context.Background(), "repo3", "tag3", CreateRandomImage().AsImageMeta()) So(err, ShouldBeNil) repoMetaAttributeIterator := mdynamodb.NewBaseDynamoAttributesIterator( dynamoWrapper.Client, dynamoWrapper.RepoMetaTablename, "RepoMeta", 1, log, ) attribute, err := repoMetaAttributeIterator.First(context.Background()) So(err, ShouldBeNil) So(attribute, ShouldNotBeNil) attribute, err = repoMetaAttributeIterator.Next(context.Background()) So(err, ShouldBeNil) So(attribute, ShouldNotBeNil) attribute, err = repoMetaAttributeIterator.Next(context.Background()) So(err, ShouldBeNil) So(attribute, ShouldNotBeNil) attribute, err = repoMetaAttributeIterator.Next(context.Background()) So(err, ShouldBeNil) So(attribute, ShouldBeNil) }) } func TestIteratorErrors(t *testing.T) { Convey("errors", t, func() { badEndpoint := "endpoint" cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion("region")) So(err, ShouldBeNil) repoMetaAttributeIterator := mdynamodb.NewBaseDynamoAttributesIterator( dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) { o.BaseEndpoint = aws.String(badEndpoint) }), "RepoMetadataTable", "RepoMeta", 1, log.NewTestLogger(), ) _, err = repoMetaAttributeIterator.First(context.Background()) So(err, ShouldNotBeNil) }) } func TestWrapperErrors(t *testing.T) { tskip.SkipDynamo(t) const region = "us-east-2" endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT") uuid, err := guuid.NewV4() if err != nil { panic(err) } repoMetaTablename := "RepoMetadataTable" + uuid.String() versionTablename := "Version" + uuid.String() userDataTablename := "UserDataTable" + uuid.String() apiKeyTablename := "ApiKeyTable" + uuid.String() wrongTableName := "WRONG Tables" imageMetaTablename := "ImageMeta" + uuid.String() repoBlobsTablename := "RepoBlobs" + uuid.String() log := log.NewTestLogger() testDigest := godigest.FromString("str") image := CreateDefaultImage() multi := CreateMultiarchWith().Images([]Image{image}).Build() imageMeta := image.AsImageMeta() multiarchImageMeta := multi.AsImageMeta() badProtoBlob := []byte("bad-repo-meta") // goodRepoMetaBlob, err := proto.Marshal(&proto_go.RepoMeta{Name: "repo"}) // if err != nil { // t.FailNow() // } //nolint: contextcheck Convey("Errors", t, func() { params := mdynamodb.DBDriverParameters{ //nolint:contextcheck,staticcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, UserDataTablename: userDataTablename, APIKeyTablename: apiKeyTablename, VersionTablename: versionTablename, } client, err := mdynamodb.GetDynamoClient(params) //nolint:contextcheck So(err, ShouldBeNil) imgTrustStore, err := imagetrust.NewAWSImageTrustStore(params.Region, params.Endpoint) So(err, ShouldBeNil) dynamoWrapper, err := mdynamodb.New(client, params, log) //nolint:contextcheck So(err, ShouldBeNil) dynamoWrapper.SetImageTrustStore(imgTrustStore) So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil) //nolint:contextcheck So(dynamoWrapper.ResetTable(dynamoWrapper.RepoBlobsTablename), ShouldBeNil) //nolint:contextcheck So(dynamoWrapper.ResetTable(dynamoWrapper.ImageMetaTablename), ShouldBeNil) //nolint:contextcheck So(dynamoWrapper.ResetTable(dynamoWrapper.UserDataTablename), ShouldBeNil) //nolint:contextcheck userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("test") ctx := userAc.DeriveContext(context.Background()) Convey("RemoveRepoReference", func() { Convey("getProtoRepoMeta errors", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest) So(err, ShouldNotBeNil) }) Convey("getProtoImageMeta errors", func() { err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "tag": { MediaType: ispec.MediaTypeImageManifest, Digest: imageMeta.Digest.String(), }, }, }) So(err, ShouldBeNil) err = setImageMeta(imageMeta.Digest, badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest) So(err, ShouldNotBeNil) }) Convey("unmarshalProtoRepoBlobs errors", func() { err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) err = setRepoBlobInfo("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest) //nolint: contextcheck So(err, ShouldNotBeNil) }) }) Convey("FilterImageMeta", func() { Convey("FilterImageMeta with duplicate digests", func() { image := CreateRandomImage() err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta()) So(err, ShouldBeNil) _, err = dynamoWrapper.FilterImageMeta(ctx, []string{image.DigestStr(), image.DigestStr()}) So(err, ShouldNotBeNil) }) Convey("manifest meta unmarshal error", func() { err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.FilterImageMeta(ctx, []string{image.DigestStr()}) So(err, ShouldNotBeNil) }) Convey("MediaType ImageIndex, getProtoImageMeta fails", func() { err := dynamoWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta) //nolint: contextcheck So(err, ShouldBeNil) err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) // manifests are missing _, err = dynamoWrapper.FilterImageMeta(ctx, []string{multiarchImageMeta.Digest.String()}) So(err, ShouldNotBeNil) }) }) Convey("UpdateSignaturesValidity", func() { digest := image.Digest() Convey("image meta blob not found", func() { err := dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest) So(err, ShouldNotBeNil) }) Convey("UpdateSignaturesValidity with context done", func() { ctx, cancel := context.WithCancel(context.Background()) cancel() err := dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest) So(err, ShouldNotBeNil) }) Convey("image meta unmarshal fail", func() { err := setImageMeta(digest, badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest) So(err, ShouldNotBeNil) }) Convey("repo meta blob not found", func() { err := dynamoWrapper.SetImageMeta(digest, imageMeta) So(err, ShouldBeNil) err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest) So(err, ShouldNotBeNil) }) Convey("repo meta unmarshal fail", func() { err := dynamoWrapper.SetImageMeta(digest, imageMeta) So(err, ShouldBeNil) err = setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest) So(err, ShouldNotBeNil) }) }) Convey("UpdateStatsOnDownload", func() { Convey("unmarshalProtoRepoMeta error", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.UpdateStatsOnDownload("repo", "ref") So(err, ShouldNotBeNil) }) Convey("ref is tag and tag is not found", func() { err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) err = dynamoWrapper.UpdateStatsOnDownload("repo", "not-found-tag") //nolint: contextcheck So(err, ShouldNotBeNil) }) Convey("digest not found in statistics", func() { err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) err = dynamoWrapper.UpdateStatsOnDownload("repo", godigest.FromString("not-found").String()) //nolint: contextcheck So(err, ShouldNotBeNil) }) Convey("statistics entry missing but digest exists in tags - should create and increment", func() { // Set repo reference to create tag err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) // Manually remove Statistics entry to simulate missing Statistics repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) delete(repoMeta.Statistics, imageMeta.Digest.String()) // Set repo meta back without Statistics err = dynamoWrapper.SetRepoMeta("repo", repoMeta) So(err, ShouldBeNil) // Verify Statistics entry doesn't exist repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) _, exists := repoMeta.Statistics[imageMeta.Digest.String()] So(exists, ShouldBeFalse) // Update stats - should create Statistics entry and increment err = dynamoWrapper.UpdateStatsOnDownload("repo", "tag") //nolint: contextcheck So(err, ShouldBeNil) // Verify Statistics entry was created and incremented repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) stats, exists := repoMeta.Statistics[imageMeta.Digest.String()] So(exists, ShouldBeTrue) So(stats.DownloadCount, ShouldEqual, 1) // Update stats again - should increment existing entry err = dynamoWrapper.UpdateStatsOnDownload("repo", "tag") //nolint: contextcheck So(err, ShouldBeNil) repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) stats, exists = repoMeta.Statistics[imageMeta.Digest.String()] So(exists, ShouldBeTrue) So(stats.DownloadCount, ShouldEqual, 2) }) Convey("statistics entry missing but digest exists in tags - using digest reference", func() { // Set repo reference to create tag err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) // Manually remove Statistics entry repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) delete(repoMeta.Statistics, imageMeta.Digest.String()) // Set repo meta back without Statistics err = dynamoWrapper.SetRepoMeta("repo", repoMeta) So(err, ShouldBeNil) // Update stats using digest directly err = dynamoWrapper.UpdateStatsOnDownload("repo", imageMeta.Digest.String()) //nolint: contextcheck So(err, ShouldBeNil) // Verify Statistics entry was created repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) stats, exists := repoMeta.Statistics[imageMeta.Digest.String()] So(exists, ShouldBeTrue) So(stats.DownloadCount, ShouldEqual, 1) }) }) Convey("GetReferrersInfo", func() { Convey("unmarshalProtoRepoMeta error", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) _, err = dynamoWrapper.GetReferrersInfo("repo", "refDig", []string{}) So(err, ShouldNotBeNil) }) }) Convey("DecrementRepoStars", func() { Convey("unmarshalProtoRepoMeta error", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.DecrementRepoStars("repo") So(err, ShouldNotBeNil) }) }) Convey("IncrementRepoStars", func() { Convey("unmarshalProtoRepoMeta error", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.IncrementRepoStars("repo") So(err, ShouldNotBeNil) }) }) Convey("ResetRepoReferences", func() { Convey("repo doesn't exist - returns early without error", func() { // Verify repo doesn't exist _, err := dynamoWrapper.GetRepoMeta(ctx, "nonexistent-repo") So(err, ShouldNotBeNil) So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue) // ResetRepoReferences should return early without error err = dynamoWrapper.ResetRepoReferences("nonexistent-repo", nil) So(err, ShouldBeNil) // Verify repo still doesn't exist _, err = dynamoWrapper.GetRepoMeta(ctx, "nonexistent-repo") So(err, ShouldNotBeNil) So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue) }) Convey("repo doesn't exist with tagsToKeep - returns early without error", func() { // Verify repo doesn't exist _, err := dynamoWrapper.GetRepoMeta(ctx, "nonexistent-repo2") So(err, ShouldNotBeNil) So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue) // ResetRepoReferences should return early without error even with tagsToKeep tagsToKeep := map[string]bool{"tag1": true} err = dynamoWrapper.ResetRepoReferences("nonexistent-repo2", tagsToKeep) So(err, ShouldBeNil) // Verify repo still doesn't exist _, err = dynamoWrapper.GetRepoMeta(ctx, "nonexistent-repo2") So(err, ShouldNotBeNil) So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue) }) Convey("unmarshalProtoRepoMeta error", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) So(err, ShouldBeNil) err = dynamoWrapper.ResetRepoReferences("repo", nil) So(err, ShouldNotBeNil) }) Convey("preserve tags in tagsToKeep", func() { // Create repo with multiple tags image1 := CreateRandomImage() image2 := CreateRandomImage() err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) So(err, ShouldBeNil) // Wait a bit to ensure different timestamps time.Sleep(10 * time.Millisecond) err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) So(err, ShouldBeNil) // Get repo meta to capture TaggedTimestamp repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) So(repoMeta.Tags, ShouldContainKey, "tag1") So(repoMeta.Tags, ShouldContainKey, "tag2") tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp // Reset with only tag1 in tagsToKeep tagsToKeep := map[string]bool{"tag1": true} err = dynamoWrapper.ResetRepoReferences("repo", tagsToKeep) So(err, ShouldBeNil) // Verify tag1 is preserved with its timestamp repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) So(repoMeta.Tags, ShouldContainKey, "tag1") So(repoMeta.Tags, ShouldNotContainKey, "tag2") So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp) }) Convey("remove tags not in tagsToKeep", func() { // Create repo with multiple tags image1 := CreateRandomImage() image2 := CreateRandomImage() image3 := CreateRandomImage() err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) So(err, ShouldBeNil) err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) So(err, ShouldBeNil) err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag3", image3.AsImageMeta()) So(err, ShouldBeNil) // Reset with tag1 and tag2 in tagsToKeep tagsToKeep := map[string]bool{"tag1": true, "tag2": true} err = dynamoWrapper.ResetRepoReferences("repo", tagsToKeep) So(err, ShouldBeNil) // Verify only tag1 and tag2 are preserved repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) So(repoMeta.Tags, ShouldContainKey, "tag1") So(repoMeta.Tags, ShouldContainKey, "tag2") So(repoMeta.Tags, ShouldNotContainKey, "tag3") }) Convey("preserve statistics and stars", func() { image := CreateRandomImage() err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image.AsImageMeta()) So(err, ShouldBeNil) err = dynamoWrapper.IncrementRepoStars("repo") So(err, ShouldBeNil) // Get original stats repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) originalStars := repoMeta.StarCount // Reset with empty tagsToKeep err = dynamoWrapper.ResetRepoReferences("repo", map[string]bool{}) So(err, ShouldBeNil) // Verify statistics and stars are preserved repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") So(err, ShouldBeNil) So(repoMeta.StarCount, ShouldEqual, originalStars) }) }) Convey("GetMultipleRepoMeta", func() { Convey("repoMetaAttributeIterator.First fails", func() { dynamoWrapper.RepoMetaTablename = badTablename _, err := dynamoWrapper.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true }) So(err, ShouldNotBeNil) }) Convey("repo meta unmarshal fails", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true }) So(err, ShouldNotBeNil) }) }) Convey("GetImageMeta", func() { Convey("get image meta fails", func() { _, err := dynamoWrapper.GetImageMeta(testDigest) So(err, ShouldNotBeNil) }) Convey("image index, missing manifests are skipped gracefully", func() { err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta) So(err, ShouldBeNil) // Missing manifests are skipped gracefully, so GetImageMeta succeeds // but returns an index with no manifests imageMeta, err := dynamoWrapper.GetImageMeta(multiarchImageMeta.Digest) //nolint: contextcheck So(err, ShouldBeNil) So(len(imageMeta.Manifests), ShouldEqual, 0) }) }) Convey("GetFullImageMeta", func() { Convey("repo meta not found", func() { _, err := dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag") So(err, ShouldNotBeNil) }) Convey("unmarshalProtoRepoMeta fails", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag") So(err, ShouldNotBeNil) }) Convey("tag not found", func() { err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldBeNil) _, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag-not-found") So(err, ShouldNotBeNil) }) Convey("getProtoImageMeta fails", func() { err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "tag": { MediaType: ispec.MediaTypeImageManifest, Digest: godigest.FromString("not-found").String(), }, }, }) So(err, ShouldBeNil) _, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag") So(err, ShouldNotBeNil) }) Convey("image is index, missing manifests are skipped gracefully", func() { err := dynamoWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta) //nolint: contextcheck So(err, ShouldBeNil) err = dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "tag": { MediaType: ispec.MediaTypeImageIndex, Digest: multiarchImageMeta.Digest.String(), }, }, }) So(err, ShouldBeNil) // Missing manifests are skipped gracefully, so GetFullImageMeta succeeds // but returns an index with no manifests fullImageMeta, err := dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag") So(err, ShouldBeNil) So(len(fullImageMeta.Manifests), ShouldEqual, 0) }) }) Convey("FilterTags", func() { Convey("repoMetaAttributeIterator.First fails", func() { dynamoWrapper.RepoMetaTablename = badTablename _, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta) So(err, ShouldNotBeNil) }) Convey("repo meta unmarshal fails", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta) So(err, ShouldNotBeNil) }) Convey("found repo meta", func() { Convey("bad image manifest", func() { badImageDigest := godigest.FromString("bad-image-manifest") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "bad-image-manifest": { MediaType: ispec.MediaTypeImageManifest, Digest: badImageDigest.String(), }, }, }) So(err, ShouldBeNil) err = setImageMeta(badImageDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta) So(err, ShouldNotBeNil) }) Convey("bad image index", func() { badIndexDigest := godigest.FromString("bad-image-manifest") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "bad-image-index": { MediaType: ispec.MediaTypeImageIndex, Digest: badIndexDigest.String(), }, }, }) So(err, ShouldBeNil) err = setImageMeta(badIndexDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta) So(err, ShouldNotBeNil) }) Convey("good image index, bad inside manifest", func() { goodIndexBadManifestDigest := godigest.FromString("good-index-bad-manifests") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "good-index-bad-manifests": { MediaType: ispec.MediaTypeImageIndex, Digest: goodIndexBadManifestDigest.String(), }, }, }) So(err, ShouldBeNil) err = dynamoWrapper.SetImageMeta(goodIndexBadManifestDigest, multiarchImageMeta) //nolint: contextcheck So(err, ShouldBeNil) err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta) So(err, ShouldNotBeNil) }) }) }) Convey("SearchTags", func() { Convey("getProtoRepoMeta errors", func() { dynamoWrapper.RepoMetaTablename = badTablename _, err := dynamoWrapper.SearchTags(ctx, "repo") So(err, ShouldNotBeNil) }) Convey("found repo meta", func() { Convey("bad image manifest", func() { badImageDigest := godigest.FromString("bad-image-manifest") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "bad-image-manifest": { MediaType: ispec.MediaTypeImageManifest, Digest: badImageDigest.String(), }, }, }) So(err, ShouldBeNil) err = setImageMeta(badImageDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.SearchTags(ctx, "repo:") So(err, ShouldNotBeNil) }) Convey("bad image index", func() { badIndexDigest := godigest.FromString("bad-image-manifest") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "bad-image-index": { MediaType: ispec.MediaTypeImageIndex, Digest: badIndexDigest.String(), }, }, }) So(err, ShouldBeNil) err = setImageMeta(badIndexDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.SearchTags(ctx, "repo:") So(err, ShouldNotBeNil) }) Convey("good image index, bad inside manifest", func() { goodIndexBadManifestDigest := godigest.FromString("good-index-bad-manifests") err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "good-index-bad-manifests": { MediaType: ispec.MediaTypeImageIndex, Digest: goodIndexBadManifestDigest.String(), }, }, }) So(err, ShouldBeNil) err = dynamoWrapper.SetImageMeta(goodIndexBadManifestDigest, multiarchImageMeta) //nolint: contextcheck So(err, ShouldBeNil) err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.SearchTags(ctx, "repo:") So(err, ShouldNotBeNil) }) Convey("bad media type", func() { err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "mad-media-type": { MediaType: "bad media type", Digest: godigest.FromString("dig").String(), }, }, }) So(err, ShouldBeNil) _, err = dynamoWrapper.SearchTags(ctx, "repo:") So(err, ShouldBeNil) }) }) }) Convey("SearchRepos", func() { Convey("repoMetaAttributeIterator.First errors", func() { dynamoWrapper.RepoMetaTablename = badTablename _, err := dynamoWrapper.SearchRepos(ctx, "repo") So(err, ShouldNotBeNil) }) Convey("repo meta unmarshal errors", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.SearchRepos(ctx, "repo") So(err, ShouldNotBeNil) }) }) Convey("SetRepoReference", func() { Convey("SetProtoImageMeta fails", func() { dynamoWrapper.ImageMetaTablename = badTablename err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta()) So(err, ShouldNotBeNil) }) Convey("getProtoRepoMeta fails", func() { dynamoWrapper.RepoMetaTablename = badTablename err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta()) So(err, ShouldNotBeNil) }) Convey("getProtoRepoBlobs fails", func() { dynamoWrapper.RepoBlobsTablename = badTablename err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta()) So(err, ShouldNotBeNil) }) Convey("setRepoBlobsInfo fails", func() { // First set up image meta and repo meta successfully err := dynamoWrapper.SetImageMeta(imageMeta.Digest, imageMeta) //nolint: contextcheck So(err, ShouldBeNil) // Set up repo meta manually so getProtoRepoMeta succeeds err = dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", }) So(err, ShouldBeNil) // Set up repo blobs manually so getProtoRepoBlobs succeeds repoBlobs := &proto_go.RepoBlobs{ Name: "repo", } repoBlobsBytes, err := proto.Marshal(repoBlobs) So(err, ShouldBeNil) err = setRepoBlobInfo("repo", repoBlobsBytes, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) // Now set bad table name to cause setRepoBlobsInfo to fail dynamoWrapper.RepoBlobsTablename = badTablename err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldNotBeNil) }) Convey("setProtoRepoMeta fails", func() { // First set up image meta and repo blobs successfully err := dynamoWrapper.SetImageMeta(imageMeta.Digest, imageMeta) //nolint: contextcheck So(err, ShouldBeNil) // Set up repo meta manually so getProtoRepoMeta succeeds err = dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck Name: "repo", }) So(err, ShouldBeNil) // Set up repo blobs manually so getProtoRepoBlobs succeeds repoBlobs := &proto_go.RepoBlobs{ Name: "repo", } repoBlobsBytes, err := proto.Marshal(repoBlobs) So(err, ShouldBeNil) err = setRepoBlobInfo("repo", repoBlobsBytes, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) // Now set bad table name to cause setProtoRepoMeta to fail dynamoWrapper.RepoMetaTablename = badTablename err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta) So(err, ShouldNotBeNil) }) }) Convey("GetProtoImageMeta", func() { Convey("Get request fails", func() { dynamoWrapper.ImageMetaTablename = badTablename _, err := dynamoWrapper.GetProtoImageMeta(ctx, testDigest) So(err, ShouldNotBeNil) }) Convey("unmarshal fails", func() { err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck So(err, ShouldBeNil) _, err = dynamoWrapper.GetProtoImageMeta(ctx, testDigest) So(err, ShouldNotBeNil) }) }) Convey("SetUserData", func() { hashKey := "id" apiKeys := make(map[string]mTypes.APIKeyDetails) apiKeyDetails := mTypes.APIKeyDetails{ Label: "apiKey", Scopes: []string{"repo"}, UUID: hashKey, } apiKeys[hashKey] = apiKeyDetails userProfileSrc := mTypes.UserData{ Groups: []string{"group1", "group2"}, APIKeys: apiKeys, } err := dynamoWrapper.SetUserData(ctx, userProfileSrc) So(err, ShouldBeNil) userAc := reqCtx.NewUserAccessControl() ctx := userAc.DeriveContext(context.Background()) err = dynamoWrapper.SetUserData(ctx, mTypes.UserData{}) //nolint: contextcheck So(err, ShouldNotBeNil) }) Convey("DeleteUserData", func() { err := dynamoWrapper.DeleteUserData(ctx) So(err, ShouldBeNil) userAc := reqCtx.NewUserAccessControl() ctx := userAc.DeriveContext(context.Background()) err = dynamoWrapper.DeleteUserData(ctx) //nolint: contextcheck So(err, ShouldNotBeNil) }) Convey("ToggleBookmarkRepo no access", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) _, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "unaccesible") So(err, ShouldNotBeNil) }) Convey("ToggleBookmarkRepo GetUserMeta no user data", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": true, }) ctx := userAc.DeriveContext(context.Background()) status, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "repo") So(err, ShouldBeNil) So(status, ShouldEqual, mTypes.Added) }) Convey("ToggleBookmarkRepo GetUserMeta client error", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) dynamoWrapper.UserDataTablename = badTablename status, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "repo") So(err, ShouldNotBeNil) So(status, ShouldEqual, mTypes.NotChanged) }) Convey("GetBookmarkedRepos", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) repos, err := dynamoWrapper.GetBookmarkedRepos(ctx) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) }) Convey("ToggleStarRepo GetUserMeta bad context", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") _, err := dynamoWrapper.ToggleStarRepo(ctx, "repo") So(err, ShouldNotBeNil) }) Convey("ToggleStarRepo GetUserMeta no access", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) _, err := dynamoWrapper.ToggleStarRepo(ctx, "unaccesible") So(err, ShouldNotBeNil) }) Convey("ToggleStarRepo GetUserMeta error", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) dynamoWrapper.UserDataTablename = badTablename _, err := dynamoWrapper.ToggleStarRepo(ctx, "repo") So(err, ShouldNotBeNil) }) Convey("ToggleStarRepo GetRepoMeta error", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": true, }) ctx := userAc.DeriveContext(context.Background()) dynamoWrapper.RepoMetaTablename = badTablename _, err := dynamoWrapper.ToggleStarRepo(ctx, "repo") So(err, ShouldNotBeNil) }) Convey("GetUserData bad context", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") userData, err := dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) So(userData.BookmarkedRepos, ShouldBeEmpty) So(userData.StarredRepos, ShouldBeEmpty) }) Convey("GetUserData client error", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": true, }) ctx := userAc.DeriveContext(context.Background()) dynamoWrapper.UserDataTablename = badTablename _, err := dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) }) Convey("GetUserMeta unmarshal error, bad user data", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("username") userAc.SetGlobPatterns("read", map[string]bool{ "repo": true, }) ctx := userAc.DeriveContext(context.Background()) err := setBadUserData(dynamoWrapper.Client, userDataTablename, userAc.GetUsername()) So(err, ShouldBeNil) _, err = dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) }) Convey("SetUserData bad context", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{}) So(err, ShouldNotBeNil) }) Convey("GetUserData bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") _, err := dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) }) Convey("SetUserData bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{}) So(err, ShouldNotBeNil) }) Convey("AddUserAPIKey bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{}) So(err, ShouldNotBeNil) }) Convey("DeleteUserAPIKey bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.DeleteUserAPIKey(ctx, "") So(err, ShouldNotBeNil) }) Convey("UpdateUserAPIKeyLastUsed bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.UpdateUserAPIKeyLastUsed(ctx, "") So(err, ShouldNotBeNil) }) Convey("DeleteUserData bad context errors", func() { uacKey := reqCtx.GetContextKey() ctx := context.WithValue(context.Background(), uacKey, "bad context") err := dynamoWrapper.DeleteUserData(ctx) So(err, ShouldNotBeNil) }) Convey("GetRepoLastUpdated", func() { Convey("bad table", func() { dynamoWrapper.RepoBlobsTablename = "bad-table" lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("unmarshal error", func() { err := setRepoLastUpdated("repo", []byte("bad-blob"), dynamoWrapper) So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("item doesn't exist", func() { // Delete the repo to ensure item doesn't exist err := dynamoWrapper.DeleteRepoMeta("nonexistent-repo") So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("nonexistent-repo") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("item exists but RepoLastUpdated attribute missing", func() { // Create an item in RepoBlobsTablename without RepoLastUpdated attribute // by setting RepoBlobsInfo only repoBlobs := &proto_go.RepoBlobs{ Name: "repo-no-timestamp", } repoBlobsBytes, err := proto.Marshal(repoBlobs) So(err, ShouldBeNil) err = setRepoBlobInfo("repo-no-timestamp", repoBlobsBytes, dynamoWrapper) So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo-no-timestamp") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("empty blob", func() { // Set an empty blob for RepoLastUpdated err := setRepoLastUpdated("repo-empty-blob", []byte{}, dynamoWrapper) So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo-empty-blob") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("zero timestamp", func() { // Set a zero timestamp zeroTime := ×tamppb.Timestamp{ Seconds: 0, Nanos: 0, } zeroTimeBlob, err := proto.Marshal(zeroTime) So(err, ShouldBeNil) err = setRepoLastUpdated("repo-zero-timestamp", zeroTimeBlob, dynamoWrapper) So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo-zero-timestamp") So(lastUpdated, ShouldEqual, time.Time{}) }) Convey("valid timestamp", func() { // Set a valid timestamp validTime := timestamppb.New(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)) validTimeBlob, err := proto.Marshal(validTime) So(err, ShouldBeNil) err = setRepoLastUpdated("repo-valid-timestamp", validTimeBlob, dynamoWrapper) So(err, ShouldBeNil) lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo-valid-timestamp") So(lastUpdated, ShouldNotEqual, time.Time{}) So(lastUpdated.Year(), ShouldEqual, 2024) So(lastUpdated.Month(), ShouldEqual, time.January) So(lastUpdated.Day(), ShouldEqual, 1) }) }) Convey("DeleteUserAPIKey returns nil", func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("email") ctx := userAc.DeriveContext(context.Background()) apiKeyDetails := make(map[string]mTypes.APIKeyDetails) apiKeyDetails["id"] = mTypes.APIKeyDetails{ UUID: "id", } err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{ APIKeys: apiKeyDetails, }) So(err, ShouldBeNil) dynamoWrapper.APIKeyTablename = wrongTableName err = dynamoWrapper.DeleteUserAPIKey(ctx, "id") So(err, ShouldNotBeNil) }) Convey("AddUserAPIKey", func() { Convey("no userid found", func() { userAc := reqCtx.NewUserAccessControl() ctx := userAc.DeriveContext(context.Background()) err = dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{}) So(err, ShouldNotBeNil) }) userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("email") ctx := userAc.DeriveContext(context.Background()) err := dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{}) So(err, ShouldBeNil) dynamoWrapper.APIKeyTablename = wrongTableName err = dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{}) So(err, ShouldNotBeNil) }) Convey("GetUserAPIKeyInfo", func() { dynamoWrapper.APIKeyTablename = wrongTableName _, err := dynamoWrapper.GetUserAPIKeyInfo("key") So(err, ShouldNotBeNil) }) Convey("GetUserData", func() { userAc := reqCtx.NewUserAccessControl() ctx := userAc.DeriveContext(context.Background()) _, err := dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) userAc = reqCtx.NewUserAccessControl() userAc.SetUsername("email") ctx = userAc.DeriveContext(context.Background()) dynamoWrapper.UserDataTablename = wrongTableName _, err = dynamoWrapper.GetUserData(ctx) So(err, ShouldNotBeNil) }) Convey("PatchDB dwr.getDBVersion errors", func() { dynamoWrapper.VersionTablename = badTablename err := dynamoWrapper.PatchDB() So(err, ShouldNotBeNil) }) Convey("PatchDB patchIndex < version.GetVersionIndex", func() { err := setVersion(dynamoWrapper.Client, versionTablename, "V2") So(err, ShouldBeNil) dynamoWrapper.Patches = []func(client *dynamodb.Client, tableNames map[string]string) error{ func(client *dynamodb.Client, tableNames map[string]string) error { return nil }, func(client *dynamodb.Client, tableNames map[string]string) error { return nil }, func(client *dynamodb.Client, tableNames map[string]string) error { return nil }, } err = dynamoWrapper.PatchDB() So(err, ShouldBeNil) }) Convey("ResetRepoMetaTable client errors", func() { dynamoWrapper.RepoMetaTablename = badTablename err := dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename) So(err, ShouldNotBeNil) }) Convey("getDBVersion client errors", func() { dynamoWrapper.VersionTablename = badTablename err := dynamoWrapper.PatchDB() So(err, ShouldNotBeNil) }) }) Convey("NewDynamoDBWrapper errors", t, func() { params := mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: "", ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, UserDataTablename: userDataTablename, APIKeyTablename: apiKeyTablename, VersionTablename: versionTablename, } client, err := mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) params = mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: "", RepoBlobsInfoTablename: repoBlobsTablename, UserDataTablename: userDataTablename, APIKeyTablename: apiKeyTablename, VersionTablename: versionTablename, } client, err = mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) params = mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: "", UserDataTablename: userDataTablename, APIKeyTablename: apiKeyTablename, VersionTablename: versionTablename, } client, err = mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) params = mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, UserDataTablename: userDataTablename, APIKeyTablename: apiKeyTablename, VersionTablename: "", } client, err = mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) params = mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, VersionTablename: versionTablename, UserDataTablename: "", APIKeyTablename: apiKeyTablename, } client, err = mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) params = mdynamodb.DBDriverParameters{ //nolint:contextcheck Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, VersionTablename: versionTablename, UserDataTablename: userDataTablename, APIKeyTablename: "", } client, err = mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) _, err = mdynamodb.New(client, params, log) So(err, ShouldNotBeNil) }) } func setRepoMeta(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { //nolint: unparam userAttributeValue, err := attributevalue.Marshal(blob) if err != nil { return err } _, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#RM": "RepoMeta", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":RepoMeta": userAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: repo, }, }, TableName: aws.String(dynamoWrapper.RepoMetaTablename), UpdateExpression: aws.String("SET #RM = :RepoMeta"), }) return err } func setRepoLastUpdated(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { //nolint: unparam lastUpdatedAttributeValue, err := attributevalue.Marshal(blob) if err != nil { return err } _, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#RLU": "RepoLastUpdated", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":RepoLastUpdated": lastUpdatedAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: repo, }, }, TableName: aws.String(dynamoWrapper.RepoBlobsTablename), UpdateExpression: aws.String("SET #RLU = :RepoLastUpdated"), }) return err } func setRepoBlobInfo(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { userAttributeValue, err := attributevalue.Marshal(blob) if err != nil { return err } _, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#RB": "RepoBlobsInfo", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":RepoBlobsInfo": userAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: repo, }, }, TableName: aws.String(dynamoWrapper.RepoBlobsTablename), UpdateExpression: aws.String("SET #RB = :RepoBlobsInfo"), }) return err } func setImageMeta(digest godigest.Digest, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { userAttributeValue, err := attributevalue.Marshal(blob) if err != nil { return err } _, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#IM": "ImageMeta", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":ImageMeta": userAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: digest.String(), }, }, TableName: aws.String(dynamoWrapper.ImageMetaTablename), UpdateExpression: aws.String("SET #IM = :ImageMeta"), }) return err } func setBadUserData(client *dynamodb.Client, userDataTablename, userID string) error { userAttributeValue, err := attributevalue.Marshal("string") if err != nil { return err } _, err = client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#UM": "UserData", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":UserData": userAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: userID, }, }, TableName: aws.String(userDataTablename), UpdateExpression: aws.String("SET #UM = :UserData"), }) return err } func setVersion(client *dynamodb.Client, versionTablename string, version string) error { mdAttributeValue, err := attributevalue.Marshal(version) if err != nil { return err } _, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{ ExpressionAttributeNames: map[string]string{ "#V": "Version", }, ExpressionAttributeValues: map[string]types.AttributeValue{ ":Version": mdAttributeValue, }, Key: map[string]types.AttributeValue{ "TableKey": &types.AttributeValueMemberS{ Value: "DBVersion", }, }, TableName: aws.String(versionTablename), UpdateExpression: aws.String("SET #V = :Version"), }) return err } func TestDynamoDBCountRepos(t *testing.T) { tskip.SkipDynamo(t) const region = "us-east-2" endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT") uuid, err := guuid.NewV4() if err != nil { panic(err) } repoMetaTablename := "RepoMetadataTable" + uuid.String() versionTablename := "Version" + uuid.String() imageMetaTablename := "ImageMeta" + uuid.String() repoBlobsTablename := "RepoBlobs" + uuid.String() userDataTablename := "UserDataTable" + uuid.String() apiKeyTablename := "ApiKeyTable" + uuid.String() log := log.NewTestLogger() Convey("CountRepos", t, func() { params := mdynamodb.DBDriverParameters{ Endpoint: endpoint, Region: region, RepoMetaTablename: repoMetaTablename, ImageMetaTablename: imageMetaTablename, RepoBlobsInfoTablename: repoBlobsTablename, VersionTablename: versionTablename, APIKeyTablename: apiKeyTablename, UserDataTablename: userDataTablename, } client, err := mdynamodb.GetDynamoClient(params) So(err, ShouldBeNil) dynamoWrapper, err := mdynamodb.New(client, params, log) So(err, ShouldBeNil) So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil) So(dynamoWrapper.ResetTable(dynamoWrapper.ImageMetaTablename), ShouldBeNil) ctx := context.Background() Convey("returns 0 on empty table", func() { count, err := dynamoWrapper.CountRepos(ctx) So(err, ShouldBeNil) So(count, ShouldEqual, 0) }) Convey("returns correct count after adding repos", func() { for i := range 3 { err := dynamoWrapper.SetRepoReference(ctx, fmt.Sprintf("repo%d", i), "tag", CreateRandomImage().AsImageMeta()) So(err, ShouldBeNil) } count, err := dynamoWrapper.CountRepos(ctx) So(err, ShouldBeNil) So(count, ShouldEqual, 3) }) }) }