Files
MoShiro/backend/tests/MoShiro.Backend.Tests/InstallTests.cs
T

122 lines
7.0 KiB
C#

using System.Net;
using System.Text.Json.Nodes;
namespace MoShiro.Backend.Tests;
/// <summary>一行安装:Agent 和 Probe 共用同一份脚本,角色、网关、镜像由一次性代码决定,节点名字默认取主机名。</summary>
[Collection("probe-e2e")]
public sealed class InstallTests
{
private readonly BackendFixture _backend;
public InstallTests(BackendFixture backend)
{
_backend = backend;
}
private async Task<JsonNode> CreateAsync(string role)
{
using HttpResponseMessage response = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = role }));
return JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
}
[Theory]
[InlineData("agent")]
[InlineData("probe")]
public async Task Commands_AreOneLiners_WithoutAPlaceholder_AndTheScriptIsFilledInForTheRole(string role)
{
JsonNode created = await CreateAsync(role);
string code = created["plaintext"]!.GetValue<string>();
JsonNode commands = created["install_commands"]!;
string linux = commands["linux"]!.GetValue<string>();
string macos = commands["macos"]!.GetValue<string>();
string windows = commands["windows"]!.GetValue<string>();
Assert.StartsWith("curl -fsSL '", linux, StringComparison.Ordinal);
Assert.EndsWith("/install?code=" + code + "' | bash", linux, StringComparison.Ordinal);
Assert.EndsWith("/install?code=" + code + "' | zsh", macos, StringComparison.Ordinal);
Assert.StartsWith("irm '", windows, StringComparison.Ordinal);
Assert.EndsWith("/install.ps1?code=" + code + "' | iex", windows, StringComparison.Ordinal);
Assert.DoesNotContain("<", linux + macos + windows, StringComparison.Ordinal); // 没有要用户自己替换的占位符
// 脚本不需要登录(拿代码就是授权),不带缓存
using HttpResponseMessage sh = await _backend.Anonymous.GetAsync("/install?code=" + code);
string script = await sh.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, sh.StatusCode);
Assert.Equal("no-store", sh.Headers.CacheControl?.ToString());
Assert.Contains($"ROLE='{role}'", script, StringComparison.Ordinal);
Assert.Contains($"CODE='{code}'", script, StringComparison.Ordinal);
Assert.Contains($"/moshiro-{role}:latest'", script, StringComparison.Ordinal);
Assert.Contains("SERVER='http", script, StringComparison.Ordinal);
Assert.DoesNotContain("@@", script, StringComparison.Ordinal);
using HttpResponseMessage ps = await _backend.Anonymous.GetAsync("/install.ps1?code=" + code);
string powershell = await ps.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, ps.StatusCode);
Assert.Contains($"$role = '{role}'", powershell, StringComparison.Ordinal);
Assert.DoesNotContain("@@", powershell, StringComparison.Ordinal);
}
[Fact]
public async Task TheScript_GetsTheSignedManifestOfItsRole_ByteForByte_AndNothingWithoutAValidCode()
{
string manifest = """{"schema":1,"component":"probe","channel":"dev","image":"registry.example/moshiro-dev/moshiro-probe","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","tag":"dev-1","version":"0.1.1","commit":"abc","built_at":"2026-10-06T00:00:00Z","seq":1}""";
using HttpResponseMessage published = await _backend.Admin.PostAsync("/api/admin/releases", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = "MEUCIQDc" }));
Assert.True(published.StatusCode is HttpStatusCode.Created or HttpStatusCode.Conflict, await published.Content.ReadAsStringAsync());
string code = (await CreateAsync("probe"))["plaintext"]!.GetValue<string>();
using HttpResponseMessage served = await _backend.Anonymous.GetAsync("/install/manifest?code=" + code);
Assert.Equal(HttpStatusCode.OK, served.StatusCode);
Assert.Equal("no-store", served.Headers.CacheControl?.ToString());
string body = await served.Content.ReadAsStringAsync();
Assert.Contains("\"component\":\"probe\"", body, StringComparison.Ordinal); // 签名对象是原始字节,面板一个字节也不改
using HttpResponseMessage signature = await _backend.Anonymous.GetAsync("/install/manifest.sig?code=" + code);
Assert.Equal(HttpStatusCode.OK, signature.StatusCode);
Assert.False(string.IsNullOrWhiteSpace(await signature.Content.ReadAsStringAsync()));
Assert.Equal(HttpStatusCode.NotFound, (await _backend.Anonymous.GetAsync("/install/manifest?code=jt_wrong")).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await _backend.Anonymous.GetAsync("/install/manifest.sig")).StatusCode);
}
[Fact]
public async Task TheScript_CarriesTheReleaseKey_ToVerifyTheManifest()
{
string code = (await CreateAsync("agent"))["plaintext"]!.GetValue<string>();
string script = await _backend.Anonymous.GetStringAsync("/install?code=" + code);
Assert.Contains("-----BEGIN PUBLIC KEY-----", script, StringComparison.Ordinal);
Assert.DoesNotContain("@@", script, StringComparison.Ordinal);
Assert.Contains("install/manifest?code=", script, StringComparison.Ordinal);
}
[Fact]
public async Task Codes_RecordWhoCreatedThem()
{
JsonNode created = await CreateAsync("agent");
Assert.StartsWith("key:", created["token"]!["created_by"]!.GetValue<string>(), StringComparison.Ordinal); // Admin 客户端是 API key
}
[Fact]
public async Task Script_IsNotServedForUnknownRevokedOrUsedUpCodes()
{
foreach (string path in new[] { "/install", "/install?code=", "/install?code=jt_nope", "/install.ps1?code=jt_nope", "/install.sh?code=jt_nope" })
{
using HttpResponseMessage response = await _backend.Anonymous.GetAsync(path);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
JsonNode revoked = await CreateAsync("agent");
using HttpResponseMessage delete = await _backend.Admin.DeleteAsync("/api/admin/join-tokens/" + revoked["token"]!["id"]!.GetValue<string>());
Assert.True(delete.IsSuccessStatusCode);
using HttpResponseMessage gone = await _backend.Anonymous.GetAsync("/install?code=" + revoked["plaintext"]!.GetValue<string>());
Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode);
// 用完了(一次性令牌已经注册过一个节点)
JsonNode used = await CreateAsync("probe");
string code = used["plaintext"]!.GetValue<string>();
using HttpResponseMessage before = await _backend.Anonymous.GetAsync("/install?code=" + code);
Assert.Equal(HttpStatusCode.OK, before.StatusCode);
await using (await MoShiro.TestSupport.AgentSimulator.EnrollAsync(_backend.Anonymous, code, _backend.UniqueName("used"))) { }
using HttpResponseMessage after = await _backend.Anonymous.GetAsync("/install?code=" + code);
Assert.Equal(HttpStatusCode.NotFound, after.StatusCode);
}
}