Skip to content

Commit

Permalink
Adding support for ~/NuGet.exe route that returns the exe
Browse files Browse the repository at this point in the history
corresponding to the latest NuGet.CommandLine package
  • Loading branch information
pranavkm committed May 11, 2012
1 parent 90a05bc commit f9e29c8
Show file tree
Hide file tree
Showing 16 changed files with 426 additions and 38 deletions.
50 changes: 49 additions & 1 deletion Facts/Controllers/ApiControllerFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,51 @@ public void WillCreateAPackageWithTheUserMatchingTheApiKey()

packageSvc.Verify(x => x.CreatePackage(It.IsAny<IPackage>(), matchingUser));
}

[Fact]
public void CreatePackageRefreshesNuGetExeIfCommandLinePackageIsUploaded()
{
// Arrange
var nuGetPackage = new Mock<IPackage>();
nuGetPackage.Setup(x => x.Id).Returns("NuGet.CommandLine");
nuGetPackage.Setup(x => x.Version).Returns(new SemanticVersion("1.0.42"));
var packageSvc = new Mock<IPackageService>();
packageSvc.Setup(p => p.CreatePackage(nuGetPackage.Object, It.IsAny<User>())).Returns(new Package { IsLatestStable = true });
var userSvc = new Mock<IUserService>();
var nugetExeDownloader = new Mock<INuGetExeDownloaderService>(MockBehavior.Strict);
nugetExeDownloader.Setup(s => s.UpdateExecutable(nuGetPackage.Object)).Verifiable();
var matchingUser = new User();
userSvc.Setup(x => x.FindByApiKey(It.IsAny<Guid>())).Returns(matchingUser);
var controller = CreateController(userSvc: userSvc, packageSvc: packageSvc, nugetExeDownloader: nugetExeDownloader, packageFromInputStream: nuGetPackage.Object);

// Act
controller.CreatePackagePut(Guid.NewGuid().ToString());

// Assert
nugetExeDownloader.Verify();
}

[Fact]
public void CreatePackageDoesNotRefreshNuGetExeIfItIsNotLatestStable()
{
// Arrange
var nuGetPackage = new Mock<IPackage>();
nuGetPackage.Setup(x => x.Id).Returns("NuGet.CommandLine");
nuGetPackage.Setup(x => x.Version).Returns(new SemanticVersion("2.0.0-alpha"));
var packageSvc = new Mock<IPackageService>();
packageSvc.Setup(p => p.CreatePackage(nuGetPackage.Object, It.IsAny<User>())).Returns(new Package { IsLatest = true, IsLatestStable = false });
var userSvc = new Mock<IUserService>();
var nugetExeDownloader = new Mock<INuGetExeDownloaderService>(MockBehavior.Strict);
var matchingUser = new User();
userSvc.Setup(x => x.FindByApiKey(It.IsAny<Guid>())).Returns(matchingUser);
var controller = CreateController(userSvc: userSvc, packageSvc: packageSvc, nugetExeDownloader: nugetExeDownloader, packageFromInputStream: nuGetPackage.Object);

// Act
controller.CreatePackagePut(Guid.NewGuid().ToString());

// Assert
nugetExeDownloader.Verify(s => s.UpdateExecutable(It.IsAny<IPackage>()), Times.Never());
}
}

public class TheDeletePackageAction
Expand Down Expand Up @@ -492,12 +537,15 @@ private static ApiController CreateController(
Mock<IPackageService> packageSvc = null,
Mock<IPackageFileService> fileService = null,
Mock<IUserService> userSvc = null,
Mock<INuGetExeDownloaderService> nugetExeDownloader = null,
IPackage packageFromInputStream = null)
{
packageSvc = packageSvc ?? new Mock<IPackageService>();
userSvc = userSvc ?? new Mock<IUserService>();
fileService = fileService ?? new Mock<IPackageFileService>(MockBehavior.Strict);
var controller = new Mock<ApiController>(packageSvc.Object, fileService.Object, userSvc.Object);
nugetExeDownloader = nugetExeDownloader ?? new Mock<INuGetExeDownloaderService>(MockBehavior.Strict);

var controller = new Mock<ApiController>(packageSvc.Object, fileService.Object, userSvc.Object, nugetExeDownloader.Object);
controller.CallBase = true;
if (packageFromInputStream != null)
controller.Setup(x => x.ReadPackageFromRequest()).Returns(packageFromInputStream);
Expand Down
1 change: 1 addition & 0 deletions Facts/Facts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<Compile Include="Services\CloudBlobFileStorageServiceFacts.cs" />
<Compile Include="Services\FileSystemFileStorageServiceFacts.cs" />
<Compile Include="Services\MessageServiceFacts.cs" />
<Compile Include="Services\NuGetExeDownloaderServiceFacts.cs" />
<Compile Include="Services\PackageServiceFacts.cs" />
<Compile Include="Services\UploadFileServiceFacts.cs" />
<Compile Include="Services\UsersServiceFacts.cs" />
Expand Down
161 changes: 161 additions & 0 deletions Facts/Services/NuGetExeDownloaderServiceFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System;
using System.IO;
using System.Linq;
using System.Web.Mvc;
using Moq;
using NuGet;
using Xunit;

namespace NuGetGallery.Services
{
public class NuGetExeDownloaderServiceFacts
{
private static readonly string _exePath = @"x:\NuGetGallery\nuget.exe";

[Fact]
public void CreateNuGetExeDownloadDoesNotExtractFileIfItAlreadyExistsAndIsRecent()
{
// Arrange
var fileSystem = new Mock<IFileSystemService>(MockBehavior.Strict);
fileSystem.Setup(s => s.FileExists(_exePath)).Returns(true).Verifiable();
fileSystem.Setup(s => s.GetCreationTimeUtc(_exePath))
.Returns(DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(60)))
.Verifiable();

// Act
var downloaderSvc = GetDownloaderService(fileSystemSvc: fileSystem);
var result = downloaderSvc.CreateNuGetExeDownloadActionnResult();

// Assert
fileSystem.Verify();
AssertActionResult(result);
}

[Fact]
public void CreateNuGetExeDownloadExtractsFileIfItDoesNotExist()
{
// Arrange
var fileSystem = new Mock<IFileSystemService>(MockBehavior.Strict);
fileSystem.Setup(s => s.FileExists(_exePath)).Returns(false);
fileSystem.Setup(s => s.OpenWrite(_exePath)).Returns(Stream.Null);

var package = new Package { Version = "2.0.0" };
var packageService = new Mock<IPackageService>(MockBehavior.Strict);
packageService.Setup(s => s.FindPackageByIdAndVersion("NuGet.CommandLine", null, false))
.Returns(package)
.Verifiable();
var packageFileSvc = new Mock<IPackageFileService>(MockBehavior.Strict);
packageFileSvc.Setup(s => s.DownloadPackageFile(package))
.Returns(CreateCommandLinePackage)
.Verifiable();

// Act
var downloaderSvc = GetDownloaderService(packageService, packageFileSvc, fileSystem);
var result = downloaderSvc.CreateNuGetExeDownloadActionnResult();

// Assert
packageFileSvc.Verify();
packageService.Verify();
AssertActionResult(result);
}

[Fact]
public void CreateNuGetExeDownloadExtractsFileIfItExistsButIsNotRecent()
{
// Arrange
var fileSystem = new Mock<IFileSystemService>(MockBehavior.Strict);
fileSystem.Setup(s => s.FileExists(_exePath)).Returns(true);
fileSystem.Setup(s => s.GetCreationTimeUtc(_exePath))
.Returns(DateTime.UtcNow.Subtract(TimeSpan.FromHours(32)));

This comment has been minimized.

Copy link
@jeffhandley

jeffhandley May 29, 2012

Member

Why have a 32-hour staleness if we're proactively updating it whenever NuGet.CommandLine is uploaded?

fileSystem.Setup(s => s.OpenWrite(_exePath)).Returns(Stream.Null);

var package = new Package { Version = "2.0.0" };
var packageService = new Mock<IPackageService>(MockBehavior.Strict);
packageService.Setup(s => s.FindPackageByIdAndVersion("NuGet.CommandLine", null, false))
.Returns(package)
.Verifiable();
var packageFileSvc = new Mock<IPackageFileService>(MockBehavior.Strict);
packageFileSvc.Setup(s => s.DownloadPackageFile(package))
.Returns(CreateCommandLinePackage)
.Verifiable();

// Act
var downloaderSvc = GetDownloaderService(packageService, packageFileSvc, fileSystem);
var result = downloaderSvc.CreateNuGetExeDownloadActionnResult();

// Assert
packageFileSvc.Verify();
packageService.Verify();
AssertActionResult(result);
}

[Fact]
public void UpdateExecutableExtractsExeToDisk()
{
// Arrange
var fileSystem = new Mock<IFileSystemService>(MockBehavior.Strict);
fileSystem.Setup(s => s.OpenWrite(_exePath)).Returns(Stream.Null);

var nugetPackage = new Mock<IPackage>();
nugetPackage.Setup(s => s.GetFiles()).Returns(new[] { CreateExePackageFile() }.AsQueryable());

// Act
var downloaderSvc = GetDownloaderService(fileSystemSvc: fileSystem);
downloaderSvc.UpdateExecutable(nugetPackage.Object);

// Assert
fileSystem.Verify();
}

private static void AssertActionResult(ActionResult result)
{
Assert.IsType<FilePathResult>(result);
var filePathResult = (FilePathResult)result;
Assert.Equal(_exePath, filePathResult.FileName);
Assert.Equal(@"application/octet-stream", filePathResult.ContentType);
Assert.Equal(@"NuGet.exe", filePathResult.FileDownloadName);

This comment has been minimized.

Copy link
@jeffhandley

jeffhandley May 29, 2012

Member

Let's normalize this to lower-case "nuget.exe" - we're seeing some Mono bugs come in about case-sensitivity.

}

private static Stream CreateCommandLinePackage()
{
var packageBuilder = new PackageBuilder
{
Id = "NuGet.CommandLine",
Version = new SemanticVersion("2.0.0"),
Description = "Some desc"
};
packageBuilder.Authors.Add("test");
var exeFile = CreateExePackageFile();
packageBuilder.Files.Add(exeFile);

var memoryStream = new MemoryStream();
packageBuilder.Save(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);

return memoryStream;
}

private static IPackageFile CreateExePackageFile()
{
var exeFile = new Mock<IPackageFile>();
exeFile.Setup(s => s.Path).Returns(@"tools\NuGet.exe");
exeFile.Setup(s => s.GetStream()).Returns(Stream.Null);
return exeFile.Object;
}

private static NuGetExeDownloaderService GetDownloaderService(
Mock<IPackageService> packageSvc = null,
Mock<IPackageFileService> packageFileSvc = null,
Mock<IFileSystemService> fileSystemSvc = null)
{
packageSvc = packageSvc ?? new Mock<IPackageService>(MockBehavior.Strict);
packageFileSvc = packageFileSvc ?? new Mock<IPackageFileService>(MockBehavior.Strict);
fileSystemSvc = fileSystemSvc ?? new Mock<IFileSystemService>(MockBehavior.Strict);

return new NuGetExeDownloaderService(packageSvc.Object, packageFileSvc.Object, fileSystemSvc.Object)
{
NuGetExePath = _exePath
};
}
}
}
6 changes: 6 additions & 0 deletions Website/ApiController.generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public System.Web.Mvc.ActionResult PublishPackage() {
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
public class ActionNamesClass {
public readonly string GetPackage = "GetPackageApi";
public readonly string GetNuGetExe = "GetNuGetExeApi";
public readonly string VerifyPackageKey = "VerifyPackageKeyApi";
public readonly string CreatePackagePut = "PushPackageApi";
public readonly string CreatePackagePost = "PushPackageApi";
Expand Down Expand Up @@ -102,6 +103,11 @@ public override System.Web.Mvc.ActionResult GetPackage(string id, string version
return callInfo;
}

public override System.Web.Mvc.ActionResult GetNuGetExe() {
var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.GetNuGetExe);
return callInfo;
}

public override System.Web.Mvc.ActionResult VerifyPackageKey(string apiKey, string id, string version) {
var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.VerifyPackageKey);
callInfo.RouteValueDictionary.Add("apiKey", apiKey);
Expand Down
4 changes: 4 additions & 0 deletions Website/App_Start/ContainerBindings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public override void Load()
.To<LuceneIndexingService>()
.InRequestScope();

Bind<INuGetExeDownloaderService>()
.To<NuGetExeDownloaderService>()
.InRequestScope();

Lazy<IMailSender> mailSenderThunk = new Lazy<IMailSender>(() =>
{
var settings = Kernel.Get<GallerySetting>();
Expand Down
5 changes: 5 additions & 0 deletions Website/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ public static void RegisterRoutes(RouteCollection routes)
"api/v2/",
typeof(V2Feed));

routes.MapRoute(
RouteName.DownloadNuGetExe,
"nuget.exe",
new { controller = MVC.Api.Name, action = MVC.Api.ActionNames.GetNuGetExe });

// Redirected Legacy Routes

routes.Redirect(
Expand Down
1 change: 1 addition & 0 deletions Website/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class Constants
public const int DefaultPasswordResetTokenExpirationHours = 24;
public const int MaxEmailSubjectLength = 255;
public const string PackageContentType = "application/zip";
public const string OctetStreamContentType = "application/octet-stream";
public const string NuGetPackageFileExtension = ".nupkg";
public const string PackageFileDownloadUriTemplate = "packages/{0}/{1}/download";
public const string PackageFileSavePathTemplate = "{0}.{1}{2}";
Expand Down
29 changes: 26 additions & 3 deletions Website/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@
using System.Linq;
using System.Net;
using System.Web.Mvc;
using System.Web.UI;
using NuGet;

namespace NuGetGallery
{
public partial class ApiController : Controller
{
private const string NuGetCommandLinePackage = "NuGet.CommandLine";
private readonly IPackageService packageSvc;
private readonly IUserService userSvc;
private readonly IPackageFileService packageFileSvc;
private readonly INuGetExeDownloaderService nugetExeDownloaderSvc;

public ApiController(IPackageService packageSvc, IPackageFileService packageFileSvc, IUserService userSvc)
public ApiController(IPackageService packageSvc,
IPackageFileService packageFileSvc,
IUserService userSvc,
INuGetExeDownloaderService nugetExeDownloaderSvc)
{
this.packageSvc = packageSvc;
this.packageFileSvc = packageFileSvc;
this.userSvc = userSvc;
this.nugetExeDownloaderSvc = nugetExeDownloaderSvc;
}

[ActionName("GetPackageApi"), HttpGet]
[ActionName("GetPackageApi"), HttpGet]
public virtual ActionResult GetPackage(string id, string version)
{
// if the version is null, the user is asking for the latest version. Presumably they don't want pre release versions.
Expand All @@ -38,7 +45,17 @@ public virtual ActionResult GetPackage(string id, string version)
if (!string.IsNullOrWhiteSpace(package.ExternalPackageUrl))
return Redirect(package.ExternalPackageUrl);
else
{
return packageFileSvc.CreateDownloadPackageActionResult(package);
}
}

[ActionName("GetNuGetExeApi"),
HttpGet,
OutputCache(VaryByParam = "none", Location = OutputCacheLocation.ServerAndClient, Duration = 600)]
public virtual ActionResult GetNuGetExe()
{
return nugetExeDownloaderSvc.CreateNuGetExeDownloadActionnResult();
}

[ActionName("VerifyPackageKeyApi"), HttpGet]
Expand Down Expand Up @@ -112,7 +129,13 @@ private ActionResult CreatePackageInternal(string apiKey)
}
}

packageSvc.CreatePackage(packageToPush, user);
var package = packageSvc.CreatePackage(packageToPush, user);
if (packageToPush.Id.Equals(NuGetCommandLinePackage, StringComparison.OrdinalIgnoreCase) && package.IsLatestStable)
{
// If we're pushing a new stable version of NuGet.CommandLine, update the extracted executable.
nugetExeDownloaderSvc.UpdateExecutable(packageToPush);
}

return new HttpStatusCodeResult(201);
}

Expand Down
1 change: 1 addition & 0 deletions Website/RouteNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class RouteName
public const string Profile = "Profile";
public const string DisplayPackage = "package-route";
public const string DownloadPackage = "DownloadPackage";
public const string DownloadNuGetExe = "DownloadNuGetExe";
public const string Home = "Home";
public const string Policies = "Policies";
public const string ListPackages = "ListPackages";
Expand Down
8 changes: 7 additions & 1 deletion Website/Services/FileSystemService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;

namespace NuGetGallery
{
Expand Down Expand Up @@ -33,5 +34,10 @@ public Stream OpenWrite(string path)
{
return File.OpenWrite(path);
}

public DateTimeOffset GetCreationTimeUtc(string path)
{
return File.GetCreationTimeUtc(path);
}
}
}
4 changes: 3 additions & 1 deletion Website/Services/IFileSystemService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;

namespace NuGetGallery
{
Expand All @@ -10,5 +11,6 @@ public interface IFileSystemService
bool FileExists(string path);
Stream OpenRead(string path);
Stream OpenWrite(string path);
DateTimeOffset GetCreationTimeUtc(string path);
}
}
Loading

0 comments on commit f9e29c8

Please sign in to comment.