using System; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; 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 NuGet.Configuration; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using Xunit; namespace isnd.host.tests { [Collection("Web server collection")] public class UnitTestWebHost : IClassFixture { 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; } [Fact] public void TestHaveTestDbContextAndMigrate() { using var serviceScope = server.Host.Services.CreateScope(); var services = serviceScope.ServiceProvider; var myDependency = services.GetRequiredService(); if (myDependency.Database.ProviderName?.Contains("InMemory", StringComparison.OrdinalIgnoreCase) == true) { myDependency.Database.EnsureCreated(); } else { myDependency.Database.Migrate(); } } [Fact] public void LegacyApiKeyProtectorCanUnprotectValuesFromOlderClients() { const string clearValue = "legacy-api-key"; var legacyProtectedValue = new Isn.DefaultDataProtector().Protect(clearValue); var unprotected = ApiKeyProtector.TryUnprotectKey(new ThrowingProtector(), legacyProtectedValue); Assert.Equal(clearValue, unprotected); } [Fact] public void ApiKeyProtectorReturnsRawValueWhenInputIsNotProtected() { const string rawApiKey = "CfDJ8AstXUEkxpJBrmoENwy__WNPxqcOwAuxyYgiU3Mebns5yxAT3VrC5vTuWTRSGZBFB7IGUyFkUYsp2nhRy64NqeUzLgOwEcU4FPOf-yaAtPeTMeStRd60bRh2ZYyQkQ3hrhW-lwpCLwYtNOnOyX2jayuzByF-4QfHIEhg6lbWenzf"; var result = ApiKeyProtector.TryUnprotectKey(new ThrowingProtector(), rawApiKey); Assert.Equal(rawApiKey, result); } [Fact] public void TestDropUser() { 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) { 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 dbContext = serviceScope.ServiceProvider.GetRequiredService(); var userId = $"dummy-user-{Guid.NewGuid():N}"; 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; string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json"; var throttle = new NullThrottle(); 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; string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json"; var prov = new RegistrationResourceV3Provider(); var source = new PackageSource(pkgSourceUrl); var repo = new SourceRepository(source, new INuGetResourceProvider[] { prov }); prov.TryCreate(repo, CancellationToken.None); } private Task PkgSourceMessageHandler() { 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"); public byte[] Unprotect(byte[] protectedData) => throw new CryptographicException("unexpected unprotect call"); public IDataProtector CreateProtector(string purpose) => this; } } }