diff --git a/src/OrasProject.Oras/Exceptions/InvalidReferenceException.cs b/src/OrasProject.Oras/Exceptions/InvalidReferenceException.cs
index 886cad2..7916646 100644
--- a/src/OrasProject.Oras/Exceptions/InvalidReferenceException.cs
+++ b/src/OrasProject.Oras/Exceptions/InvalidReferenceException.cs
@@ -16,7 +16,7 @@
namespace OrasProject.Oras.Exceptions;
///
-/// InvalidReferenceException is thrown when the reference is invlid
+/// InvalidReferenceException is thrown when the reference is invalid.
///
public class InvalidReferenceException : FormatException
{
diff --git a/src/OrasProject.Oras/Exceptions/InvalidResponseException.cs b/src/OrasProject.Oras/Exceptions/InvalidResponseException.cs
new file mode 100644
index 0000000..dd525d1
--- /dev/null
+++ b/src/OrasProject.Oras/Exceptions/InvalidResponseException.cs
@@ -0,0 +1,36 @@
+// Copyright The ORAS Authors.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+
+namespace OrasProject.Oras.Exceptions;
+
+///
+/// InvalidResponseException is thrown when the response is invalid.
+///
+public class InvalidResponseException : FormatException
+{
+ public InvalidResponseException()
+ {
+ }
+
+ public InvalidResponseException(string? message)
+ : base(message)
+ {
+ }
+
+ public InvalidResponseException(string? message, Exception? inner)
+ : base(message, inner)
+ {
+ }
+}
diff --git a/src/OrasProject.Oras/Registry/Reference.cs b/src/OrasProject.Oras/Registry/Reference.cs
index f52ca3a..d7b7de1 100644
--- a/src/OrasProject.Oras/Registry/Reference.cs
+++ b/src/OrasProject.Oras/Registry/Reference.cs
@@ -200,7 +200,7 @@ public static bool TryParse(string reference, [NotNullWhen(true)] out Reference?
return false;
}
}
-
+
public Reference(Reference other)
{
if (other == null)
diff --git a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
index efae9e9..8877412 100644
--- a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
+++ b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
@@ -14,8 +14,6 @@
using OrasProject.Oras.Exceptions;
using OrasProject.Oras.Oci;
using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -274,7 +272,7 @@ private async Task UpdateReferrersIndex(Descriptor subject,
{
// 1. pull the original referrers index list using referrers tag schema
var referrersTag = Referrers.BuildReferrersTag(subject);
- var (oldDesc, oldReferrers) = await PullReferrersIndexList(referrersTag, cancellationToken).ConfigureAwait(false);
+ var (oldDesc, oldReferrers) = await Repository.PullReferrersIndexList(referrersTag, cancellationToken).ConfigureAwait(false);
// 2. apply the referrer change to referrers list
var (updatedReferrers, updateRequired) =
@@ -309,34 +307,6 @@ private async Task UpdateReferrersIndex(Descriptor subject,
await DeleteAsync(oldDesc, cancellationToken).ConfigureAwait(false);
}
- ///
- /// PullReferrersIndexList retrieves the referrers index list associated with the given referrers tag.
- /// It fetches the index manifest from the repository, deserializes it into an `Index` object,
- /// and returns the descriptor along with the list of manifests (referrers). If the referrers index is not found,
- /// an empty descriptor and an empty list are returned.
- ///
- ///
- ///
- ///
- internal async Task<(Descriptor?, IList)> PullReferrersIndexList(String referrersTag, CancellationToken cancellationToken = default)
- {
- try
- {
- var (desc, content) = await FetchAsync(referrersTag, cancellationToken).ConfigureAwait(false);
- var index = JsonSerializer.Deserialize(content);
- if (index == null)
- {
- throw new JsonException($"null index manifests list when pulling referrers index list for referrers tag {referrersTag}");
- }
- return (desc, index.Manifests);
- }
- catch (NotFoundException)
- {
- return (null, ImmutableArray.Empty);
- }
- }
-
-
///
/// Pushes the manifest content, matching the expected descriptor.
///
diff --git a/src/OrasProject.Oras/Registry/Remote/Referrers.cs b/src/OrasProject.Oras/Registry/Remote/Referrers.cs
index 951650e..6efca7a 100644
--- a/src/OrasProject.Oras/Registry/Remote/Referrers.cs
+++ b/src/OrasProject.Oras/Registry/Remote/Referrers.cs
@@ -12,6 +12,7 @@
// limitations under the License.
using System.Collections.Generic;
+using System.Linq;
using OrasProject.Oras.Content;
using OrasProject.Oras.Oci;
@@ -105,4 +106,40 @@ internal static (IList, bool) ApplyReferrerChanges(IList
return (updatedReferrers, updateRequired);
}
+
+ ///
+ /// IsReferrersFilterApplied checks if requstedFilter is in the applied filters list.
+ ///
+ ///
+ ///
+ ///
+ internal static bool IsReferrersFilterApplied(string? appliedFilters, string requestedFilter) {
+ if (string.IsNullOrEmpty(appliedFilters) || string.IsNullOrEmpty(requestedFilter))
+ {
+ return false;
+ }
+
+ var filters = appliedFilters.Split(",");
+ foreach (var filter in filters)
+ {
+ if (filter == requestedFilter)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// FilterReferrers filters out a list of referrers based on the specified artifact type
+ ///
+ ///
+ ///
+ ///
+ internal static IList FilterReferrers(IList referrers, string? artifactType)
+ {
+ return string.IsNullOrEmpty(artifactType) ? referrers : referrers.Where(referrer => referrer.ArtifactType == artifactType).ToList();
+ }
}
+
diff --git a/src/OrasProject.Oras/Registry/Remote/Repository.cs b/src/OrasProject.Oras/Registry/Remote/Repository.cs
index a3a59c1..bd423ea 100644
--- a/src/OrasProject.Oras/Registry/Remote/Repository.cs
+++ b/src/OrasProject.Oras/Registry/Remote/Repository.cs
@@ -15,6 +15,7 @@
using OrasProject.Oras.Oci;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Net;
@@ -25,6 +26,8 @@
using System.Threading;
using System.Threading.Tasks;
using System.Web;
+using OrasProject.Oras.Content;
+using Index = OrasProject.Oras.Oci.Index;
namespace OrasProject.Oras.Registry.Remote;
@@ -48,6 +51,14 @@ public class Repository : IRepository
public RepositoryOptions Options => _opts;
private int _referrersState = (int) Referrers.ReferrersState.Unknown;
+
+ ///
+ /// _filterTypeArtifactType is the "artifactType" filter applied on the list of referrers.
+ ///
+ /// References:
+ /// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#listing-referrers
+ ///
+ private const string _filterTypeArtifactType = "artifactType";
///
/// ReferrersState indicates the Referrers API state of the remote repository.
@@ -66,6 +77,21 @@ internal Referrers.ReferrersState ReferrersState
}
}
+ ///
+ /// ReferrerListPageSize specifies the page size when invoking the Referrers API.
+ /// If zero, the page size is determined by the remote registry.
+ /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#listing-referrers
+ ///
+ public int ReferrerListPageSize;
+
+ ///
+ /// _headerOciFiltersApplied is the "OCI-Filters-Applied" header.
+ /// If present on the response, it contains a comma-separated list of the applied filters.
+ /// Reference:
+ /// - https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#listing-referrers
+ ///
+ private const string _headerOciFiltersApplied = "OCI-Filters-Applied";
+
internal static readonly string[] DefaultManifestMediaTypes =
[
Docker.MediaType.Manifest,
@@ -371,6 +397,195 @@ internal Reference ParseReferenceFromContentReference(string reference)
public async Task MountAsync(Descriptor descriptor, string fromRepository, Func>? getContent = null, CancellationToken cancellationToken = default)
=> await ((IMounter)Blobs).MountAsync(descriptor, fromRepository, getContent, cancellationToken).ConfigureAwait(false);
+ ///
+ /// ReferrersAsync retrieves referrers for the given descriptor and artifact type if specified
+ /// and return a streaming of descriptors asynchronously for consumption.
+ /// If referrers API is not supported, the function falls back to a tag schema for retrieving referrers.
+ /// If the referrers are supported via an API, the state is updated accordingly.
+ ///
+ ///
+ ///
+ ///
+ public async IAsyncEnumerable ReferrersAsync(Descriptor descriptor, string? artifactType,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ if (ReferrersState == Referrers.ReferrersState.NotSupported)
+ {
+ // fall back to tag schema to retrieve referrers
+ await foreach (var referrer in ReferrersByTagSchema(descriptor, artifactType, cancellationToken)
+ .ConfigureAwait(false))
+ {
+ yield return referrer;
+ }
+
+ yield break;
+ }
+
+ // referrers state is unknown or supported
+ await foreach (var referrer in ReferrersByApi(descriptor, artifactType, cancellationToken)
+ .ConfigureAwait(false))
+ {
+ // If Referrers API is supported, then it would return referrers continuously
+ // otherwise, this line of code is not executed
+ // and the ReferrerState would be set to false in the method ReferrersByApi.
+ yield return referrer;
+ }
+
+ if (ReferrersState == Referrers.ReferrersState.NotSupported)
+ {
+ // referrers state is set to NotSupported by ReferrersByApi, fall back to tag schema to retrieve referrers
+ await foreach (var referrer in ReferrersByTagSchema(descriptor, artifactType, cancellationToken)
+ .ConfigureAwait(false))
+ {
+ yield return referrer;
+ }
+ }
+ }
+
+ ///
+ /// ReferrersByApi retrieves a collection of referrers asynchronously based on the given descriptor and optional artifact type.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal async IAsyncEnumerable ReferrersByApi(Descriptor descriptor, string? artifactType,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var reference = new Reference(Options.Reference)
+ {
+ ContentReference = descriptor.Digest
+ };
+ var nextPageUrl = new UriFactory(reference).BuildReferrersUrl(artifactType);
+
+ while (nextPageUrl != null)
+ {
+ // If ReferrerListPageSize is greater than 0, modify the URL to include the page size query parameter
+ if (ReferrerListPageSize > 0)
+ {
+ var uriBuilder = new UriBuilder(nextPageUrl);
+ var query = HttpUtility.ParseQueryString(uriBuilder.Query);
+ query.Add("n", ReferrerListPageSize.ToString());
+ uriBuilder.Query = query.ToString();
+ nextPageUrl = uriBuilder.Uri;
+ }
+
+ using var response = await _opts.HttpClient.GetAsync(nextPageUrl, cancellationToken).ConfigureAwait(false);
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ // If the status code is OK, continue processing the response
+ break;
+ case HttpStatusCode.NotFound:
+ // If the status code is NotFound, handle as an error, possibly a non-existent repository
+ var exception = await response.ParseErrorResponseAsync(cancellationToken)
+ .ConfigureAwait(false);
+ if (exception.Errors?.First().Code == nameof(ResponseException.ErrorCode.NAME_UNKNOWN))
+ {
+ // Repository is not found, Referrers API status is unknown
+ // Propagate the exception to the caller
+ throw exception;
+ }
+
+ // Set ReferrerState to false and return earlier
+ SetReferrersState(false);
+ yield break;
+ default:
+ // For any other status code, parse and throw the error response
+ throw await response.ParseErrorResponseAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ var mediaType = response.Content.Headers.ContentType?.MediaType;
+ if (mediaType != MediaType.ImageIndex)
+ {
+ // Referrers API is not properly supported, set it to false and return early
+ SetReferrersState(false);
+ yield break;
+ }
+
+ using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var referrersIndex = JsonSerializer.Deserialize(content) ?? throw new InvalidResponseException(
+ $"{response.RequestMessage?.Method} {response.RequestMessage?.RequestUri}: failed to decode response");
+
+ // Set ReferrerState to Supported
+ SetReferrersState(true);
+
+ var referrers = referrersIndex.Manifests;
+ // If artifactType is specified, apply any filters based on the artifact type
+ if (!string.IsNullOrEmpty(artifactType))
+ {
+ if (!response.Headers.TryGetValues(_headerOciFiltersApplied, out var values)
+ || !Referrers.IsReferrersFilterApplied(values.FirstOrDefault(), _filterTypeArtifactType))
+ {
+ // Filter the referrers based on the artifact type if necessary
+ referrers = Referrers.FilterReferrers(referrers, artifactType);
+ }
+ }
+
+ foreach (var referrer in referrers)
+ {
+ // return referrer if any
+ yield return referrer;
+ }
+
+ // update nextPageUrl
+ nextPageUrl = response.ParseLink();
+ }
+ }
+
+ ///
+ /// ReferrersByTagSchema retrieves referrers based on referrers tag schema, filters out referrers based on specified artifact type
+ /// and return a collection of referrers asynchronously when referrers API is not supported.
+ /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
+ ///
+ ///
+ ///
+ ///
+ internal async IAsyncEnumerable ReferrersByTagSchema(Descriptor descriptor, string? artifactType,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var referrersTag = Referrers.BuildReferrersTag(descriptor);
+ var (_, referrers) = await PullReferrersIndexList(referrersTag, cancellationToken).ConfigureAwait(false);
+ var filteredReferrers = Referrers.FilterReferrers(referrers, artifactType);
+ foreach (var referrer in filteredReferrers)
+ {
+ yield return referrer;
+ }
+ }
+
+ ///
+ /// PullReferrersIndexList retrieves the referrers index list associated with the given referrers tag.
+ /// It fetches the index manifest from the repository, deserializes it into an `Index` object,
+ /// and returns the descriptor along with the list of manifests (referrers). If the referrers index is not found,
+ /// an empty descriptor and an empty list are returned.
+ ///
+ ///
+ ///
+ ///
+ internal async Task<(Descriptor?, IList)> PullReferrersIndexList(String referrersTag,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var result = await FetchAsync(referrersTag, cancellationToken).ConfigureAwait(false);
+ LimitSize(result.Descriptor, Options.MaxMetadataBytes);
+ using (var stream = result.Stream)
+ {
+ var indexBytes = await stream.ReadAllAsync(result.Descriptor, cancellationToken).ConfigureAwait(false);
+ var index = JsonSerializer.Deserialize(indexBytes) ?? throw new JsonException(
+ $"error when deserialize index manifest for referrersTag {referrersTag}");
+ return (result.Descriptor, index.Manifests);
+ }
+ }
+ catch (NotFoundException)
+ {
+ return (null, ImmutableArray.Empty);
+ }
+ }
+
///
/// PingReferrersAsync returns true if the Referrers API is available for the repository,
/// otherwise returns false
diff --git a/src/OrasProject.Oras/Registry/Remote/UriFactory.cs b/src/OrasProject.Oras/Registry/Remote/UriFactory.cs
index 88c678e..29ebc07 100644
--- a/src/OrasProject.Oras/Registry/Remote/UriFactory.cs
+++ b/src/OrasProject.Oras/Registry/Remote/UriFactory.cs
@@ -102,10 +102,11 @@ public Uri BuildRepositoryBlobUpload()
builder.Path += "/blobs/uploads/";
return builder.Uri;
}
-
+
///
- /// Builds the URL for accessing the Referrers API
- /// Format: :///v2//referrers/?artifactType=
+ /// BuildReferrersUrl builds the URL for accessing referrers API
+ /// Format: :///v2//referrers/?artifactType=
+ /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
///
///
///
@@ -119,7 +120,7 @@ public Uri BuildReferrersUrl(string? artifactType = null)
query.Add("artifactType", artifactType);
builder.Query = query.ToString();
}
-
+
return builder.Uri;
}
diff --git a/tests/OrasProject.Oras.Tests/Exceptions/ExceptionTest.cs b/tests/OrasProject.Oras.Tests/Exceptions/ExceptionTest.cs
index c00ce67..76d023b 100644
--- a/tests/OrasProject.Oras.Tests/Exceptions/ExceptionTest.cs
+++ b/tests/OrasProject.Oras.Tests/Exceptions/ExceptionTest.cs
@@ -57,6 +57,16 @@ public async Task ReferrersSupportLevelAlreadySetException()
await Assert.ThrowsAsync(() => throw new ReferrersStateAlreadySetException("Referrers state has already been set"));
await Assert.ThrowsAsync(() => throw new ReferrersStateAlreadySetException("Referrers state has already been set", null));
}
+
+ [Fact]
+ public async Task InvalidResponseException()
+ {
+ await Assert.ThrowsAsync(() => throw new InvalidResponseException());
+ await Assert.ThrowsAsync(() =>
+ throw new InvalidResponseException("Invalid response"));
+ await Assert.ThrowsAsync(() =>
+ throw new InvalidResponseException("Invalid response", null));
+ }
[Fact]
public async Task SizeLimitExceededException()
diff --git a/tests/OrasProject.Oras.Tests/Remote/ManifestStoreTest.cs b/tests/OrasProject.Oras.Tests/Remote/ManifestStoreTest.cs
index b151142..87db2c3 100644
--- a/tests/OrasProject.Oras.Tests/Remote/ManifestStoreTest.cs
+++ b/tests/OrasProject.Oras.Tests/Remote/ManifestStoreTest.cs
@@ -23,7 +23,6 @@
using static OrasProject.Oras.Content.Digest;
using Index = OrasProject.Oras.Oci.Index;
using Xunit;
-using Xunit.Abstractions;
namespace OrasProject.Oras.Tests.Remote;
@@ -72,8 +71,7 @@ public async Task ManifestStore_PullReferrersIndexListSuccessfully()
PlainHttp = true,
});
var cancellationToken = new CancellationToken();
- var store = new ManifestStore(repo);
- var (receivedDesc, receivedManifests) = await store.PullReferrersIndexList(expectedIndexDesc.Digest, cancellationToken);
+ var (receivedDesc, receivedManifests) = await repo.PullReferrersIndexList(expectedIndexDesc.Digest, cancellationToken);
Assert.True(AreDescriptorsEqual(expectedIndexDesc, receivedDesc));
for (var i = 0; i < receivedManifests.Count; ++i)
{
@@ -81,6 +79,51 @@ public async Task ManifestStore_PullReferrersIndexListSuccessfully()
}
}
+ [Fact]
+ public async Task ManifestStore_PullReferrersIndexList_ExceedSizeLimit()
+ {
+ var expectedIndex = RandomIndex();
+ var expectedIndexBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedIndex));
+ var expectedIndexDesc = new Descriptor()
+ {
+ Digest = ComputeSHA256(expectedIndexBytes),
+ MediaType = MediaType.ImageIndex,
+ Size = expectedIndexBytes.Length
+ };
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{expectedIndexDesc.Digest}")
+ {
+ if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageIndex))
+ {
+ return new HttpResponseMessage(HttpStatusCode.BadRequest);
+ }
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { expectedIndexDesc.Digest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ MaxMetadataBytes = expectedIndexDesc.Size - 1
+ });
+ var cancellationToken = new CancellationToken();
+ var exception = await Assert.ThrowsAsync(async () => await repo.PullReferrersIndexList(expectedIndexDesc.Digest, cancellationToken));
+ Assert.Equal($"content size {expectedIndexDesc.Size} exceeds MaxMetadataBytes {repo.Options.MaxMetadataBytes}", exception.Message);
+ }
+
[Fact]
public async Task ManifestStore_PullReferrersIndexListNotFound()
{
@@ -101,8 +144,7 @@ public async Task ManifestStore_PullReferrersIndexListNotFound()
PlainHttp = true,
});
var cancellationToken = new CancellationToken();
- var store = new ManifestStore(repo);
- var (receivedDesc, receivedManifests) = await store.PullReferrersIndexList("test", cancellationToken);
+ var (receivedDesc, receivedManifests) = await repo.PullReferrersIndexList("test", cancellationToken);
Assert.Null(receivedDesc);
Assert.Empty(receivedManifests);
}
diff --git a/tests/OrasProject.Oras.Tests/Remote/ReferrersTest.cs b/tests/OrasProject.Oras.Tests/Remote/ReferrersTest.cs
index ed01505..2201480 100644
--- a/tests/OrasProject.Oras.Tests/Remote/ReferrersTest.cs
+++ b/tests/OrasProject.Oras.Tests/Remote/ReferrersTest.cs
@@ -267,4 +267,121 @@ public void ApplyReferrerChanges_NoUpdateWhenOldAndNewReferrersAreEmpty()
Assert.Empty(updatedReferrers);
Assert.False(updateRequired);
}
+
+ [Fact]
+ public void IsReferrersFilterApplied_AppliedFiltersNull_ReturnsFalse()
+ {
+ string? appliedFilters = null;
+ const string requestedFilter = "artifactType";
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_AppliedFiltersEmpty_ReturnsFalse()
+ {
+ const string appliedFilters = "";
+ const string requestedFilter = "artifactType";
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_RequestedFilterNull_ReturnsFalse()
+ {
+ const string appliedFilters = "artifactType,annotation";
+ string? requestedFilter = null;
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_RequestedFilterEmpty_ReturnsFalse()
+ {
+ const string appliedFilters = "artifactType,annotation";
+ const string requestedFilter = "";
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_RequestedFilterMatches_ReturnsTrue()
+ {
+ const string appliedFilters = "artifactType,annotation";
+ const string requestedFilter = "artifactType";
+
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_SingleAppliedFiltersRequestedFilterMatches_ReturnsTrue()
+ {
+ const string appliedFilters = "filter1";
+ const string requestedFilter = "filter1";
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsReferrersFilterApplied_RequestedFilterDoesNotMatch_ReturnsFalse()
+ {
+ const string appliedFilters = "filter1,filter2";
+ const string requestedFilter = "filter3";
+ var result = Referrers.IsReferrersFilterApplied(appliedFilters, requestedFilter);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void FilterReferrers_WithNullOrEmptyArtifactType_ShouldReturnAllReferrers()
+ {
+ var referrers = new List
+ {
+ RandomDescriptor(),
+ RandomDescriptor(),
+ RandomDescriptor(),
+ };
+ string? artifactType = null;
+ var result = Referrers.FilterReferrers(referrers, artifactType);
+ Assert.Equal(3, result.Count);
+ Assert.Equal(referrers, result);
+
+ artifactType = "";
+ result = Referrers.FilterReferrers(referrers, artifactType);
+ Assert.Equal(3, result.Count);
+ Assert.Equal(referrers, result);
+ }
+
+ [Fact]
+ public void FilterReferrers_WithValidArtifactType_ShouldReturnMatchingReferrers()
+ {
+ var referrers = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ };
+ const string artifactType = "doc/example";
+ var result = Referrers.FilterReferrers(referrers, artifactType);
+
+ Assert.Equal(2, result.Count);
+ Assert.True(result.All(r => r.ArtifactType == artifactType));
+ }
+
+ [Fact]
+ public void FilterReferrers_WithArtifactTypeThatDoesNotExist_ShouldReturnEmptyList()
+ {
+ var referrers = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ };
+ const string artifactType = "NonExistentType";
+ var result = Referrers.FilterReferrers(referrers, artifactType);
+ Assert.Empty(result);
+ }
+
}
diff --git a/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs b/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs
index 8968496..17d7286 100644
--- a/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs
+++ b/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs
@@ -23,6 +23,7 @@
using System.Text.RegularExpressions;
using System.Web;
using Xunit;
+using Xunit.Abstractions;
using static OrasProject.Oras.Content.Digest;
using static OrasProject.Oras.Tests.Remote.Util.Util;
using static OrasProject.Oras.Tests.Remote.Util.RandomDataGenerator;
@@ -41,10 +42,18 @@ public struct TestIOStruct
public bool ErrExpectedOnGET;
}
+ private ITestOutputHelper _iTestOutputHelper;
+ public RepositoryTest(ITestOutputHelper iTestOutputHelper)
+ {
+ _iTestOutputHelper = iTestOutputHelper;
+ }
private byte[] _theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao"u8.ToArray();
private const string _theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078";
private const string _dockerContentDigestHeader = "Docker-Content-Digest";
+
+ private const string _headerOciFiltersApplied = "OCI-Filters-Applied";
+
// The following truth table aims to cover the expected GET/HEAD request outcome
// for all possible permutations of the client/server "containing a digest", for
@@ -2592,6 +2601,306 @@ public void SetReferrersState_ShouldNotThrowException_WhenSettingSameValue()
var exception = Record.Exception(() => repo.ReferrersState = Referrers.ReferrersState.Supported);
Assert.Null(exception);
}
+
+ [Fact]
+ public async Task Repository_ReferrersByTagSchema_Successfully()
+ {
+ var referrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(referrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ var expectedIndexDesc = new Descriptor()
+ {
+ Digest = ComputeSHA256(expectedIndexBytes),
+ MediaType = MediaType.ImageIndex,
+ };
+
+ var desc = RandomDescriptor();
+ var referrersTag = Referrers.BuildReferrersTag(desc);
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{referrersTag}")
+ {
+ if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageIndex))
+ {
+ return new HttpResponseMessage(HttpStatusCode.BadRequest);
+ }
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { expectedIndexDesc.Digest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // no artifact type filtering
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByTagSchema(desc, null, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+ Assert.Equivalent(expectedIndex.Manifests, returnedReferrers1);
+
+ // filter out referrers with artifact type doc/example
+ var artifactType2 = "doc/example";
+ var returnedReferrers2 = new List();
+ await foreach (var referrer in repo.ReferrersByTagSchema(desc, artifactType2, cancellationToken))
+ {
+ returnedReferrers2.Add(referrer);
+ }
+ Assert.True(returnedReferrers2.All(referrer => referrer.ArtifactType == artifactType2));
+ Assert.Equal(3, returnedReferrers2.Count);
+
+ // filter out non-existent referrers with artifact type non/non
+ var artifactType3 = "non/non";
+ var returnedReferrers3 = new List();
+ await foreach (var referrer in repo.ReferrersByTagSchema(desc, artifactType3, cancellationToken))
+ {
+ returnedReferrers3.Add(referrer);
+ }
+ Assert.Empty(returnedReferrers3);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByTagSchema_ReferrersNotFound()
+ {
+ var desc = RandomDescriptor();
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+ var cancellationToken = new CancellationToken();
+
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByTagSchema(desc, null, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+ Assert.Empty(returnedReferrers1);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_SinglePageWithoutFilter_Successfully()
+ {
+ var expectedReferrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(expectedReferrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+
+ var desc = RandomDescriptor();
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/referrers/{desc.Digest}")
+ {
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { desc.Digest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // no artifact type specified
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByApi(desc, null, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+ Assert.Equivalent(expectedReferrersList, returnedReferrers1);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_SinglePageWithServerSideFilter_Successfully()
+ {
+ var expectedReferrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(expectedReferrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ const string artifactType = "doc/example";
+ var desc = RandomDescriptor();
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ var expectedQuery = HttpUtility.ParseQueryString("");
+ expectedQuery["artifactType"] = artifactType;
+ var expectedQueryString = expectedQuery.ToString();
+
+ if (req.RequestUri?.PathAndQuery == $"/v2/test/referrers/{desc.Digest}?{expectedQueryString}")
+ {
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { desc.Digest });
+ res.Headers.Add(_headerOciFiltersApplied, new string[] {"artifactType"});
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // filter out referrers with artifact type "doc/example"
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByApi(desc, artifactType, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+
+ Assert.True(returnedReferrers1.All(referrer => referrer.ArtifactType == artifactType));
+ Assert.Equal(3, returnedReferrers1.Count);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_SinglePageWithClientSideFilter_Successfully()
+ {
+ var expectedReferrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(expectedReferrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ const string artifactType = "doc/example";
+ var desc = RandomDescriptor();
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ var expectedQuery = HttpUtility.ParseQueryString("");
+ expectedQuery["artifactType"] = artifactType;
+ var expectedQueryString = expectedQuery.ToString();
+
+ if (req.RequestUri?.PathAndQuery == $"/v2/test/referrers/{desc.Digest}?{expectedQueryString}")
+ {
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { desc.Digest });
+ res.Headers.Add(_headerOciFiltersApplied, new string[] {"abc/abc"});
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // filter out referrers with artifact type "doc/example"
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByApi(desc, artifactType, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+ Assert.True(returnedReferrers1.All(referrer => referrer.ArtifactType == artifactType));
+ Assert.Equal(3, returnedReferrers1.Count);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_ThrowsNotSupportedException_WhenReferrersApiNotSupported()
+ {
+ var desc = RandomDescriptor();
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersByApi(desc, null, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+
+ Assert.Empty(returnedReferrers1);
+ }
[Fact]
public async Task PingReferrers_ShouldReturnTrueWhenReferrersAPISupported()
@@ -2718,6 +3027,435 @@ public async Task PingReferrers_ShouldReturnFalseWhenReferrersAPINotSupportedNoC
Assert.Equal(Referrers.ReferrersState.NotSupported, repo.ReferrersState);
}
+ [Fact]
+ public async Task Repository_ReferrersByApi_ThrowsResponseException_WhenRepoNotFound()
+ {
+ var desc = RandomDescriptor();
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ res.Content = new StringContent(@"{ ""errors"": [ { ""code"": ""NAME_UNKNOWN"", ""message"": ""some error"" } ] }");
+ res.StatusCode = HttpStatusCode.NotFound;
+ return res;
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var isInvoked = false;
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var _ in repo.ReferrersByApi(desc, null, cancellationToken))
+ {
+ isInvoked = true;
+ }
+ });
+
+ Assert.False(isInvoked);
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ Assert.NotNull(exception.Errors);
+ Assert.Single(exception.Errors);
+ Assert.Equal("NAME_UNKNOWN", exception.Errors[0].Code);
+ Assert.Equal("some error", exception.Errors[0].Message);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_ThrowsResponseException_WhenServerErrors()
+ {
+ var desc = RandomDescriptor();
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ res.Content = new StringContent(@"{ ""errors"": [ { ""code"": ""INTERNAL_SERVER_ERROR"", ""message"": ""some error"" } ] }");
+ res.StatusCode = HttpStatusCode.InternalServerError;
+ return res;
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var isInvoked = false;
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var _ in repo.ReferrersByApi(desc, null, cancellationToken))
+ {
+ isInvoked = true;
+ }
+ });
+ Assert.False(isInvoked);
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ Assert.NotNull(exception.Errors);
+ Assert.Single(exception.Errors);
+ Assert.Equal("INTERNAL_SERVER_ERROR", exception.Errors[0].Code);
+ Assert.Equal("some error", exception.Errors[0].Message);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersByApi_WhenContentIsNotImageIndex()
+ {
+ var expectedReferrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(expectedReferrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ const string artifactType = "doc/example";
+ var desc = RandomDescriptor();
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ var expectedQuery = HttpUtility.ParseQueryString("");
+ expectedQuery["artifactType"] = artifactType;
+ var expectedQueryString = expectedQuery.ToString();
+
+ if (req.RequestUri?.PathAndQuery == $"/v2/test/referrers/{desc.Digest}?{expectedQueryString}")
+ {
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers = new List();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ await foreach (var referrer in repo.ReferrersByApi(desc, artifactType, cancellationToken))
+ {
+ returnedReferrers.Add(referrer);
+ }
+ Assert.Equal(Referrers.ReferrersState.NotSupported, repo.ReferrersState);
+ Assert.Empty(returnedReferrers);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersAsync_SinglePage_ReferrersApiSupported()
+ {
+ var expectedReferrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(expectedReferrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ const string artifactType = "doc/example";
+ var desc = RandomDescriptor();
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ var expectedQuery = HttpUtility.ParseQueryString("");
+ expectedQuery["artifactType"] = artifactType;
+ var expectedQueryString = expectedQuery.ToString();
+
+ if (req.RequestUri?.PathAndQuery == $"/v2/test/referrers/{desc.Digest}?{expectedQueryString}")
+ {
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { desc.Digest });
+ res.Headers.Add(_headerOciFiltersApplied, new string[] {"abc/abc"});
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // filter out referrers with artifact type "doc/example"
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var returnedReferrers = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, artifactType, cancellationToken))
+ {
+ returnedReferrers.Add(referrer);
+ }
+ Assert.True(returnedReferrers.All(referrer => referrer.ArtifactType == artifactType));
+ Assert.Equal(3, returnedReferrers.Count);
+ Assert.Equal(Referrers.ReferrersState.Supported, repo.ReferrersState);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersAsync_MultiplePages_ReferrersApiSupported()
+ {
+ var expectedReferrersList = new List>
+ {
+ new (){
+ RandomDescriptor(artifactType: "first/example"),
+ RandomDescriptor(artifactType: "first/example"),
+ RandomDescriptor(artifactType: "first/example"),
+ RandomDescriptor(artifactType: "first/example")
+ },
+ new (){
+ RandomDescriptor(artifactType: "second/example"),
+ RandomDescriptor(artifactType: "second/example"),
+ RandomDescriptor(artifactType: "second/example"),
+ RandomDescriptor(artifactType: "second/example")
+ },
+ new (){
+ RandomDescriptor(artifactType: "third/example"),
+ RandomDescriptor(artifactType: "third/example"),
+ RandomDescriptor(artifactType: "third/example"),
+ RandomDescriptor(artifactType: "third/example")
+ },
+
+ };
+
+ var desc = RandomDescriptor();
+
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ var path = $"/v2/test/referrers/{desc.Digest}";
+ IList? referrers = null;
+ if (req.RequestUri?.AbsolutePath == path)
+ {
+ var query = HttpUtility.ParseQueryString(req.RequestUri.Query);
+ switch (query.Get("test"))
+ {
+ case "foo":
+ res.Headers.Add("Link", new string?[]{ $"<{req.RequestUri.Scheme}://{req.RequestUri.Host}{path}?test=bar>; rel=\"next\"" });
+ referrers = expectedReferrersList.ElementAtOrDefault(1);
+ break;
+ case "bar":
+ referrers = expectedReferrersList.ElementAtOrDefault(2);
+ break;
+ default:
+ referrers = expectedReferrersList.ElementAtOrDefault(0);
+ res.Headers.Add("Link", new string?[]{ $"<{path}?test=foo>; rel=\"next\"" });
+ break;
+ }
+
+ var expectedIndex = RandomIndex(referrers);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { desc.Digest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var returnedReferrers = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, null, cancellationToken))
+ {
+ returnedReferrers.Add(referrer);
+ }
+ Assert.Equivalent(expectedReferrersList.SelectMany(list => list).ToList(), returnedReferrers);
+ Assert.Equal(Referrers.ReferrersState.Supported, repo.ReferrersState);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersAsync_WhenReferrersApiNotSupported()
+ {
+ var referrersList = new List
+ {
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"abc/abc"),
+ RandomDescriptor(artifactType:"doc/example"),
+ RandomDescriptor(artifactType:"doc/example"),
+ };
+ var expectedIndex = RandomIndex(referrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ var expectedIndexDesc = new Descriptor()
+ {
+ Digest = ComputeSHA256(expectedIndexBytes),
+ MediaType = MediaType.ImageIndex,
+ };
+
+ var desc = RandomDescriptor();
+ var referrersTag = Referrers.BuildReferrersTag(desc);
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{referrersTag}")
+ {
+ if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageIndex))
+ {
+ return new HttpResponseMessage(HttpStatusCode.BadRequest);
+ }
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { expectedIndexDesc.Digest });
+ return res;
+ }
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ // no artifact type filtering
+ var fn1 = (IList referrers) =>
+ {
+ Assert.Equivalent(expectedIndex.Manifests, referrers);
+ };
+ var cancellationToken = new CancellationToken();
+ var returnedReferrers1 = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, null, cancellationToken))
+ {
+ returnedReferrers1.Add(referrer);
+ }
+ Assert.Equivalent(expectedIndex.Manifests, returnedReferrers1);
+
+ // filter out referrers with artifact type doc/example
+ var artifactType2 = "doc/example";
+ var returnedReferrers2 = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, artifactType2, cancellationToken))
+ {
+ returnedReferrers2.Add(referrer);
+ }
+ Assert.Equal(3, returnedReferrers2.Count);
+ Assert.True(returnedReferrers2.All(referrer => referrer.ArtifactType == artifactType2));
+
+ // filter out non-existent referrers with artifact type non/non
+ var artifactType3 = "non/non";
+ var returnedReferrers3 = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, artifactType3, cancellationToken))
+ {
+ returnedReferrers3.Add(referrer);
+ }
+ Assert.Empty(returnedReferrers3);
+ }
+
+ [Fact]
+ public async Task Repository_ReferrersAsync_ReferrersStateUnknown_ReferrersApiNotSupported()
+ {
+ var referrersList = new List
+ {
+ RandomDescriptor(artifactType: "doc/example"),
+ RandomDescriptor(artifactType: "doc/abc"),
+ RandomDescriptor(artifactType: "abc/abc"),
+ RandomDescriptor(artifactType: "abc/abc"),
+ RandomDescriptor(artifactType: "doc/example"),
+ RandomDescriptor(artifactType: "doc/example"),
+ };
+ var expectedIndex = RandomIndex(referrersList);
+ var expectedIndexBytes = JsonSerializer.SerializeToUtf8Bytes(expectedIndex);
+ var desc = RandomDescriptor();
+ var referrersTag = Referrers.BuildReferrersTag(desc);
+ var mockedHttpHandler = (HttpRequestMessage req, CancellationToken cancellationToken) =>
+ {
+ var res = new HttpResponseMessage();
+ res.RequestMessage = req;
+ if (req.Method != HttpMethod.Get)
+ {
+ return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
+ }
+
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/referrers/{desc.Digest}")
+ {
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+
+ if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{referrersTag}")
+ {
+ if (req.Headers.TryGetValues("Accept", out IEnumerable? values) &&
+ !values.Contains(MediaType.ImageIndex))
+ {
+ return new HttpResponseMessage(HttpStatusCode.BadRequest);
+ }
+
+ res.Content = new ByteArrayContent(expectedIndexBytes);
+ res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageIndex });
+ res.Headers.Add(_dockerContentDigestHeader, new string[] { ComputeSHA256(expectedIndexBytes) });
+ return res;
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(mockedHttpHandler),
+ PlainHttp = true,
+ });
+
+ var cancellationToken = new CancellationToken();
+ Assert.Equal(Referrers.ReferrersState.Unknown, repo.ReferrersState);
+ var returnedReferrers = new List();
+ await foreach (var referrer in repo.ReferrersAsync(desc, null, cancellationToken))
+ {
+ returnedReferrers.Add(referrer);
+ }
+
+ Assert.Equivalent(expectedIndex.Manifests, returnedReferrers);
+ Assert.Equal(Referrers.ReferrersState.NotSupported, repo.ReferrersState);
+ }
+
[Fact]
public async Task PingReferrers_ShouldFailWhenReturnNotFound()
{
diff --git a/tests/OrasProject.Oras.Tests/Remote/UriFactoryTest.cs b/tests/OrasProject.Oras.Tests/Remote/UriFactoryTest.cs
index 1fc7dd3..c85d5a5 100644
--- a/tests/OrasProject.Oras.Tests/Remote/UriFactoryTest.cs
+++ b/tests/OrasProject.Oras.Tests/Remote/UriFactoryTest.cs
@@ -27,7 +27,7 @@ public void BuildReferrersUrl_WithArtifactType_ShouldAddArtifactTypeToQueryStrin
var reference = Reference.Parse("localhost:5000/test");
reference.ContentReference = desc.Digest;
-
+
const string artifactType = "doc/example";
var expectedPath = $"referrers/{reference.ContentReference}";
const string expectedQuery = "artifactType=doc%2fexample";
@@ -41,8 +41,6 @@ public void BuildReferrersUrl_WithoutArtifactType()
var desc = RandomDescriptor();
var reference = Reference.Parse("localhost:5000/test");
reference.ContentReference = desc.Digest;
-
-
var expectedPath = $"referrers/{reference.ContentReference}";
var result = new UriFactory(reference).BuildReferrersUrl();
Assert.Equal($"https://localhost:5000/v2/test/{expectedPath}", result.ToString());
diff --git a/tests/OrasProject.Oras.Tests/Remote/Util/RandomDataGenerator.cs b/tests/OrasProject.Oras.Tests/Remote/Util/RandomDataGenerator.cs
index f34df44..7415074 100644
--- a/tests/OrasProject.Oras.Tests/Remote/Util/RandomDataGenerator.cs
+++ b/tests/OrasProject.Oras.Tests/Remote/Util/RandomDataGenerator.cs
@@ -38,11 +38,11 @@ public static string RandomString()
return new string(stringChars);
}
- public static Descriptor RandomDescriptor(string mediaType = MediaType.ImageManifest)
+ public static Descriptor RandomDescriptor(string mediaType = MediaType.ImageManifest, string artifactType = "")
{
var randomBytes = RandomBytes();
return new Descriptor
- { MediaType = mediaType, Digest = Digest.ComputeSHA256(randomBytes), Size = randomBytes.Length };
+ { MediaType = mediaType, Digest = Digest.ComputeSHA256(randomBytes), Size = randomBytes.Length, ArtifactType = artifactType };
}
public static (Manifest, byte[]) RandomManifest()
@@ -72,15 +72,20 @@ public static byte[] RandomBytes()
return Encoding.UTF8.GetBytes(RandomString());
}
- public static Index RandomIndex()
+ public static Index RandomIndex(IList? manifests = null)
{
- return new Index()
+ if (manifests == null)
{
- Manifests = new List
+ manifests = new List
{
RandomDescriptor(),
RandomDescriptor(),
- },
+ RandomDescriptor(),
+ };
+ }
+ return new Index()
+ {
+ Manifests = manifests,
MediaType = MediaType.ImageIndex,
};
}