Skip to content

Commit b978258

Browse files
authored
display package on detail page(#8166)
*unit test *rename *move saveReadmeFileAsyn to PackageFileService
1 parent ec26c1d commit b978258

6 files changed

Lines changed: 362 additions & 11 deletions

File tree

src/NuGetGallery/Controllers/PackagesController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2624,7 +2624,7 @@ protected virtual async Task<JsonResult> VerifyPackageInternal(
26242624

26252625
if (formData.Edit != null)
26262626
{
2627-
if (package.HasReadMe && package.EmbeddedReadmeType != EmbeddedReadmeFileType.Absent)
2627+
if (_readMeService.HasReadMeSource(formData.Edit.ReadMe) && package.HasReadMe && package.EmbeddedReadmeType != EmbeddedReadmeFileType.Absent)
26282628
{
26292629
return Json(HttpStatusCode.BadRequest, new[] { new JsonValidationMessage(Strings.ReadmeNotEditableWithEmbeddedReadme) });
26302630
}

src/NuGetGallery/Services/IPackageFileService.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.IO;
56
using System.Threading.Tasks;
67
using System.Web.Mvc;
78
using NuGet.Services.Entities;
@@ -33,6 +34,22 @@ public interface IPackageFileService : ICorePackageFileService
3334
/// <param name="readMeMd">Markdown content.</param>
3435
Task SaveReadMeMdFileAsync(Package package, string readMeMd);
3536

37+
/// <summary>
38+
/// Save the readme file from package stream. This method should throw if the package
39+
/// does not have an embedded readme file
40+
/// </summary>
41+
/// <param name="package">Package information.</param>
42+
/// <param name="packageStream">Package stream with .nupkg contents.</param>
43+
Task ExtractAndSaveReadmeFileAsync(Package package, Stream packageStream);
44+
45+
/// <summary>
46+
/// Saves the package readme.md file to storage. This method should throw if the package
47+
/// does not have an embedded readme file
48+
/// </summary>
49+
/// <param name="package">The package associated with the readme.</param>
50+
/// <param name="readmeFile">The content of readme file.</param>
51+
Task SaveReadmeFileAsync(Package package, Stream readmeFile);
52+
3653
/// <summary>
3754
/// Downloads the readme.md from storage.
3855
/// </summary>

src/NuGetGallery/Services/PackageFileService.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System.Text;
77
using System.Threading.Tasks;
88
using System.Web.Mvc;
9+
using NuGet.Packaging;
910
using NuGet.Services.Entities;
11+
using NuGetGallery.Packaging;
1012

1113
namespace NuGetGallery
1214
{
@@ -73,6 +75,70 @@ public async Task SaveReadMeMdFileAsync(Package package, string readMeMd)
7375
}
7476
}
7577

78+
/// <summary>
79+
/// Save the readme file from package stream. This method should throw if the package
80+
/// does not have an embedded readme file
81+
/// </summary>
82+
/// <param name="package">Package information.</param>
83+
/// <param name="packageStream">Package stream with .nupkg contents.</param>
84+
public async Task ExtractAndSaveReadmeFileAsync(Package package, Stream packageStream)
85+
{
86+
if (package == null)
87+
{
88+
throw new ArgumentNullException(nameof(package));
89+
}
90+
91+
if (packageStream == null)
92+
{
93+
throw new ArgumentNullException(nameof(packageStream));
94+
}
95+
96+
packageStream.Seek(0, SeekOrigin.Begin);
97+
using (var packageArchiveReader = new PackageArchiveReader(packageStream, leaveStreamOpen: true))
98+
{
99+
var packageMetadata = PackageMetadata.FromNuspecReader(packageArchiveReader.GetNuspecReader(), strict: true);
100+
if (string.IsNullOrWhiteSpace(packageMetadata.ReadmeFile))
101+
{
102+
throw new InvalidOperationException("No readme file specified in the nuspec");
103+
}
104+
105+
var filename = FileNameHelper.GetZipEntryPath(packageMetadata.ReadmeFile);
106+
var ReadmeFileEntry = packageArchiveReader.GetEntry(filename); // throws on non-existent file
107+
using (var readmeFileStream = ReadmeFileEntry.Open())
108+
{
109+
await SaveReadmeFileAsync(package, readmeFileStream);
110+
}
111+
}
112+
}
113+
114+
/// <summary>
115+
/// Saves the package readme.md file to storage. This method should throw if the package
116+
/// does not have an embedded readme file
117+
/// </summary>
118+
/// <param name="package">The package associated with the readme.</param>
119+
/// <param name="readmeFile">The content of readme file.</param>
120+
public Task SaveReadmeFileAsync(Package package, Stream readmeFile)
121+
{
122+
if (package == null)
123+
{
124+
throw new ArgumentNullException(nameof(package));
125+
}
126+
127+
if (readmeFile == null)
128+
{
129+
throw new ArgumentNullException(nameof(readmeFile));
130+
}
131+
132+
if (package.EmbeddedReadmeType == EmbeddedReadmeFileType.Absent)
133+
{
134+
throw new ArgumentException("Package must have an embedded readme", nameof(package));
135+
}
136+
137+
var fileName = FileNameHelper.BuildFileName(package, ReadMeFilePathTemplateActive, ServicesConstants.MarkdownFileExtension);
138+
139+
return _fileStorageService.SaveFileAsync(CoreConstants.Folders.PackageReadMesFolderName, fileName, readmeFile, overwrite: true);
140+
}
141+
76142
/// <summary>
77143
/// Downloads the readme.md from storage.
78144
/// </summary>

src/NuGetGallery/Services/PackageUploadService.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public PackageUploadService(
4747
throw new ArgumentNullException(nameof(diagnosticsService));
4848
}
4949
_vulnerabilityService = vulnerabilityService ?? throw new ArgumentNullException(nameof(vulnerabilityService));
50-
_metadataValidationService = metadataValidationService ?? throw new ArgumentNullException(nameof(metadataValidationService));
50+
_metadataValidationService = metadataValidationService ?? throw new ArgumentNullException(nameof(metadataValidationService));
5151
}
5252

5353
public async Task<PackageValidationResult> ValidateBeforeGeneratePackageAsync(
@@ -189,16 +189,32 @@ await _packageFileService.DeleteValidationPackageFileAsync(
189189
// validation pipeline that would normally store the license file, so we'll do it ourselves here.
190190
await _coreLicenseFileService.ExtractAndSaveLicenseFileAsync(package, packageFile);
191191
}
192+
193+
var isReadmeFileExtractedAndSaved = false;
194+
if (package.HasReadMe && package.EmbeddedReadmeType != EmbeddedReadmeFileType.Absent)
195+
{
196+
await _packageFileService.ExtractAndSaveReadmeFileAsync(package, packageFile);
197+
isReadmeFileExtractedAndSaved = true;
198+
}
199+
192200
try
193201
{
194202
packageFile.Seek(0, SeekOrigin.Begin);
195203
await _packageFileService.SavePackageFileAsync(package, packageFile);
196204
}
197-
catch when (package.EmbeddedLicenseType != EmbeddedLicenseFileType.Absent)
205+
catch when (package.EmbeddedLicenseType != EmbeddedLicenseFileType.Absent || isReadmeFileExtractedAndSaved)
198206
{
199-
await _coreLicenseFileService.DeleteLicenseFileAsync(
200-
package.PackageRegistration.Id,
201-
package.NormalizedVersion);
207+
if (package.EmbeddedLicenseType != EmbeddedLicenseFileType.Absent)
208+
{
209+
await _coreLicenseFileService.DeleteLicenseFileAsync(
210+
package.PackageRegistration.Id,
211+
package.NormalizedVersion);
212+
}
213+
214+
if (isReadmeFileExtractedAndSaved)
215+
{
216+
await _packageFileService.DeleteReadMeMdFileAsync(package);
217+
}
202218
throw;
203219
}
204220
}
@@ -237,6 +253,7 @@ await _packageFileService.DeletePackageFileAsync(
237253
await _coreLicenseFileService.DeleteLicenseFileAsync(
238254
package.PackageRegistration.Id,
239255
package.NormalizedVersion);
256+
await _packageFileService.DeleteReadMeMdFileAsync(package);
240257
}
241258

242259
return ReturnConflictOrThrow(ex);

tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Web.Mvc;
1010
using Moq;
1111
using NuGet.Services.Entities;
12+
using NuGetGallery.TestUtils;
1213
using Xunit;
1314

1415
namespace NuGetGallery
@@ -262,6 +263,133 @@ public async Task WhenValid_SavesReadMeFile()
262263
}
263264
}
264265

266+
public class TheSaveReadmeFileAsyncMethod
267+
{
268+
[Fact]
269+
public async Task WhenPackageNull_ThrowsArgumentNullException()
270+
{
271+
var service = CreateService();
272+
273+
await Assert.ThrowsAsync<ArgumentNullException>(async () => await service.SaveReadmeFileAsync(null, Stream.Null));
274+
}
275+
276+
[Fact]
277+
public async Task WhenStreamIsNull_ThrowsArgumentException()
278+
{
279+
var service = CreateService();
280+
var package = CreatePackage();
281+
282+
await Assert.ThrowsAsync<ArgumentNullException>(async () => await service.SaveReadmeFileAsync(package, null));
283+
}
284+
285+
[Fact]
286+
public async Task WhenEmbeddedReadmeTypeIsAbsent_ThrowsArgumentException()
287+
{
288+
var service = CreateService();
289+
var package = CreatePackage();
290+
package.EmbeddedReadmeType = EmbeddedReadmeFileType.Absent;
291+
var packageStream = GeneratePackageWithReadmeFile("readme.md");
292+
293+
var ex = await Assert.ThrowsAsync<ArgumentException>(() => service.SaveReadmeFileAsync(package, packageStream));
294+
Assert.Equal("package", ex.ParamName);
295+
Assert.Contains("embedded readme", ex.Message);
296+
}
297+
298+
[Fact]
299+
public async Task WhenValid_SavesReadmeFile()
300+
{
301+
// Arrange.
302+
var fileServiceMock = new Mock<IFileStorageService>();
303+
fileServiceMock.Setup(f => f.SaveFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<bool>()))
304+
.Returns(Task.CompletedTask)
305+
.Verifiable();
306+
var service = CreateService(fileServiceMock);
307+
308+
var package = new Package()
309+
{
310+
PackageRegistration = new PackageRegistration() { Id = "Foo" },
311+
Version = "1.0.0",
312+
EmbeddedReadmeType = EmbeddedReadmeFileType.Markdown
313+
};
314+
var packageStream = GeneratePackageWithReadmeFile("readme.md");
315+
316+
// Act.
317+
await service.SaveReadmeFileAsync(package, packageStream);
318+
319+
// Assert.
320+
fileServiceMock.Verify(f => f.SaveFileAsync(CoreConstants.Folders.PackageReadMesFolderName, "active/foo/1.0.0.md", It.IsAny<Stream>(), true),
321+
Times.Once);
322+
}
323+
}
324+
325+
public class ExtractAndSaveReadmeFileAsyncMethod
326+
{
327+
[Fact]
328+
public async Task ThrowsWhenPackageIsNull()
329+
{
330+
var service = CreateService();
331+
var ex = await Assert.ThrowsAsync<ArgumentNullException>(() => service.ExtractAndSaveReadmeFileAsync(
332+
package: null,
333+
packageStream: Mock.Of<Stream>()));
334+
335+
Assert.Equal("package", ex.ParamName);
336+
}
337+
338+
[Fact]
339+
public async Task ThrowsWhenPackagesStreamIsNull()
340+
{
341+
var service = CreateService();
342+
var ex = await Assert.ThrowsAsync<ArgumentNullException>(() => service.ExtractAndSaveReadmeFileAsync(
343+
package: Mock.Of<Package>(),
344+
packageStream: null));
345+
346+
Assert.Equal("packageStream", ex.ParamName);
347+
}
348+
349+
[Fact]
350+
public async Task ThrowsOnMissingReadmeFile()
351+
{
352+
var service = CreateService();
353+
const string readmeFileName = "readme.md";
354+
var packageStream = GeneratePackageWithReadmeFile(readmeFileName, false);
355+
var pacakge = PackageServiceUtility.CreateTestPackage();
356+
357+
var ex = await Assert.ThrowsAsync<FileNotFoundException>(() => service.ExtractAndSaveReadmeFileAsync(pacakge, packageStream));
358+
Assert.Contains(readmeFileName, ex.Message);
359+
}
360+
361+
[Theory]
362+
[InlineData("readme.md")]
363+
[InlineData("foo\\readme.md")]
364+
[InlineData("foo/readme.md")]
365+
public async Task SavesReadmeFile(String readmeFileName)
366+
{
367+
var fileServiceMock = new Mock<IFileStorageService>();
368+
var service = CreateService(fileServiceMock);
369+
var packageStream = GeneratePackageWithReadmeFile(readmeFileName);
370+
var package = CreatePackage();
371+
package.HasReadMe = true;
372+
package.EmbeddedReadmeType = EmbeddedReadmeFileType.Markdown;
373+
var savedReadmeBytes = new byte[ReadmeFileContents.Length];
374+
375+
fileServiceMock.Setup(x => x.SaveFileAsync(
376+
CoreConstants.Folders.PackageReadMesFolderName,
377+
It.IsAny<string>(),
378+
It.IsAny<Stream>(),
379+
true))
380+
.Completes()
381+
.Callback<string, string, Stream, bool>((_, __, s, ___) => s.Read(savedReadmeBytes, 0, savedReadmeBytes.Length))
382+
.Verifiable();
383+
384+
// Act.
385+
await service.ExtractAndSaveReadmeFileAsync(package, packageStream);
386+
387+
// Assert.
388+
fileServiceMock.VerifyAll();
389+
Assert.Equal(ReadmeFileContents, savedReadmeBytes);
390+
}
391+
}
392+
265393
public class TheDownloadReadMeMdFileAsyncMethod
266394
{
267395
[Fact]
@@ -353,9 +481,13 @@ static Package CreatePackage()
353481
return package;
354482
}
355483

356-
static MemoryStream CreatePackageFileStream()
484+
private static byte[] ReadmeFileContents => Encoding.UTF8.GetBytes("Sample readme md file");
485+
486+
private static MemoryStream GeneratePackageWithReadmeFile(string readmeFileName = null, bool saveReadmeFile = true)
357487
{
358-
return new MemoryStream(new byte[] { 0, 0, 1, 0, 1, 0, 1, 0 }, 0, 8, true, true);
488+
return PackageServiceUtility.CreateNuGetPackageStream(
489+
readmeFilename: readmeFileName,
490+
readmeFileContents: readmeFileName != null && saveReadmeFile ? ReadmeFileContents : null);
359491
}
360492

361493
static PackageFileService CreateService(Mock<IFileStorageService> fileStorageSvc = null)

0 commit comments

Comments
 (0)