diff --git a/test/data/nuget-artifacts/dummy.nuget.1.0.0.nupkg b/test/data/nuget-artifacts/dummy.nuget.1.0.0.nupkg new file mode 100644 index 0000000..070a94b Binary files /dev/null and b/test/data/nuget-artifacts/dummy.nuget.1.0.0.nupkg differ diff --git a/test/data/nuget-artifacts/dummy.nuget.1.0.0/dummy.nuget.nuspec b/test/data/nuget-artifacts/dummy.nuget.1.0.0/dummy.nuget.nuspec new file mode 100644 index 0000000..feb3a4a --- /dev/null +++ b/test/data/nuget-artifacts/dummy.nuget.1.0.0/dummy.nuget.nuspec @@ -0,0 +1,9 @@ + + + + dummy.nuget + 1.0.0 + isnd.tests + Dummy test package used by integration tests. + + diff --git a/test/isnd.tests/UnitTestWebHost.cs b/test/isnd.tests/UnitTestWebHost.cs index be248f2..c71b5ea 100644 --- a/test/isnd.tests/UnitTestWebHost.cs +++ b/test/isnd.tests/UnitTestWebHost.cs @@ -1,27 +1,39 @@ -using System.Threading; using System; +using System.IO; +using System.IO.Compression; +using System.Linq; using System.Net.Http; -using Xunit; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Isn.Abstract; using isnd.Data; +using isnd.Data.ApiKeys; +using isnd.Helpers; +using isnd.tests; +using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using isnd.tests; -using NuGet.Protocol; +using NuGet.Common; using NuGet.Configuration; -using System.Threading.Tasks; +using NuGet.Protocol; using NuGet.Protocol.Core.Types; -using Isn.Abstract; -using isnd.Helpers; -using Microsoft.AspNetCore.DataProtection; -using System.Security.Cryptography; +using Xunit; namespace isnd.host.tests -{ +{ [Collection("Web server collection")] public class UnitTestWebHost : IClassFixture { - WebServerFixture server; + private const string DummyPackageId = "dummy.nuget"; + private const string DummyPackageVersion = "1.0.0"; + private const string DummyArtifactRelativePath = "test/data/nuget-artifacts/dummy.nuget.1.0.0.nupkg"; + + private readonly WebServerFixture server; + public UnitTestWebHost(WebServerFixture server) { this.server = server; @@ -30,18 +42,16 @@ namespace isnd.host.tests [Fact] public void TestHaveTestDbContextAndMigrate() { - using (var serviceScope = server.Host.Services.CreateScope()) + using var serviceScope = server.Host.Services.CreateScope(); + var services = serviceScope.ServiceProvider; + var myDependency = services.GetRequiredService(); + if (myDependency.Database.ProviderName?.Contains("InMemory", StringComparison.OrdinalIgnoreCase) == true) { - var services = serviceScope.ServiceProvider; - var myDependency = services.GetRequiredService(); - if (myDependency.Database.ProviderName?.Contains("InMemory", StringComparison.OrdinalIgnoreCase) == true) - { - myDependency.Database.EnsureCreated(); - } - else - { - myDependency.Database.Migrate(); - } + myDependency.Database.EnsureCreated(); + } + else + { + myDependency.Database.Migrate(); } } @@ -67,67 +77,121 @@ namespace isnd.host.tests } [Fact] - void TestDropUser() + public void TestDropUser() { - - using (var serviceScope = server.Host.Services.CreateScope()) + using var serviceScope = server.Host.Services.CreateScope(); + var services = serviceScope.ServiceProvider; + var dbContext = services.GetRequiredService(); + var paul = dbContext.Users.FirstOrDefaultAsync(u => u.Email == "paul@pschneider.fr").Result; + if (paul != null) { - var services = serviceScope.ServiceProvider; - var dbContext = services.GetRequiredService(); - var paul = dbContext.Users.FirstOrDefaultAsync(u => u.Email == "paul@pschneider.fr").Result; - if (paul!=null) - { - dbContext.Users.Remove(paul); - dbContext.SaveChanges(); - } + dbContext.Users.Remove(paul); + dbContext.SaveChanges(); } } - [Fact] public void NugetInstallsTest() { + var pkgSourceUrl = GetServerBaseUrl() + "/" + Constants.APIPrefix + "index.json"; + using var client = new HttpClient(); + var response = client.GetAsync(pkgSourceUrl).GetAwaiter().GetResult(); + var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + Assert.True(response.IsSuccessStatusCode, $"Expected {pkgSourceUrl} to be reachable but got {(int)response.StatusCode} {response.ReasonPhrase}"); + Assert.False(string.IsNullOrWhiteSpace(body)); + Assert.Contains("PackagePublish/2.0.0", body, StringComparison.OrdinalIgnoreCase); + Assert.Contains("RegistrationsBaseUrl/Versioned", body, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void DummyNugetArtifactShouldBeAReadablePackage() + { + var packagePath = GetDummyArtifactPath(); + Assert.True(File.Exists(packagePath), $"Missing dummy artifact at {packagePath}"); + + var fileInfo = new FileInfo(packagePath); + Assert.True(fileInfo.Length > 0, "Dummy artifact is empty."); + + using var stream = File.OpenRead(packagePath); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false); + var nuspecEntry = archive.GetEntry("dummy.nuget.nuspec"); + Assert.NotNull(nuspecEntry); + + using var nuspecStream = nuspecEntry.Open(); + var nuspec = XDocument.Load(nuspecStream); + XNamespace ns = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"; + var id = nuspec.Root?.Element(ns + "metadata")?.Element(ns + "id")?.Value; + var version = nuspec.Root?.Element(ns + "metadata")?.Element(ns + "version")?.Value; + + Assert.Equal(DummyPackageId, id); + Assert.Equal(DummyPackageVersion, version); + } + + [Fact] + public async Task PushDummyPackageAndDiscoverItThroughNuGetEndpoints() + { + var apiKeyValue = $"dummy-key-{Guid.NewGuid():N}"; + var packagePath = GetDummyArtifactPath(); + Assert.True(File.Exists(packagePath), $"Missing dummy artifact at {packagePath}"); + using (var serviceScope = server.Host.Services.CreateScope()) { - var isnSettings = serviceScope.ServiceProvider.GetService>().Value; - string pkgSourceUrl = isnSettings.ExternalUrl + "/" + Constants.APIPrefix + "index.json"; - using var client = new HttpClient(); - var response = client.GetAsync(pkgSourceUrl).GetAwaiter().GetResult(); - var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var dbContext = serviceScope.ServiceProvider.GetRequiredService(); + var userId = $"dummy-user-{Guid.NewGuid():N}"; - Assert.True(response.IsSuccessStatusCode, $"Expected {pkgSourceUrl} to be reachable but got {(int)response.StatusCode} {response.ReasonPhrase}"); - Assert.False(string.IsNullOrWhiteSpace(body)); + await ResetDummyPackageStateAsync(serviceScope.ServiceProvider, dbContext); + + dbContext.Users.Add(new ApplicationUser + { + Id = userId, + UserName = userId, + NormalizedUserName = userId.ToUpperInvariant(), + Email = $"{userId}@tests.local", + NormalizedEmail = $"{userId}@tests.local".ToUpperInvariant(), + EmailConfirmed = true + }); + + dbContext.ApiKeys.Add(new ApiKey + { + Id = apiKeyValue, + UserId = userId, + Name = "dummy-key", + CreationDate = DateTime.UtcNow, + ValidityPeriodInDays = 30 + }); + + await dbContext.SaveChangesAsync(); } + + await PushPackageAsync(packagePath, apiKeyValue); + await AssertRegistrationContainsPushedVersionAsync(DummyPackageId, DummyPackageVersion); + await AssertAutocompleteContainsPushedVersionAsync(DummyPackageId, DummyPackageVersion); } [Fact] public void TestRegistrationV3Resource() { - using (var serviceScope = server.Host.Services.CreateScope()) - { var isnSettings = serviceScope.ServiceProvider.GetService>().Value; + using var serviceScope = server.Host.Services.CreateScope(); + var isnSettings = serviceScope.ServiceProvider.GetService>().Value; string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json"; - NullThrottle throttle = new NullThrottle(); + var throttle = new NullThrottle(); - PackageSource packageSource = new PackageSource(pkgSourceUrl); - HttpSource client = new HttpSource(packageSource, PkgSourceMessageHandler, throttle); - NuGet.Protocol.RegistrationResourceV3 res = new NuGet.Protocol.RegistrationResourceV3(client , - new Uri(isnSettings.ExternalUrl + "/v3.4.0//registration")); - } + var packageSource = new PackageSource(pkgSourceUrl); + var client = new HttpSource(packageSource, PkgSourceMessageHandler, throttle); + _ = new RegistrationResourceV3(client, new Uri(isnSettings.ExternalUrl + "/v3.4.0//registration")); } - + [Fact] public void TrueTestRegistrationV3Resource() { - using (var serviceScope = server.Host.Services.CreateScope()) - { - var isnSettings = serviceScope.ServiceProvider.GetService>().Value; + using var serviceScope = server.Host.Services.CreateScope(); + var isnSettings = serviceScope.ServiceProvider.GetService>().Value; string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json"; var prov = new RegistrationResourceV3Provider(); var source = new PackageSource(pkgSourceUrl); - var repo = new SourceRepository(source, new INuGetResourceProvider[]{ prov }); + var repo = new SourceRepository(source, new INuGetResourceProvider[] { prov }); prov.TryCreate(repo, CancellationToken.None); - - } } private Task PkgSourceMessageHandler() @@ -135,6 +199,103 @@ namespace isnd.host.tests throw new NotImplementedException(); } + private string GetServerBaseUrl() + { + var address = server.Addresses.FirstOrDefault(); + Assert.False(string.IsNullOrWhiteSpace(address), "No listening address was captured from WebServerFixture."); + return address.TrimEnd('/'); + } + + private string GetDummyArtifactPath() + { + var root = FindRepoRoot(); + return Path.Combine(root, DummyArtifactRelativePath); + } + + private static string FindRepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + if (File.Exists(Path.Combine(dir.FullName, "isn.sln"))) + { + return dir.FullName; + } + + dir = dir.Parent; + } + + throw new DirectoryNotFoundException("Could not locate repository root from AppContext.BaseDirectory."); + } + + private static async Task ResetDummyPackageStateAsync(IServiceProvider serviceProvider, ApplicationDbContext dbContext) + { + var settings = serviceProvider.GetRequiredService>().Value; + var versions = dbContext.PackageVersions.Where(v => v.PackageId == DummyPackageId); + var packages = dbContext.Packages.Where(p => p.Id == DummyPackageId); + + dbContext.PackageVersions.RemoveRange(versions); + dbContext.Packages.RemoveRange(packages); + await dbContext.SaveChangesAsync(); + + var packageRootDir = settings.PackagesRootDir; + if (!Path.IsPathRooted(packageRootDir)) + { + packageRootDir = Path.Combine(FindRepoRoot(), packageRootDir); + } + + var packageOnDiskPath = Path.Combine(packageRootDir, DummyPackageId); + if (Directory.Exists(packageOnDiskPath)) + { + Directory.Delete(packageOnDiskPath, recursive: true); + } + } + + private async Task PushPackageAsync(string packagePath, string apiKey) + { + using var client = new HttpClient { BaseAddress = new Uri(GetServerBaseUrl()) }; + using var request = new HttpRequestMessage(HttpMethod.Put, $"/{Constants.APIPrefix}{isnd.Entities.ApiConfig.Publish}"); + using var multipart = new MultipartFormDataContent(); + await using var packageStream = File.OpenRead(packagePath); + using var packageContent = new StreamContent(packageStream); + + packageContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + multipart.Add(packageContent, "package", Path.GetFileName(packagePath)); + request.Content = multipart; + request.Headers.Add("X-NuGet-ApiKey", apiKey); + request.Headers.Add("X-NuGet-Client-Version", "6.11.1"); + + var response = await client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode, + $"Expected push to succeed but got {(int)response.StatusCode} {response.ReasonPhrase}. Body: {responseBody}"); + } + + private async Task AssertRegistrationContainsPushedVersionAsync(string packageId, string packageVersion) + { + using var client = new HttpClient { BaseAddress = new Uri(GetServerBaseUrl()) }; + var lowerId = packageId.ToLowerInvariant(); + var route = $"/{Constants.APIPrefix}v3.4.0/{isnd.Entities.ApiConfig.Registration}/{lowerId}/index.json"; + var response = await client.GetAsync(route); + var body = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, + $"Expected registration endpoint to succeed but got {(int)response.StatusCode} {response.ReasonPhrase}. Body: {body}"); + Assert.Contains(packageVersion, body, StringComparison.OrdinalIgnoreCase); + } + + private async Task AssertAutocompleteContainsPushedVersionAsync(string packageId, string packageVersion) + { + using var client = new HttpClient { BaseAddress = new Uri(GetServerBaseUrl()) }; + var route = $"/{Constants.APIPrefix}{isnd.Entities.ApiConfig.AutoComplete}?id={packageId}&semVerLevel=3.0.0&prerelease=true"; + var response = await client.GetAsync(route); + var body = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, + $"Expected autocomplete endpoint to succeed but got {(int)response.StatusCode} {response.ReasonPhrase}. Body: {body}"); + Assert.Contains(packageVersion, body, StringComparison.OrdinalIgnoreCase); + } + private sealed class ThrowingProtector : IDataProtector { public byte[] Protect(byte[] plaintext) => throw new CryptographicException("unexpected protect call");