Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
Expand All @@ -21,5 +21,5 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
# - name: Test
# run: dotnet test --no-restore --verbosity normal
- name: Test
run: dotnet test --no-restore --verbosity normal
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Robust.Cdn/content.db*
Robust.Cdn/manifest.db*
*.user
testData/
/.vs/**
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.6" />
<PackageVersion Include="SharpZstd" Version="1.5.6" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions Robust.Cdn.Tests/ControllerTestCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.Testing;

namespace Robust.Cdn.Tests;

/// <summary>
/// Defines a test collection that uses <c>DatabaseFixture</c> for cleanup.
/// </summary>
[CollectionDefinition("ControllerTests")]
public sealed class ControllerTestCollection
: ICollectionFixture<WebApplicationFactory<Program>>, ICollectionFixture<DatabaseFixture>
{
}
57 changes: 57 additions & 0 deletions Robust.Cdn.Tests/Controllers/StatusControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;

namespace Robust.Cdn.Tests.Controllers;

/// <summary>
/// Integration tests for <c>StatusController</c>.
///
/// Endpoint: GET /control/status
/// </summary>
[Trait("Category", "Integration")]
public sealed class StatusControllerTests(WebApplicationFactory<Program> factory, DatabaseFixture database)
: TestBase(factory, database)
{
protected override string ForkName => "testfork";

protected override Dictionary<string, string?> GetConfigurationOverrides()
{
var baseDir = Database.CreateTestVersionOnDisk(ForkName, "1.0.0");
return new()
{
["Manifest:FileDiskPath"] = baseDir,
};
}

[Fact]
public async Task GetControlStatus_ReturnsOkWithVersionInfo()
{
var client = Factory.CreateClient();
var response = await client.GetAsync("/control/status");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("OK", content);
Assert.Contains("contentVersions", content);
Assert.Contains("version", content, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task GetControlStatus_WithExistingVersion_ReturnsNonZeroCount()
{
var client = Factory.CreateClient();

// The ingest job runs asynchronously at startup. Poll until we see version info.
await PollUntilCondition(() => client.GetAsync("/control/status"), async response =>
{
var body = await response.Content.ReadAsStringAsync();
var deserialized = JsonSerializer.Deserialize<Dictionary<string, object>>(body);
return deserialized != null
&& deserialized.TryGetValue("contentVersions", out var raw)
&& int.TryParse(raw?.ToString(), out var count)
&& count > 0;
});
}
}

56 changes: 56 additions & 0 deletions Robust.Cdn.Tests/DatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.IO.Compression;

namespace Robust.Cdn.Tests;

/// <summary>
/// Collection fixture that tracks all temp database files created during a test collection
/// and cleans them up when the collection is done.
/// </summary>
public sealed class DatabaseFixture : IDisposable
{
private readonly List<string> _tempFiles = new();
private readonly List<string> _tempDirs = new();

public string CreateTempDb()
{
var path = Path.GetTempFileName();
_tempFiles.Add(path);
return path;
}

/// <summary>
/// Creates a fork directory with a version directory and a client zip for the ingest job to find.
/// Returns the base path that should be used as FileDiskPath.
/// </summary>
public string CreateTestVersionOnDisk(string fork, string version, string clientZipName = "test")
{
var baseDir = Path.Combine(Path.GetTempPath(), "RobustCdnTests", Guid.NewGuid().ToString());
var versionDir = Path.Combine(baseDir, fork, version);
Directory.CreateDirectory(versionDir);
_tempDirs.Add(baseDir);

var zipPath = Path.Combine(versionDir, $"{clientZipName}.zip");
using (var zipStream = File.Create(zipPath))
using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create))
{
var entry = zip.CreateEntry("test.txt");
using var writer = new StreamWriter(entry.Open());
writer.Write("test content");
}

return baseDir;
}

public void Dispose()
{
foreach (var file in _tempFiles)
{
try { File.Delete(file); } catch { }
}

foreach (var dir in _tempDirs)
{
try { Directory.Delete(dir, recursive: true); } catch { }
}
}
}
12 changes: 12 additions & 0 deletions Robust.Cdn.Tests/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Robust.Cdn.Tests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:58678;http://localhost:58679"
}
}
}
26 changes: 26 additions & 0 deletions Robust.Cdn.Tests/Robust.Cdn.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Robust.Cdn\Robust.Cdn.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
91 changes: 91 additions & 0 deletions Robust.Cdn.Tests/TestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;

namespace Robust.Cdn.Tests;

/// <summary>
/// Base class for integration tests providing a pre-configured <c>WebApplicationFactory</c>
/// and automatic cleanup of temp database files after the test collection finishes.
/// </summary>
[Collection("ControllerTests")]
public abstract class TestBase : IClassFixture<DatabaseFixture>
{
protected WebApplicationFactory<Program> Factory { get; }
protected DatabaseFixture Database { get; }

protected abstract string ForkName { get; }

protected TestBase(WebApplicationFactory<Program> factory, DatabaseFixture database)
{
Database = database;

var config = new Dictionary<string, string?>(GetDefaultConfiguration());
foreach (var (key, value) in GetConfigurationOverrides())
{
config[key] = value;
}

Factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(config);
});
});
}

/// <summary>
/// Provides default configuration for all tests.
/// Override to change defaults globally.
/// </summary>
private Dictionary<string, string?> GetDefaultConfiguration()
{
return new()
{
["Cdn:DatabaseFileName"] = Database.CreateTempDb(),
["Manifest:DatabaseFileName"] = Database.CreateTempDb(),
["Manifest:FileDiskPath"] = Path.GetTempPath(),
[$"Manifest:Forks:{ForkName}:ClientZipName"] = "test",
[$"Manifest:Forks:{ForkName}:BuildsPageLinkText"] = "test",
};
}

/// <summary>
/// Override to add or replace specific configuration keys for a particular test class.
/// Returned values take precedence over <c>GetDefaultConfiguration()</c>.
/// Please do not use <see cref="Factory"/> there, as this method is used
/// in c-tor or base class and factory won't be set up yet (config is required for factory).
/// </summary>
protected virtual Dictionary<string, string?> GetConfigurationOverrides() => new();

/// <summary>
/// Polls a request until it returns OK or the timeout is reached.
/// Useful for waiting on async jobs triggered during startup (e.g. version ingestion).
/// </summary>
protected static async Task<HttpResponseMessage> PollUntilCondition(
Func<Task<HttpResponseMessage>> factory,
Func<HttpResponseMessage, Task<bool>> condition,
int maxAttempts = 20,
int delayMs = 200)
{
for (var i = 0; i < maxAttempts; i++)
{
var response = await factory();
if (await condition(response))
return response;

await Task.Delay(delayMs);
}

throw new TimeoutException($"Request did not return OK after {maxAttempts * delayMs}ms");
}

/// <summary>
/// Polls a GET request until it returns OK.
/// </summary>
protected static Task<HttpResponseMessage> PollUntilOk(Func<Task<HttpResponseMessage>> factory, int maxAttempts = 20, int delayMs = 200)
{
return PollUntilCondition(factory, response => Task.FromResult(response.StatusCode == HttpStatusCode.OK), maxAttempts, delayMs);
}
}

41 changes: 0 additions & 41 deletions Robust.Cdn.sln

This file was deleted.

17 changes: 17 additions & 0 deletions Robust.Cdn.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Solution>
<Folder Name="/Solution Files/">
<File Path=".dockerignore" />
<File Path=".editorconfig" />
<File Path=".gitattributes" />
<File Path=".gitignore" />
<File Path="Directory.Packages.props" />
<File Path="Dockerfile" />
<File Path="LICENSE.txt" />
<File Path="README.md" />
<File Path="RELEASE-NOTES.md" />
</Folder>
<Project Path="Robust.Cdn.Downloader/Robust.Cdn.Downloader.csproj" />
<Project Path="Robust.Cdn.Lib/Robust.Cdn.Lib.csproj" />
<Project Path="Robust.Cdn.Tests/Robust.Cdn.Tests.csproj" />
<Project Path="Robust.Cdn/Robust.Cdn.csproj" />
</Solution>
Loading
Loading