using System.Net; using System.Text.Json.Nodes; namespace MoShiro.Backend.Tests; /// /// 一行安装:Agent 和 Probe 共用同一份静态脚本(所有人拿到的字节都一样,所以哈希固定、可以先核对再执行), /// 面板地址、角色、一次性代码都走命令里的环境变量;脚本再向面板取 CI 签名的发布清单来验证镜像。 /// [Collection("probe-e2e")] public sealed class InstallTests { private readonly BackendFixture _backend; public InstallTests(BackendFixture backend) { _backend = backend; } private async Task 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_VerifyTheScriptHash_ThenRunItWithThePublicValues(string role) { JsonNode created = await CreateAsync(role); string code = created["plaintext"]!.GetValue(); JsonNode commands = created["install_commands"]!; string linux = commands["linux"]!.GetValue(); string macos = commands["macos"]!.GetValue(); string windows = commands["windows"]!.GetValue(); Assert.StartsWith("f=$(mktemp) && curl -fsSL '", linux, StringComparison.Ordinal); Assert.Contains("/install' -o \"$f\" && echo \"", linux, StringComparison.Ordinal); Assert.Contains("sha256sum -c -", linux, StringComparison.Ordinal); Assert.Contains($"MOSHIRO_ROLE='{role}'", linux, StringComparison.Ordinal); Assert.Contains($"MOSHIRO_CODE='{code}'", linux, StringComparison.Ordinal); Assert.EndsWith("sh \"$f\"", linux, StringComparison.Ordinal); Assert.Contains("shasum -a 256 -c -", macos, StringComparison.Ordinal); Assert.StartsWith("$f = Join-Path $env:TEMP", windows, StringComparison.Ordinal); Assert.Contains($"$env:MOSHIRO_ROLE='{role}'", windows, StringComparison.Ordinal); Assert.EndsWith("iex (Get-Content -Raw $f)", windows, StringComparison.Ordinal); Assert.DoesNotContain("<", linux + macos + windows, StringComparison.Ordinal); // 没有要用户自己替换的占位符 } [Fact] public async Task TheScripts_AreStatic_TheSameBytesForEveryone_AndNeedNoCode() { foreach (string path in new[] { "/install", "/install.sh", "/install.ps1" }) { using HttpResponseMessage first = await _backend.Anonymous.GetAsync(path); using HttpResponseMessage second = await _backend.Anonymous.GetAsync(path + "?code=jt_whatever"); Assert.Equal(HttpStatusCode.OK, first.StatusCode); Assert.Equal("no-store", first.Headers.CacheControl?.ToString()); string body = await first.Content.ReadAsStringAsync(); Assert.Equal(body, await second.Content.ReadAsStringAsync()); // 带不带码、带什么码,内容都一样 Assert.DoesNotContain("@@", body, StringComparison.Ordinal); Assert.Contains("MOSHIRO_SERVER", body, StringComparison.Ordinal); Assert.Contains("MOSHIRO_ROLE", body, StringComparison.Ordinal); Assert.Contains("MOSHIRO_CODE", body, StringComparison.Ordinal); } // 同一份字节:sh 的两个路径一致 Assert.Equal(await _backend.Anonymous.GetStringAsync("/install"), await _backend.Anonymous.GetStringAsync("/install.sh")); } [Fact] public async Task TheScripts_CarryTheReleaseKey_ThatTheBackendShips() { string key = MoShiro.Backend.Api.InstallScripts.ReleaseKey(); Assert.StartsWith("-----BEGIN PUBLIC KEY-----", key, StringComparison.Ordinal); foreach (string path in new[] { "/install", "/install.ps1" }) { string script = (await _backend.Anonymous.GetStringAsync(path)).Replace("\r\n", "\n", StringComparison.Ordinal); Assert.Contains(key.Replace("\r\n", "\n", StringComparison.Ordinal), script, StringComparison.Ordinal); // 脚本里内置的和面板发布的是同一把 } } [Fact] public async Task TheScript_GetsTheSignedManifestOfItsRole_ByteForByte() { 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(); 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()); Assert.Contains("\"component\":\"probe\"", await served.Content.ReadAsStringAsync(), 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())); } [Fact] public async Task TheManifest_IsNotServedForUnknownRevokedOrUsedUpCodes() { foreach (string path in new[] { "/install/manifest", "/install/manifest?code=", "/install/manifest?code=jt_nope", "/install/manifest.sig?code=jt_nope" }) { using HttpResponseMessage response = await _backend.Anonymous.GetAsync(path); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } JsonNode revoked = await CreateAsync("probe"); using HttpResponseMessage delete = await _backend.Admin.DeleteAsync("/api/admin/join-tokens/" + revoked["token"]!["id"]!.GetValue()); Assert.True(delete.IsSuccessStatusCode); using HttpResponseMessage gone = await _backend.Anonymous.GetAsync("/install/manifest?code=" + revoked["plaintext"]!.GetValue()); Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode); // 用完了(一次性令牌已经注册过一个节点):脚本在动手之前就能从这里知道,给出明确的提示 JsonNode used = await CreateAsync("probe"); string code = used["plaintext"]!.GetValue(); await using (await MoShiro.TestSupport.AgentSimulator.EnrollAsync(_backend.Anonymous, code, _backend.UniqueName("used"))) { } using HttpResponseMessage after = await _backend.Anonymous.GetAsync("/install/manifest?code=" + code); Assert.Equal(HttpStatusCode.NotFound, after.StatusCode); } [Fact] public async Task Codes_RecordWhoCreatedThem() { JsonNode created = await CreateAsync("agent"); Assert.StartsWith("key:", created["token"]!["created_by"]!.GetValue(), StringComparison.Ordinal); // Admin 客户端是 API key } }