diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs index a51f64f9e29..be490248dd6 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs @@ -110,7 +110,7 @@ private void DetailControlModel_PropertyChanged(object sender, PropertyChangedEv { NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () => { - if (_readmeTabEnabled) + if (_readmeTabEnabled && e.PropertyName == nameof(DetailControlModel.PackageMetadata)) { await ReadmePreviewViewModel.SetPackageMetadataAsync(DetailControlModel.PackageMetadata, CancellationToken.None); } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs index ff897fe3b1b..c28e59d21a2 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs @@ -13,28 +13,46 @@ namespace NuGet.PackageManagement.UI.ViewModels { public sealed class ReadmePreviewViewModel : TitledPageViewModelBase { - private bool _errorLoadingReadme; + private bool _errorWithReadme; private INuGetPackageFileService _nugetPackageFileService; private string _rawReadme; private DetailedPackageMetadata _packageMetadata; private bool _canRenderLocalReadme; + private bool _isBusy; public ReadmePreviewViewModel(INuGetPackageFileService packageFileService, ItemFilter itemFilter, bool isReadmeFeatureEnabled) { _nugetPackageFileService = packageFileService ?? throw new ArgumentNullException(nameof(packageFileService)); _canRenderLocalReadme = CanRenderLocalReadme(itemFilter); _nugetPackageFileService = packageFileService; - _errorLoadingReadme = false; + _errorWithReadme = false; + _isBusy = false; _rawReadme = string.Empty; _packageMetadata = null; Title = Resources.Label_Readme_Tab; IsVisible = isReadmeFeatureEnabled; } - public bool ErrorLoadingReadme + public bool IsReadmeReady { get => !IsBusy && !ErrorWithReadme; } + + public bool ErrorWithReadme { - get => _errorLoadingReadme; - set => SetAndRaisePropertyChanged(ref _errorLoadingReadme, value); + get => _errorWithReadme; + set + { + SetAndRaisePropertyChanged(ref _errorWithReadme, value); + RaisePropertyChanged(nameof(IsReadmeReady)); + } + } + + public bool IsBusy + { + get => _isBusy; + set + { + SetAndRaisePropertyChanged(ref _isBusy, value); + RaisePropertyChanged(nameof(IsReadmeReady)); + } } public string ReadmeMarkdown @@ -76,7 +94,7 @@ private async Task LoadReadmeAsync(CancellationToken cancellationToken) { ReadmeMarkdown = _canRenderLocalReadme && !string.IsNullOrWhiteSpace(_packageMetadata.PackagePath) ? Resources.Text_NoReadme : string.Empty; IsVisible = !string.IsNullOrWhiteSpace(ReadmeMarkdown); - ErrorLoadingReadme = false; + ErrorWithReadme = false; return; } @@ -85,29 +103,34 @@ private async Task LoadReadmeAsync(CancellationToken cancellationToken) { ReadmeMarkdown = string.Empty; IsVisible = false; - ErrorLoadingReadme = false; + ErrorWithReadme = false; return; } var readme = Resources.Text_NoReadme; - await ThreadHelper.JoinableTaskFactory.RunAsync(async () => + try { - await TaskScheduler.Default; - using var readmeStream = await _nugetPackageFileService.GetReadmeAsync(readmeUrl, cancellationToken); - if (readmeStream is null) + IsBusy = true; + ErrorWithReadme = false; + await ThreadHelper.JoinableTaskFactory.RunAsync(async () => { - return; - } + await TaskScheduler.Default; + using var readmeStream = await _nugetPackageFileService.GetReadmeAsync(readmeUrl, cancellationToken); + if (readmeStream is null) + { + return; + } - using StreamReader streamReader = new StreamReader(readmeStream); - readme = await streamReader.ReadToEndAsync(); - }); - - if (!cancellationToken.IsCancellationRequested) + using StreamReader streamReader = new StreamReader(readmeStream); + readme = await streamReader.ReadToEndAsync(); + }); + } + finally { ReadmeMarkdown = readme; IsVisible = !string.IsNullOrWhiteSpace(readme); - ErrorLoadingReadme = false; + ErrorWithReadme = false; + IsBusy = false; } } } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml index f582abcf9e1..7ac5326c491 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml @@ -9,6 +9,8 @@ xmlns:imagingTheme="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Imaging" xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog" xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Utilities" + Background="{DynamicResource {x:Static nuget:Brushes.DetailPaneBackground}}" + Foreground="{DynamicResource {x:Static nuget:Brushes.UIText}}" DataContextChanged="UserControl_DataContextChanged" Unloaded="PackageReadmeControl_Unloaded" Loaded="PackageReadmeControl_Loaded" @@ -28,12 +30,14 @@ x:Uid="descriptionMarkdownPreview" ClipToBounds="True" Focusable="False" - Visibility="{Binding Path=ErrorLoadingReadme, Mode=OneWay, Converter={StaticResource NegatedBooleanToVisibilityConverter}}" - /> + Visibility="{Binding Path=IsReadmeReady, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" /> + + Visibility="{Binding Path=ErrorWithReadme, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" /> diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml.cs index 05a3a80f580..caf94947c14 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml.cs @@ -7,7 +7,6 @@ using System.Windows; using System.Windows.Controls; using Microsoft.VisualStudio.Markdown.Platform; -using Microsoft.VisualStudio.Shell; using NuGet.PackageManagement.UI.ViewModels; using NuGet.VisualStudio; using NuGet.VisualStudio.Telemetry; @@ -39,7 +38,7 @@ private void ReadmeViewModel_PropertyChanged(object sender, PropertyChangedEvent { if (e.PropertyName == nameof(ReadmePreviewViewModel.ReadmeMarkdown)) { - NuGetUIThreadHelper.JoinableTaskFactory.Run(UpdateMarkdownAsync); + NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync).PostOnFailure(nameof(PackageReadmeControl), nameof(ReadmeViewModel_PropertyChanged)); } } @@ -79,7 +78,7 @@ private async Task UpdateMarkdownAsync() } catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) { - ReadmeViewModel.ErrorLoadingReadme = true; + ReadmeViewModel.ErrorWithReadme = true; ReadmeViewModel.ReadmeMarkdown = string.Empty; await TelemetryUtility.PostFaultAsync(ex, nameof(ReadmePreviewViewModel)); } @@ -104,10 +103,8 @@ private void PackageReadmeControl_Unloaded(object sender, RoutedEventArgs e) private void PackageReadmeControl_Loaded(object sender, RoutedEventArgs e) { - ThreadHelper.JoinableTaskFactory.Run(async () => - { - await UpdateMarkdownAsync(); - }); + NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync) + .PostOnFailure(nameof(PackageReadmeControl), nameof(PackageReadmeControl_Loaded)); } } } diff --git a/src/NuGet.Core/NuGet.Protocol/Model/PackageSearchMetadata.cs b/src/NuGet.Core/NuGet.Protocol/Model/PackageSearchMetadata.cs index e8cf3e97452..27230ab7542 100644 --- a/src/NuGet.Core/NuGet.Protocol/Model/PackageSearchMetadata.cs +++ b/src/NuGet.Core/NuGet.Protocol/Model/PackageSearchMetadata.cs @@ -106,8 +106,8 @@ public string Owners [JsonConverter(typeof(SafeUriConverter))] public Uri ReadmeUrl { get; private set; } - [JsonProperty(PropertyName = JsonProperties.ReadmeFileUrl)] - public string ReadmeFileUrl { get; private set; } + [JsonIgnore] + public string ReadmeFileUrl { get; internal set; } [JsonIgnore] public Uri ReportAbuseUrl { get; set; } diff --git a/src/NuGet.Core/NuGet.Protocol/Providers/PackageMetadataResourceV3Provider.cs b/src/NuGet.Core/NuGet.Protocol/Providers/PackageMetadataResourceV3Provider.cs index 0916f2db6d5..259156fc3aa 100644 --- a/src/NuGet.Core/NuGet.Protocol/Providers/PackageMetadataResourceV3Provider.cs +++ b/src/NuGet.Core/NuGet.Protocol/Providers/PackageMetadataResourceV3Provider.cs @@ -23,6 +23,7 @@ public override async Task> TryCreate(SourceReposito { var regResource = await source.GetResourceAsync(token); var reportAbuseResource = await source.GetResourceAsync(token); + var readmeResource = await source.GetResourceAsync(token); var packageDetailsUriResource = await source.GetResourceAsync(token); var httpSourceResource = await source.GetResourceAsync(token); @@ -32,7 +33,8 @@ public override async Task> TryCreate(SourceReposito httpSourceResource.HttpSource, regResource, reportAbuseResource, - packageDetailsUriResource); + packageDetailsUriResource, + readmeResource); } return new Tuple(curResource != null, curResource); diff --git a/src/NuGet.Core/NuGet.Protocol/Providers/ReadmeUriTemplateResourceProvider.cs b/src/NuGet.Core/NuGet.Protocol/Providers/ReadmeUriTemplateResourceProvider.cs new file mode 100644 index 00000000000..6acc478494a --- /dev/null +++ b/src/NuGet.Core/NuGet.Protocol/Providers/ReadmeUriTemplateResourceProvider.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Protocol.Core.Types; + +namespace NuGet.Protocol +{ + /// NuGet.Protocol resource provider for . + /// When successful, returns an instance of . + internal class ReadmeUriTemplateResourceProvider : ResourceProvider + { + public ReadmeUriTemplateResourceProvider() + : base(typeof(ReadmeUriTemplateResource), + nameof(ReadmeUriTemplateResource), + NuGetResourceProviderPositions.Last) + { + } + + /// + public override async Task> TryCreate(SourceRepository source, CancellationToken token) + { + ReadmeUriTemplateResource? resource = null; + var serviceIndex = await source.GetResourceAsync(token); + if (serviceIndex != null) + { + var uriTemplate = serviceIndex.GetServiceEntryUri(ServiceTypes.ReadmeFileUrl)?.OriginalString; + + // construct a new resource + resource = string.IsNullOrWhiteSpace(uriTemplate) ? null : new ReadmeUriTemplateResource(uriTemplate); + } + + return new Tuple(resource != null, resource); + } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/Repository.cs b/src/NuGet.Core/NuGet.Protocol/Repository.cs index 606a2a06860..9e4a15d2fca 100644 --- a/src/NuGet.Core/NuGet.Protocol/Repository.cs +++ b/src/NuGet.Core/NuGet.Protocol/Repository.cs @@ -53,6 +53,7 @@ public virtual IEnumerable> GetCoreV3() yield return new Lazy(() => new RegistrationResourceV3Provider()); yield return new Lazy(() => new SymbolPackageUpdateResourceV3Provider()); yield return new Lazy(() => new ReportAbuseResourceV3Provider()); + yield return new Lazy(() => new ReadmeUriTemplateResourceProvider()); yield return new Lazy(() => new PackageDetailsUriResourceV3Provider()); yield return new Lazy(() => new ServiceIndexResourceV3Provider()); yield return new Lazy(() => new ODataServiceDocumentResourceV2Provider()); diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/PackageMetadataResourceV3.cs b/src/NuGet.Core/NuGet.Protocol/Resources/PackageMetadataResourceV3.cs index 7ec23f3c980..f32598f4aae 100644 --- a/src/NuGet.Core/NuGet.Protocol/Resources/PackageMetadataResourceV3.cs +++ b/src/NuGet.Core/NuGet.Protocol/Resources/PackageMetadataResourceV3.cs @@ -21,6 +21,7 @@ public class PackageMetadataResourceV3 : PackageMetadataResource { private readonly RegistrationResourceV3 _regResource; private readonly ReportAbuseResourceV3 _reportAbuseResource; + private readonly ReadmeUriTemplateResource _readmeUriTemplateResource; private readonly PackageDetailsUriResourceV3 _packageDetailsUriResource; private readonly HttpSource _client; @@ -36,6 +37,16 @@ public PackageMetadataResourceV3( _packageDetailsUriResource = packageDetailsUriResource; } + internal PackageMetadataResourceV3( + HttpSource client, + RegistrationResourceV3 regResource, + ReportAbuseResourceV3 reportAbuseResource, + PackageDetailsUriResourceV3 packageDetailsUriResource, + ReadmeUriTemplateResource readmeResource) : this(client, regResource, reportAbuseResource, packageDetailsUriResource) + { + _readmeUriTemplateResource = readmeResource; + } + /// PackageId for package we're looking. /// Whether to include PreRelease versions into result. /// Whether to include Unlisted versions into result. @@ -269,6 +280,10 @@ private void ProcessRegistrationPage( { catalogEntry.ReportAbuseUrl = _reportAbuseResource?.GetReportAbuseUrl(catalogEntry.PackageId, catalogEntry.Version); catalogEntry.PackageDetailsUrl = _packageDetailsUriResource?.GetUri(catalogEntry.PackageId, catalogEntry.Version); + if (string.IsNullOrWhiteSpace(catalogEntry.ReadmeFileUrl)) + { + catalogEntry.ReadmeFileUrl = _readmeUriTemplateResource?.GetReadmeUrl(catalogEntry.PackageId, catalogEntry.Version); + } catalogEntry = metadataCache.GetObject(catalogEntry); results.Add(catalogEntry); } diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/ReadmeUriTemplateResource.cs b/src/NuGet.Core/NuGet.Protocol/Resources/ReadmeUriTemplateResource.cs new file mode 100644 index 00000000000..de928aecd97 --- /dev/null +++ b/src/NuGet.Core/NuGet.Protocol/Resources/ReadmeUriTemplateResource.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +#if NETCOREAPP +using System; +#endif + +namespace NuGet.Protocol +{ + /// + /// A resource that provides the URI for downloading a README file based on a template. + /// + internal class ReadmeUriTemplateResource : INuGetResource + { + private readonly string _uriTemplate; + private const string LowerId = "{lower_id}"; + private const string LowerVersion = "{lower_version}"; + + public ReadmeUriTemplateResource(string uriTemplate) + { + _uriTemplate = uriTemplate; + } + + /// + /// Get the URL for downloading the readme file. + /// + /// The package id + /// The package version + /// URL to download README, built using the URI template. + public string GetReadmeUrl(string id, NuGetVersion version) + { + if (_uriTemplate == null) + { + return string.Empty; + } + + var uriString = _uriTemplate +#if NETCOREAPP + .Replace(LowerId, id.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase) + .Replace(LowerVersion, version.ToNormalizedString().ToLowerInvariant(), StringComparison.OrdinalIgnoreCase); +#else + .Replace(LowerId, id.ToLowerInvariant()) + .Replace(LowerVersion, version.ToNormalizedString().ToLowerInvariant()); +#endif + + return uriString; + } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs b/src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs index 729c6ec0bb1..a8adc9e0cf3 100644 --- a/src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs +++ b/src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs @@ -18,11 +18,13 @@ public static class ServiceTypes public static readonly string Version510 = "/5.1.0"; internal const string Version670 = "/6.7.0"; internal const string Version6110 = "/6.11.0"; + internal const string Version6130 = "/6.13.0"; public static readonly string[] SearchQueryService = { "SearchQueryService" + Versioned, "SearchQueryService" + Version340, "SearchQueryService" + Version300beta }; public static readonly string[] RegistrationsBaseUrl = { $"RegistrationsBaseUrl{Versioned}", $"RegistrationsBaseUrl{Version360}", $"RegistrationsBaseUrl{Version340}", $"RegistrationsBaseUrl{Version300rc}", $"RegistrationsBaseUrl{Version300beta}", "RegistrationsBaseUrl" }; public static readonly string[] SearchAutocompleteService = { "SearchAutocompleteService" + Versioned, "SearchAutocompleteService" + Version300beta }; public static readonly string[] ReportAbuse = { "ReportAbuseUriTemplate" + Versioned, "ReportAbuseUriTemplate" + Version300 }; + internal static readonly string[] ReadmeFileUrl = { "ReadmeUriTemplate" + Versioned, "ReadmeUriTemplate" + Version6130 }; public static readonly string[] PackageDetailsUriTemplate = { "PackageDetailsUriTemplate" + Version510 }; public static readonly string[] LegacyGallery = { "LegacyGallery" + Versioned, "LegacyGallery" + Version200 }; public static readonly string[] PackagePublish = { "PackagePublish" + Versioned, "PackagePublish" + Version200 }; diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/ViewModels/ReadmePreviewViewModelTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/ViewModels/ReadmePreviewViewModelTests.cs index ecd4a848c0e..8037fec25cf 100644 --- a/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/ViewModels/ReadmePreviewViewModelTests.cs +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/ViewModels/ReadmePreviewViewModelTests.cs @@ -36,7 +36,7 @@ public void Constructor_Defaults() var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.All, true); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(string.Empty, target.ReadmeMarkdown); } @@ -56,7 +56,7 @@ public async Task SetPackageMetadataAsync_WithoutReadmeUrl_NoReadmeReturned(stri await target.SetPackageMetadataAsync(package, CancellationToken.None); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(string.Empty, target.ReadmeMarkdown); } @@ -77,7 +77,7 @@ public async Task SetCurrentFilter_ItemFilterAll_NoLocalReadmeReturned() await target.ItemFilterChangedAsync(ItemFilter.All); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(string.Empty, target.ReadmeMarkdown); } @@ -101,7 +101,7 @@ public async Task SetCurrentFilter_ItemFilterRenderingLocalReadme_LocalReadmeRet await target.ItemFilterChangedAsync(filter); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(readmeContents, target.ReadmeMarkdown); } @@ -121,7 +121,7 @@ public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_RenderLocalReadmeTr await target.SetPackageMetadataAsync(package, CancellationToken.None); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(readmeContents, target.ReadmeMarkdown); } @@ -141,7 +141,7 @@ public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_RenderLocalReadmeFa await target.SetPackageMetadataAsync(package, CancellationToken.None); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(string.Empty, target.ReadmeMarkdown); } @@ -159,7 +159,7 @@ public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_FileNotFound_NoRead await target.SetPackageMetadataAsync(package, CancellationToken.None); //Assert - Assert.False(target.ErrorLoadingReadme); + Assert.False(target.ErrorWithReadme); Assert.Equal(Resources.Text_NoReadme, target.ReadmeMarkdown); } } diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Providers/ReadmeUriTemplateResourceProviderTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Providers/ReadmeUriTemplateResourceProviderTests.cs new file mode 100644 index 00000000000..24c0e6c21cf --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Providers/ReadmeUriTemplateResourceProviderTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Newtonsoft.Json.Linq; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; +using Xunit; + +namespace NuGet.Protocol.Tests.Providers +{ + public class ReadmeUriTemplateResourceProviderTests + { + private const string ResourceType = "ReadmeUriTemplate/6.13.0"; + + private readonly PackageSource _packageSource; + private readonly ReadmeUriTemplateResourceProvider _target; + + public ReadmeUriTemplateResourceProviderTests() + { + _packageSource = new PackageSource("https://unit.test"); + _target = new ReadmeUriTemplateResourceProvider(); + } + + [Fact] + public async Task TryCreate_WhenResourceDoesNotExist_ReturnsNull() + { + var resourceProviders = new ResourceProvider[] + { + CreateServiceIndexResourceV3Provider(), + _target + }; + var sourceRepository = new SourceRepository(_packageSource, resourceProviders); + + Tuple result = await _target.TryCreate(sourceRepository, CancellationToken.None); + + Assert.False(result.Item1); + Assert.Null(result.Item2); + } + + [Fact] + public async Task TryCreate_WhenResourceExists_ReturnsValidResourceAsync() + { + var serviceEntry = new RawServiceIndexEntry("https://unit.test/packages/{lower_id}/{lower_version}/readme", ResourceType); + var resourceProviders = new ResourceProvider[] + { + CreateServiceIndexResourceV3Provider(serviceEntry), + _target + }; + var sourceRepository = new SourceRepository(_packageSource, resourceProviders); + + Tuple result = await _target.TryCreate(sourceRepository, CancellationToken.None); + + Assert.True(result.Item1); + Assert.IsType(result.Item2); + } + + private static ServiceIndexResourceV3Provider CreateServiceIndexResourceV3Provider(params RawServiceIndexEntry[] entries) + { + var provider = new Mock(); + + provider.Setup(x => x.Name) + .Returns(nameof(ServiceIndexResourceV3Provider)); + provider.Setup(x => x.ResourceType) + .Returns(typeof(ServiceIndexResourceV3)); + + var resources = new JArray(); + + foreach (var entry in entries) + { + resources.Add( + new JObject( + new JProperty("@id", entry.Uri), + new JProperty("@type", entry.Type))); + } + + var index = new JObject(); + + index.Add("version", "3.0.0"); + index.Add("resources", resources); + index.Add("@context", + new JObject( + new JProperty("@vocab", "http://schema.nuget.org/schema#"), + new JProperty("comment", "http://www.w3.org/2000/01/rdf-schema#comment"))); + + var serviceIndexResource = new ServiceIndexResourceV3(index, DateTime.UtcNow); + var tryCreateResult = new Tuple(true, serviceIndexResource); + + provider.Setup(x => x.TryCreate(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(tryCreateResult)); + + return provider.Object; + } + + private class RawServiceIndexEntry + { + public RawServiceIndexEntry(string uri, string type) + { + Uri = uri; + Type = type ?? throw new ArgumentNullException(nameof(type)); + } + + public string Uri { get; } + public string Type { get; } + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/ReadmeUriTemplateResourceTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/ReadmeUriTemplateResourceTests.cs new file mode 100644 index 00000000000..4fc29638c39 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/ReadmeUriTemplateResourceTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using NuGet.Versioning; +using Xunit; + +namespace NuGet.Protocol.Tests +{ + public class ReadmeUriTemplateResourceTests + { + [Theory] + [InlineData("")] + [InlineData(null)] + public void ReadmeUriTemplateResource_GetReadmeUrl_BlankTemplate_ReturnsEmptyString(string uriTemplate) + { + string expectedResult = string.Empty; + var resource = new ReadmeUriTemplateResource(uriTemplate); + + var actual = resource.GetReadmeUrl("TestPackage", NuGetVersion.Parse("1.0.0")); + + Assert.Equal(expectedResult, actual); + } + + [Fact] + public void ReadmeUriTemplateResource_GetReadmeUrl_ReturnsFormedUrl() + { + const string uriTemplate = "https://test.nuget.org/{lower_id}/{lower_version}/readme"; + const string expectedResult = "https://test.nuget.org/testpackage/1.0.0/readme"; + var resource = new ReadmeUriTemplateResource(uriTemplate); + + var actual = resource.GetReadmeUrl("TestPackage", NuGetVersion.Parse("1.0.0")); + + Assert.Equal(expectedResult, actual.ToString()); + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/RepositoryTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/RepositoryTests.cs index 0546f38aae5..a08cd7b0249 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/RepositoryTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/RepositoryTests.cs @@ -63,7 +63,7 @@ public void Provider_WithDefaultProvider_ReturnsDefaultResourceProviders() int actualCount = resourceProviders.Count(); - Assert.Equal(48, actualCount); + Assert.Equal(49, actualCount); } } }