113 lines
6.4 KiB
C#
113 lines
6.4 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using MoShiro.Backend.Application;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
public sealed class BackendUpdateTests : IAsyncLifetime
|
|
{
|
|
private const string Digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
private readonly BackendFixture _backend = new();
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
await _backend.InitializeAsync();
|
|
_backend.Services.GetRequiredService<BackendOptions>().UpdateChannel = "dev";
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => _backend.DisposeAsync();
|
|
|
|
[Theory]
|
|
[InlineData("moshiro-dev", "dev")]
|
|
[InlineData("registry.example/team/moshiro-dev", "dev")]
|
|
[InlineData("moshiro", "release")]
|
|
[InlineData("registry.example/team/moshiro", "release")]
|
|
public void AutoChannel_FollowsTheConfiguredImageRepository(string imageNamespace, string expected)
|
|
{
|
|
Assert.Equal(expected, BackendUpdates.ChannelOf(new BackendOptions { UpdateChannel = "auto", ImageNamespace = imageNamespace }));
|
|
}
|
|
|
|
private async Task<HttpResponseMessage> PublishAsync(long seq, string channel = "dev")
|
|
{
|
|
string manifest = $$"""{"component":"backend","channel":"{{channel}}","image":"registry.example/moshiro-{{channel}}/moshiro-backend","digest":"{{Digest}}","tag":"{{channel}}-{{seq}}","version":"0.1.1","seq":{{seq}}}""";
|
|
return await _backend.Admin.PostAsync("/api/admin/releases", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = "MEUC" }));
|
|
}
|
|
|
|
private Task<JsonNode> ViewAsync() => _backend.GetJsonAsync(_backend.Admin, "/api/admin/backend/update");
|
|
|
|
private async Task VerifiedAsync(long seq)
|
|
{
|
|
string directory = Path.Combine(_backend.DataDirectory, "update-status");
|
|
Directory.CreateDirectory(directory);
|
|
var status = new UpdateStatus
|
|
{
|
|
State = "available",
|
|
Current = "sha256:" + new string('b', 64),
|
|
Available = new UpdateAvailable { Seq = seq, Digest = Digest, Tag = "dev-" + seq, Version = "0.1.1" },
|
|
};
|
|
await File.WriteAllTextAsync(Path.Combine(directory, "status.json"), JsonSerializer.Serialize(status, MoShiroJsonContext.Default.UpdateStatus));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Webhook_PreservesChannels_AndNeverConfirmsAnUpdateByItself()
|
|
{
|
|
using HttpResponseMessage dev = await PublishAsync(1);
|
|
using HttpResponseMessage release = await PublishAsync(1, "release");
|
|
Assert.Equal(HttpStatusCode.Created, dev.StatusCode);
|
|
Assert.Equal(HttpStatusCode.Created, release.StatusCode);
|
|
JsonNode view = await ViewAsync();
|
|
Assert.Equal("dev", view["channel"]!.GetValue<string>());
|
|
Assert.Equal("dev-1", view["available"]!["tag"]!.GetValue<string>());
|
|
string requestDirectory = Path.Combine(_backend.DataDirectory, "update");
|
|
Assert.False(File.Exists(Path.Combine(requestDirectory, "apply.request")));
|
|
Assert.Contains("\"channel\":\"dev\"", await File.ReadAllTextAsync(Path.Combine(requestDirectory, "offer.manifest")), StringComparison.Ordinal);
|
|
ReleaseOffer? stable = await _backend.Services.GetRequiredService<ReleaseService>().GetBackendAsync("release", default);
|
|
Assert.Equal("release-1", stable!.Tag);
|
|
using HttpResponseMessage replay = await PublishAsync(1);
|
|
Assert.Equal(HttpStatusCode.Conflict, replay.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Confirm_RequiresHostVerification_AndRejectsASupersededSequence()
|
|
{
|
|
using HttpResponseMessage first = await PublishAsync(10);
|
|
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
|
|
using HttpResponseMessage unverified = await _backend.Admin.PostAsync("/api/admin/backend/update", BackendFixture.Json(new JsonObject { ["seq"] = 10 }));
|
|
Assert.Equal(HttpStatusCode.PreconditionFailed, unverified.StatusCode);
|
|
await VerifiedAsync(10);
|
|
using HttpResponseMessage accepted = await _backend.Admin.PostAsync("/api/admin/backend/update", BackendFixture.Json(new JsonObject { ["seq"] = 10 }));
|
|
Assert.Equal(HttpStatusCode.NoContent, accepted.StatusCode);
|
|
string path = Path.Combine(_backend.DataDirectory, "update", "apply.request");
|
|
Assert.Equal("10", await File.ReadAllTextAsync(path));
|
|
Assert.Equal("requested", (await ViewAsync())["state"]!.GetValue<string>());
|
|
using HttpResponseMessage second = await PublishAsync(11);
|
|
Assert.Equal(HttpStatusCode.Created, second.StatusCode);
|
|
using HttpResponseMessage stale = await _backend.Admin.PostAsync("/api/admin/backend/update", BackendFixture.Json(new JsonObject { ["seq"] = 10 }));
|
|
Assert.Equal(HttpStatusCode.PreconditionFailed, stale.StatusCode);
|
|
Assert.Equal("10", await File.ReadAllTextAsync(path)); // 新 webhook 不会把确认请求悄悄改成 11
|
|
await VerifiedAsync(11);
|
|
using HttpResponseMessage newConfirmation = await _backend.Admin.PostAsync("/api/admin/backend/update", BackendFixture.Json(new JsonObject { ["seq"] = 11 }));
|
|
Assert.Equal(HttpStatusCode.NoContent, newConfirmation.StatusCode);
|
|
Assert.Equal("11", await File.ReadAllTextAsync(path));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Live_PushesCurrentChannel_AndAnonymousCannotReadOrConfirm()
|
|
{
|
|
using HttpResponseMessage denied = await _backend.Anonymous.GetAsync("/api/admin/backend/update");
|
|
Assert.Equal(HttpStatusCode.Unauthorized, denied.StatusCode);
|
|
using HttpResponseMessage cannotConfirm = await _backend.Anonymous.PostAsync("/api/admin/backend/update", BackendFixture.Json(new JsonObject { ["seq"] = 1 }));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, cannotConfirm.StatusCode);
|
|
await using SseClient live = await SseClient.ConnectAsync(_backend.Admin, "/api/admin/backend/update/live");
|
|
await live.WaitForAsync(e => e.Name == "backend_update");
|
|
using HttpResponseMessage release = await PublishAsync(20);
|
|
Assert.Equal(HttpStatusCode.Created, release.StatusCode);
|
|
SseEvent updated = await live.WaitForAsync(e => e.Name == "backend_update" && e.Data?["available"]?["seq"]?.GetValue<long>() == 20);
|
|
Assert.Equal("dev", updated.Data!["channel"]!.GetValue<string>());
|
|
Assert.False(File.Exists(Path.Combine(_backend.DataDirectory, "update", "apply.request")));
|
|
}
|
|
}
|