Skip to content

Commit f6615e7

Browse files
authored
Create OwnerDetailsUriTemplateResourceV3 and provider (#5763)
1 parent 3ebcf80 commit f6615e7

10 files changed

Lines changed: 308 additions & 1 deletion

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using NuGet.Protocol.Core.Types;
10+
using NuGet.Protocol.Resources;
11+
12+
namespace NuGet.Protocol.Providers
13+
{
14+
/// <summary>NuGet.Protocol resource provider for <see cref="OwnerDetailsUriTemplateResourceV3"/> in V3 HTTP feeds.</summary>
15+
/// <remarks>When successful, returns an instance of <see cref="OwnerDetailsUriTemplateResourceV3"/>.</remarks>
16+
public class OwnerDetailsUriResourceV3Provider : ResourceProvider
17+
{
18+
public OwnerDetailsUriResourceV3Provider()
19+
: base(typeof(OwnerDetailsUriTemplateResourceV3),
20+
nameof(OwnerDetailsUriTemplateResourceV3),
21+
NuGetResourceProviderPositions.Last)
22+
{
23+
}
24+
25+
/// <inheritdoc cref="ResourceProvider.TryCreate(SourceRepository, CancellationToken)"/>
26+
public override async Task<Tuple<bool, INuGetResource?>> TryCreate(SourceRepository source, CancellationToken token)
27+
{
28+
OwnerDetailsUriTemplateResourceV3? resource = null;
29+
ServiceIndexResourceV3? serviceIndex = await source.GetResourceAsync<ServiceIndexResourceV3>(token);
30+
if (serviceIndex != null)
31+
{
32+
Uri? uriTemplate = serviceIndex.GetServiceEntryUri(ServiceTypes.OwnerDetailsUriTemplate);
33+
34+
if (uriTemplate != null)
35+
{
36+
resource = OwnerDetailsUriTemplateResourceV3.CreateOrNull(uriTemplate);
37+
}
38+
}
39+
40+
return new Tuple<bool, INuGetResource?>(resource != null, resource);
41+
}
42+
}
43+
}

src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
#nullable enable
2+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider
3+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.OwnerDetailsUriResourceV3Provider() -> void
4+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3
5+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.GetUri(string! owner) -> System.Uri!
6+
override NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.TryCreate(NuGet.Protocol.Core.Types.SourceRepository! source, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task<System.Tuple<bool, NuGet.Protocol.Core.Types.INuGetResource?>!>!
7+
static NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.CreateOrNull(System.Uri! uriTemplate) -> NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3?
28
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
39
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
410
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.set -> void

src/NuGet.Core/NuGet.Protocol/PublicAPI/netcoreapp5.0/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
#nullable enable
2+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider
3+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.OwnerDetailsUriResourceV3Provider() -> void
4+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3
5+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.GetUri(string! owner) -> System.Uri!
6+
override NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.TryCreate(NuGet.Protocol.Core.Types.SourceRepository! source, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task<System.Tuple<bool, NuGet.Protocol.Core.Types.INuGetResource?>!>!
7+
static NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.CreateOrNull(System.Uri! uriTemplate) -> NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3?
28
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
39
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
410
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.set -> void

src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
#nullable enable
2+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider
3+
NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.OwnerDetailsUriResourceV3Provider() -> void
4+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3
5+
NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.GetUri(string! owner) -> System.Uri!
6+
override NuGet.Protocol.Providers.OwnerDetailsUriResourceV3Provider.TryCreate(NuGet.Protocol.Core.Types.SourceRepository! source, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task<System.Tuple<bool, NuGet.Protocol.Core.Types.INuGetResource?>!>!
7+
static NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3.CreateOrNull(System.Uri! uriTemplate) -> NuGet.Protocol.Resources.OwnerDetailsUriTemplateResourceV3?
28
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
39
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.get -> System.Collections.Generic.IReadOnlyList<string>
410
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.OwnersList.set -> void

src/NuGet.Core/NuGet.Protocol/Repository.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public virtual IEnumerable<Lazy<INuGetResourceProvider>> GetCoreV3()
8181
yield return new Lazy<INuGetResourceProvider>(() => new PluginResourceProvider());
8282
yield return new Lazy<INuGetResourceProvider>(() => new RepositorySignatureResourceProvider());
8383
yield return new Lazy<INuGetResourceProvider>(() => new VulnerabilityInfoResourceV3Provider());
84+
yield return new Lazy<INuGetResourceProvider>(() => new OwnerDetailsUriResourceV3Provider());
8485

8586
// Local repository providers
8687
yield return new Lazy<INuGetResourceProvider>(() => new FindLocalPackagesResourceUnzippedProvider());
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using NuGet.Protocol.Core.Types;
8+
9+
namespace NuGet.Protocol.Resources
10+
{
11+
/// <summary>Owner Details Uri Template for NuGet V3 HTTP feeds.</summary>
12+
/// <remarks>Not intended to be created directly. Use <see cref="SourceRepository.GetResourceAsync{T}(CancellationToken)"/>
13+
/// with <see cref="OwnerDetailsUriTemplateResourceV3"/> for T, and typecast to this class.
14+
public class OwnerDetailsUriTemplateResourceV3 : INuGetResource
15+
{
16+
private readonly string _template;
17+
18+
private OwnerDetailsUriTemplateResourceV3(string template)
19+
{
20+
_template = template ?? throw new ArgumentNullException(nameof(template));
21+
}
22+
23+
/// <summary>
24+
/// Creates the specified Owner Details Uri template provided by the server if it exists and is valid.
25+
/// </summary>
26+
/// <param name="uriTemplate">The Absolute Uri template provided by the server.</param>
27+
/// <returns>A valid Owner Details Uri template, or null.</returns>
28+
public static OwnerDetailsUriTemplateResourceV3? CreateOrNull(Uri uriTemplate)
29+
{
30+
if (uriTemplate is null)
31+
{
32+
throw new ArgumentNullException(nameof(uriTemplate));
33+
}
34+
35+
if (uriTemplate.OriginalString.Length == 0
36+
|| !uriTemplate.IsAbsoluteUri
37+
|| !uriTemplate.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
38+
{
39+
return null;
40+
}
41+
42+
return new OwnerDetailsUriTemplateResourceV3(uriTemplate.OriginalString);
43+
}
44+
45+
/// <summary>
46+
/// Gets a URL for viewing package Owner URL outside of Visual Studio. The URL will not be verified to exist.
47+
/// </summary>
48+
/// <param name="owner">The owner username.</param>
49+
/// <returns>The first URL from the resource, with the URI template applied.</returns>
50+
public Uri GetUri(string owner)
51+
{
52+
var uriString = _template
53+
#if NETCOREAPP
54+
.Replace("{owner}", owner, StringComparison.OrdinalIgnoreCase);
55+
#else
56+
.Replace("{owner}", owner);
57+
#endif
58+
59+
return new Uri(uriString);
60+
}
61+
}
62+
}

src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public static class ServiceTypes
1717
public static readonly string Version500 = "/5.0.0";
1818
public static readonly string Version510 = "/5.1.0";
1919
internal const string Version670 = "/6.7.0";
20+
internal const string Version6110 = "/6.11.0";
2021

2122
public static readonly string[] SearchQueryService = { "SearchQueryService" + Versioned, "SearchQueryService" + Version340, "SearchQueryService" + Version300beta };
2223
public static readonly string[] RegistrationsBaseUrl = { $"RegistrationsBaseUrl{Versioned}", $"RegistrationsBaseUrl{Version360}", $"RegistrationsBaseUrl{Version340}", $"RegistrationsBaseUrl{Version300rc}", $"RegistrationsBaseUrl{Version300beta}", "RegistrationsBaseUrl" };
@@ -29,5 +30,6 @@ public static class ServiceTypes
2930
public static readonly string[] RepositorySignatures = { "RepositorySignatures" + Version500, "RepositorySignatures" + Version490, "RepositorySignatures" + Version470 };
3031
public static readonly string[] SymbolPackagePublish = { "SymbolPackagePublish" + Version490 };
3132
internal static readonly string[] VulnerabilityInfo = { "VulnerabilityInfo" + Version670 };
33+
internal static readonly string[] OwnerDetailsUriTemplate = { "OwnerDetailsUriTemplate" + Version6110 };
3234
}
3335
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using FluentAssertions;
10+
using NuGet.Configuration;
11+
using NuGet.Packaging;
12+
using NuGet.Protocol.Core.Types;
13+
using NuGet.Protocol.Providers;
14+
using Xunit;
15+
16+
namespace NuGet.Protocol.Tests.Providers
17+
{
18+
public class OwnerDetailsUriResourceV3ProviderTests
19+
{
20+
[Fact]
21+
public async Task TryCreate_NoResourceInServiceIndex_ReturnsFalseAsync()
22+
{
23+
// Arrange
24+
var serviceIndexProvider = MockServiceIndexResourceV3Provider.Create();
25+
var target = new OwnerDetailsUriResourceV3Provider();
26+
var providers = new INuGetResourceProvider[] { serviceIndexProvider, target };
27+
28+
PackageSource packageSource = new PackageSource("https://nuget.test/v3/index.json");
29+
SourceRepository sourceRepository = new SourceRepository(packageSource, providers);
30+
31+
// Act
32+
Tuple<bool, INuGetResource?> result = await target.TryCreate(sourceRepository, CancellationToken.None);
33+
34+
// Assert
35+
bool providerHandlesInputSource = result.Item1;
36+
INuGetResource? resource = result.Item2;
37+
38+
providerHandlesInputSource.Should().BeFalse();
39+
resource.Should().BeNull();
40+
}
41+
42+
[Fact]
43+
public async Task TryCreate_ResourceInServiceIndex_ReturnsTrueAsync()
44+
{
45+
// Arrange
46+
var ownerDetailsResourceEntry = new ServiceIndexEntry(new Uri("https://nuget.test/profiles/{owner}?_src=template"), ServiceTypes.OwnerDetailsUriTemplate[0], MinClientVersionUtility.GetNuGetClientVersion());
47+
var serviceIndexProvider = MockServiceIndexResourceV3Provider.Create();
48+
var target = new OwnerDetailsUriResourceV3Provider();
49+
var providers = new INuGetResourceProvider[] { serviceIndexProvider, target };
50+
51+
PackageSource packageSource = new PackageSource("https://nuget.test/v3/index.json");
52+
SourceRepository sourceRepository = new SourceRepository(packageSource, providers);
53+
54+
// Act
55+
Tuple<bool, INuGetResource?> result = await target.TryCreate(sourceRepository, CancellationToken.None);
56+
57+
// Assert
58+
bool providerHandlesInputSource = result.Item1;
59+
INuGetResource? resource = result.Item2;
60+
61+
providerHandlesInputSource.Should().BeFalse();
62+
resource.Should().BeNull();
63+
}
64+
}
65+
}

test/NuGet.Core.Tests/NuGet.Protocol.Tests/RepositoryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void Provider_WithDefaultProvider_ReturnsDefaultResourceProviders()
6363

6464
int actualCount = resourceProviders.Count();
6565

66-
Assert.Equal(47, actualCount);
66+
Assert.Equal(48, actualCount);
6767
}
6868
}
6969
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using FluentAssertions;
8+
using NuGet.Protocol.Resources;
9+
using Xunit;
10+
11+
namespace NuGet.Protocol.Tests.Resources
12+
{
13+
public class OwnerDetailsUriTemplateResourceV3Tests
14+
{
15+
private readonly Uri _template = new Uri("https://nuget.test/profiles/{owner}?_src=template");
16+
17+
[Fact]
18+
public void CreateOrNull_WhenNullTemplate_Throws()
19+
{
20+
// Arrange
21+
Uri? template = null;
22+
23+
// Act & Assert
24+
Assert.Throws<ArgumentNullException>(() => OwnerDetailsUriTemplateResourceV3.CreateOrNull(template!));
25+
}
26+
27+
[Fact]
28+
public void CreateOrNull_WhenTemplateNotAbsoluteUri_CreatesNullResource()
29+
{
30+
// Arrange
31+
var template = new Uri("/owner/profile", UriKind.Relative);
32+
33+
// Act
34+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(template);
35+
36+
// Assert
37+
target.Should().BeNull();
38+
}
39+
40+
[Fact]
41+
public void CreateOrNull_WhenTemplateNotHttps_CreatesNullResource()
42+
{
43+
// Arrange
44+
var template = new Uri("http://nuget.test/profiles/{owner}?_src=template");
45+
46+
// Act
47+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(template);
48+
49+
// Assert
50+
target.Should().BeNull();
51+
}
52+
53+
[Fact]
54+
public void CreateOrNull_WhenValidTemplateHttps_CreatesResource()
55+
{
56+
// Arrange & Act
57+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(_template);
58+
59+
// Assert
60+
target.Should().NotBeNull();
61+
}
62+
63+
[Fact]
64+
public void GetUri_WithSpacesInOwnerParameter_CreatesValidOwnerUriWithEncoding()
65+
{
66+
// Arrange
67+
string owner = "Microsoft Microsoft Microsoft";
68+
string formattedOwner = "Microsoft%20Microsoft%20Microsoft";
69+
70+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(_template);
71+
72+
// Act
73+
Uri ownerUri = target!.GetUri(owner);
74+
75+
// Assert
76+
ownerUri.Should().NotBeNull();
77+
ownerUri.IsAbsoluteUri.Should().BeTrue();
78+
ownerUri.AbsoluteUri.Should().Be($"https://nuget.test/profiles/{formattedOwner}?_src=template");
79+
}
80+
81+
[Theory]
82+
[InlineData("microsoft")]
83+
[InlineData("MiCroSoFT")]
84+
public void GetUri_WithValidOwnerParameter_CreatesValidOwnerUriWithSameCasing(string owner)
85+
{
86+
// Arrange
87+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(_template);
88+
89+
// Act
90+
Uri ownerUri = target!.GetUri(owner);
91+
92+
// Assert
93+
ownerUri.Should().NotBeNull();
94+
ownerUri.IsAbsoluteUri.Should().BeTrue();
95+
ownerUri.AbsoluteUri.Should().Be($"https://nuget.test/profiles/{owner}?_src=template");
96+
}
97+
98+
[Theory]
99+
[InlineData(null)]
100+
[InlineData("")]
101+
public void GetUri_WithInvalidOwnerParameter_ReturnsOriginalTemplate(string owner)
102+
{
103+
// Arrange
104+
var target = OwnerDetailsUriTemplateResourceV3.CreateOrNull(_template);
105+
string templateWithoutOwner = "https://nuget.test/profiles/?_src=template";
106+
107+
// Act
108+
Uri ownerUri = target!.GetUri(owner);
109+
110+
// Assert
111+
ownerUri.Should().NotBeNull();
112+
ownerUri.IsAbsoluteUri.Should().BeTrue();
113+
ownerUri.AbsoluteUri.Should().Be(templateWithoutOwner);
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)