isn/test/isnd.tests/UnitTestWebHost.cs

307 lines
13 KiB
C#
Raw Normal View History

2021-05-09 13:45:55 +01:00
using System;
2026-07-05 23:20:25 +01:00
using System.IO;
using System.IO.Compression;
using System.Linq;
2026-07-05 18:16:52 +01:00
using System.Net.Http;
2026-07-05 23:20:25 +01:00
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Isn.Abstract;
2021-08-15 19:09:01 +01:00
using isnd.Data;
2026-07-05 23:20:25 +01:00
using isnd.Data.ApiKeys;
using isnd.Helpers;
using isnd.tests;
using Microsoft.AspNetCore.DataProtection;
2021-05-11 20:52:39 +01:00
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
2021-09-08 01:55:00 +01:00
using Microsoft.Extensions.Options;
2023-02-06 21:43:18 +00:00
using NuGet.Configuration;
2026-07-05 23:20:25 +01:00
using NuGet.Protocol;
2023-02-06 21:43:18 +00:00
using NuGet.Protocol.Core.Types;
2026-07-05 23:20:25 +01:00
using Xunit;
2021-09-08 01:55:00 +01:00
2021-08-13 18:55:25 +01:00
namespace isnd.host.tests
2026-07-05 23:20:25 +01:00
{
2023-01-29 13:04:50 +00:00
[Collection("Web server collection")]
public class UnitTestWebHost : IClassFixture<WebServerFixture>
2021-05-09 13:45:55 +01:00
{
2026-07-05 23:20:25 +01:00
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;
2023-01-29 13:04:50 +00:00
public UnitTestWebHost(WebServerFixture server)
{
this.server = server;
}
[Fact]
2021-07-05 21:24:37 +01:00
public void TestHaveTestDbContextAndMigrate()
2021-05-09 13:45:55 +01:00
{
2026-07-05 23:20:25 +01:00
using var serviceScope = server.Host.Services.CreateScope();
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<ApplicationDbContext>();
if (myDependency.Database.ProviderName?.Contains("InMemory", StringComparison.OrdinalIgnoreCase) == true)
2023-01-29 13:04:50 +00:00
{
2026-07-05 23:20:25 +01:00
myDependency.Database.EnsureCreated();
}
else
{
myDependency.Database.Migrate();
2023-01-29 13:04:50 +00:00
}
}
2021-05-11 20:52:39 +01:00
2026-07-05 21:18:37 +01:00
[Fact]
public void LegacyApiKeyProtectorCanUnprotectValuesFromOlderClients()
{
const string clearValue = "legacy-api-key";
var legacyProtectedValue = new Isn.DefaultDataProtector().Protect(clearValue);
2026-07-05 21:44:49 +01:00
var unprotected = ApiKeyProtector.TryUnprotectKey(new ThrowingProtector(), legacyProtectedValue);
2026-07-05 21:18:37 +01:00
Assert.Equal(clearValue, unprotected);
}
2026-07-05 21:44:49 +01:00
[Fact]
public void ApiKeyProtectorReturnsRawValueWhenInputIsNotProtected()
{
const string rawApiKey = "CfDJ8AstXUEkxpJBrmoENwy__WNPxqcOwAuxyYgiU3Mebns5yxAT3VrC5vTuWTRSGZBFB7IGUyFkUYsp2nhRy64NqeUzLgOwEcU4FPOf-yaAtPeTMeStRd60bRh2ZYyQkQ3hrhW-lwpCLwYtNOnOyX2jayuzByF-4QfHIEhg6lbWenzf";
var result = ApiKeyProtector.TryUnprotectKey(new ThrowingProtector(), rawApiKey);
Assert.Equal(rawApiKey, result);
}
2023-01-29 13:04:50 +00:00
[Fact]
2026-07-05 23:20:25 +01:00
public void TestDropUser()
2023-01-29 13:04:50 +00:00
{
2026-07-05 23:20:25 +01:00
using var serviceScope = server.Host.Services.CreateScope();
var services = serviceScope.ServiceProvider;
var dbContext = services.GetRequiredService<ApplicationDbContext>();
var paul = dbContext.Users.FirstOrDefaultAsync(u => u.Email == "paul@pschneider.fr").Result;
if (paul != null)
2021-05-11 20:52:39 +01:00
{
2026-07-05 23:20:25 +01:00
dbContext.Users.Remove(paul);
dbContext.SaveChanges();
2021-05-11 20:52:39 +01:00
}
2021-05-09 13:45:55 +01:00
}
2022-06-16 17:03:38 +01:00
2022-05-25 09:07:36 +01:00
[Fact]
2023-01-29 13:04:50 +00:00
public void NugetInstallsTest()
2022-04-09 19:53:26 +01:00
{
2026-07-05 23:20:25 +01:00
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}");
2023-02-06 21:43:18 +00:00
using (var serviceScope = server.Host.Services.CreateScope())
2026-07-05 18:16:52 +01:00
{
2026-07-05 23:20:25 +01:00
var dbContext = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
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();
2023-02-06 21:43:18 +00:00
}
2026-07-05 23:20:25 +01:00
await PushPackageAsync(packagePath, apiKeyValue);
await AssertRegistrationContainsPushedVersionAsync(DummyPackageId, DummyPackageVersion);
await AssertAutocompleteContainsPushedVersionAsync(DummyPackageId, DummyPackageVersion);
2023-02-06 21:43:18 +00:00
}
[Fact]
public void TestRegistrationV3Resource()
{
2026-07-05 23:20:25 +01:00
using var serviceScope = server.Host.Services.CreateScope();
var isnSettings = serviceScope.ServiceProvider.GetService<IOptions<isnd.Entities.IsndSettings>>().Value;
2026-07-05 18:16:52 +01:00
string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json";
2026-07-05 23:20:25 +01:00
var throttle = new NullThrottle();
2023-02-06 21:43:18 +00:00
2026-07-05 23:20:25 +01:00
var packageSource = new PackageSource(pkgSourceUrl);
var client = new HttpSource(packageSource, PkgSourceMessageHandler, throttle);
_ = new RegistrationResourceV3(client, new Uri(isnSettings.ExternalUrl + "/v3.4.0//registration"));
2023-02-06 21:43:18 +00:00
}
2026-07-05 23:20:25 +01:00
2023-03-14 00:15:42 +00:00
[Fact]
public void TrueTestRegistrationV3Resource()
{
2026-07-05 23:20:25 +01:00
using var serviceScope = server.Host.Services.CreateScope();
var isnSettings = serviceScope.ServiceProvider.GetService<IOptions<isnd.Entities.IsndSettings>>().Value;
2026-07-05 18:16:52 +01:00
string pkgSourceUrl = isnSettings.ExternalUrl + "/pkgs/index.json";
2023-03-14 00:15:42 +00:00
var prov = new RegistrationResourceV3Provider();
var source = new PackageSource(pkgSourceUrl);
2026-07-05 23:20:25 +01:00
var repo = new SourceRepository(source, new INuGetResourceProvider[] { prov });
2023-03-14 00:15:42 +00:00
prov.TryCreate(repo, CancellationToken.None);
}
2023-02-06 21:43:18 +00:00
private Task<HttpHandlerResource> PkgSourceMessageHandler()
{
throw new NotImplementedException();
2022-04-09 19:53:26 +01:00
}
2026-07-05 21:18:37 +01:00
2026-07-05 23:20:25 +01:00
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<IOptions<isnd.Entities.IsndSettings>>().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);
}
2026-07-05 21:18:37 +01:00
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;
}
2021-05-09 13:45:55 +01:00
}
}