feat(admin): dispatch allowlisted maintenance jobs
CI and Deploy / test (push) Successful in 7m8s
CI and Deploy / deploy (push) Successful in 1m42s

This commit is contained in:
cesnimda
2026-09-01 16:53:54 +02:00
parent 3ec9ac1dad
commit 3b055b01f5
11 changed files with 328 additions and 8 deletions
@@ -0,0 +1,71 @@
using System.Net;
using System.Text.Json;
using JobTrackerApi.Services;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class MaintenanceJobDispatcherTests
{
[Fact]
public void Job_is_unavailable_without_a_dispatch_token()
{
var dispatcher = new MaintenanceJobDispatcher(new ConfigurationBuilder().Build(), Mock.Of<IHttpClientFactory>());
var job = Assert.Single(dispatcher.GetJobs());
Assert.Equal(MaintenanceJobDispatcher.CvBenchmarkJobId, job.Id);
Assert.False(job.Available);
Assert.Null(job.RunsUrl);
}
[Fact]
public async Task Dispatch_sends_only_the_registered_workflow_and_job_id()
{
HttpRequestMessage? captured = null;
string? body = null;
var handler = new StubHandler(async request =>
{
captured = request;
body = request.Content is null ? null : await request.Content.ReadAsStringAsync();
return new HttpResponseMessage(HttpStatusCode.NoContent);
});
var factory = new Mock<IHttpClientFactory>();
factory.Setup(x => x.CreateClient("maintenance-jobs")).Returns(new HttpClient(handler));
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["MaintenanceJobs:GiteaBaseUrl"] = "https://git.example.test/",
["MaintenanceJobs:GiteaOwner"] = "owner",
["MaintenanceJobs:GiteaRepository"] = "repo",
["MaintenanceJobs:GiteaToken"] = "test-token",
}).Build();
var dispatcher = new MaintenanceJobDispatcher(config, factory.Object);
await dispatcher.DispatchAsync(MaintenanceJobDispatcher.CvBenchmarkJobId, CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(HttpMethod.Post, captured!.Method);
Assert.Equal("https://git.example.test/api/v1/repos/owner/repo/actions/workflows/maintenance.yml/dispatches", captured.RequestUri!.ToString());
Assert.Equal("token", captured.Headers.Authorization!.Scheme);
using var json = JsonDocument.Parse(body!);
Assert.Equal("main", json.RootElement.GetProperty("ref").GetString());
Assert.Equal("cv-benchmark", json.RootElement.GetProperty("inputs").GetProperty("job").GetString());
}
[Fact]
public async Task Dispatch_rejects_any_unregistered_job_without_an_http_call()
{
var factory = new Mock<IHttpClientFactory>();
var dispatcher = new MaintenanceJobDispatcher(new ConfigurationBuilder().Build(), factory.Object);
await Assert.ThrowsAsync<KeyNotFoundException>(() => dispatcher.DispatchAsync("shell", CancellationToken.None));
factory.Verify(x => x.CreateClient(It.IsAny<string>()), Times.Never);
}
private sealed class StubHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> send) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => send(request);
}
}