Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
396 lines
24 KiB
C#
396 lines
24 KiB
C#
using System.Net;
|
|
using System.Text.Json.Nodes;
|
|
using MoShiro.Protocol;
|
|
using MoShiro.TestSupport;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
public sealed class EnrollmentAndTaskSetTests : IClassFixture<BackendFixture>
|
|
{
|
|
private readonly BackendFixture _backend;
|
|
|
|
public EnrollmentAndTaskSetTests(BackendFixture backend)
|
|
{
|
|
_backend = backend;
|
|
}
|
|
|
|
private async Task<JsonNode> AgentAsync(string id) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + id);
|
|
|
|
[Fact]
|
|
public async Task NewAgent_EnrollsConnectsAndReceivesTheDefaultLineTasks()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
|
|
Assert.Equal("active", agent.EnrollmentStatus);
|
|
Assert.Equal("active", agent.AuthStatus);
|
|
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
Assert.Equal(["connect", "tcp_echo", "udp_echo"], set.Tasks.Select(t => t.Kind).Order().ToArray());
|
|
Assert.All(set.Tasks, t =>
|
|
{
|
|
Assert.Equal("line", t.Scope);
|
|
Assert.Equal(TaskSchedule.Continuous, t.Schedule);
|
|
Assert.NotEmpty(t.Params); // 参数已经补全了默认值
|
|
});
|
|
Assert.Equal(10, set.Tasks.Single(t => t.Kind == "udp_echo").Params["rate_hz"].GetInt64());
|
|
Assert.Equal(7444, set.Tasks.Single(t => t.Kind == "tcp_echo").Params["port"].GetInt64());
|
|
Assert.True(set.Version >= 1);
|
|
|
|
JsonNode view = await AgentAsync(agent.NodeId.Value);
|
|
Assert.True(view["online"]!.GetValue<bool>());
|
|
Assert.Equal("203.0.113.10", view["public_host"]!.GetValue<string>());
|
|
Assert.Equal("0.0.1-sim", view["agent_version"]!.GetValue<string>());
|
|
Assert.Equal(200, view["local_limits"]!["max_total_mbps"]!.GetValue<double>());
|
|
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(agent.NodeId.Value))["applied_task_version"]!.GetValue<long>() == set.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TaskSet_OnlyContainsKindsTheAgentSupports()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync(kinds: ["udp_echo"]);
|
|
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 1);
|
|
Assert.Equal(["udp_echo"], set.Tasks.Select(t => t.Kind).ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reconnecting_AlwaysGetsTheCurrentSnapshotAgain()
|
|
{
|
|
AgentSimulator first = await _backend.ConnectAgentAsync();
|
|
TaskSet before = await first.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
await first.DisposeAsync();
|
|
|
|
// 同一个身份重新连接:新连接的服务端状态为空,所以会重新推送当前版本
|
|
await BackendFixture.EventuallyAsync(async () => !(await AgentAsync(first.NodeId.Value))["online"]!.GetValue<bool>());
|
|
Assert.NotNull(before);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Enrollment_RejectsBadTokens_ExhaustedTokens_AndBadProofs()
|
|
{
|
|
using HttpResponseMessage unknown = await PostEnrollAsync(new JsonObject { ["join_token"] = "jt_nope", ["name"] = "x", ["pubkey"] = "AAAA", ["tls_spki"] = "AAAA", ["proof"] = "AAAA" });
|
|
Assert.True(unknown.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.BadRequest);
|
|
|
|
string token = await _backend.CreateJoinTokenAsync(maxUses: 1);
|
|
await using AgentSimulator first = await AgentSimulator.EnrollAsync(_backend.Anonymous, token, _backend.UniqueName("once"));
|
|
InvalidOperationException reuse = await Assert.ThrowsAsync<InvalidOperationException>(() => AgentSimulator.EnrollAsync(_backend.Anonymous, token, _backend.UniqueName("twice")));
|
|
Assert.Contains("401", reuse.Message);
|
|
Assert.Contains("fleet_join_token_invalid", reuse.Message);
|
|
|
|
// 吊销之后不能再用
|
|
string revoked = await _backend.CreateJoinTokenAsync(maxUses: 5);
|
|
JsonNode tokens = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/join-tokens");
|
|
string id = tokens["items"]!.AsArray().First(t => t!["revoked_at"] is null && t["max_uses"]!.GetValue<int>() == 5)!["id"]!.GetValue<string>();
|
|
using HttpResponseMessage revoke = await _backend.Admin.DeleteAsync("/api/admin/join-tokens/" + id);
|
|
Assert.Equal(HttpStatusCode.NoContent, revoke.StatusCode);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => AgentSimulator.EnrollAsync(_backend.Anonymous, revoked, _backend.UniqueName("revoked")));
|
|
|
|
// 吊销之后记录还在(显示「已吊销」);purge=true 才是彻底删除
|
|
tokens = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/join-tokens");
|
|
Assert.Contains(tokens["items"]!.AsArray(), t => t!["id"]!.GetValue<string>() == id);
|
|
using HttpResponseMessage purge = await _backend.Admin.DeleteAsync($"/api/admin/join-tokens/{id}?purge=true");
|
|
Assert.Equal(HttpStatusCode.NoContent, purge.StatusCode);
|
|
tokens = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/join-tokens");
|
|
Assert.DoesNotContain(tokens["items"]!.AsArray(), t => t!["id"]!.GetValue<string>() == id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Enrollment_RejectsATlsPublicKeyThatDoesNotMatchItsFingerprint_AndAnUnknownRole()
|
|
{
|
|
string token = await _backend.CreateJoinTokenAsync(role: "agent");
|
|
using var mine = Fleet.Identity.NodeIdentity.Generate();
|
|
using var tls = Fleet.Identity.NodeIdentity.Generate();
|
|
using var impostor = Fleet.Identity.NodeIdentity.Generate();
|
|
string pubkey = Convert.ToBase64String(mine.PublicKeySpki);
|
|
string hash = Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)));
|
|
// 指纹是 tls 的,公钥却换成了别的:Agent 将来按指纹校验证书,两者必须对得上
|
|
using HttpResponseMessage response = await PostEnrollAsync(new JsonObject
|
|
{
|
|
["join_token"] = token,
|
|
["name"] = _backend.UniqueName("tls"),
|
|
["pubkey"] = pubkey,
|
|
["tls_spki"] = Convert.ToBase64String(Fleet.Identity.NodeIdentity.Fingerprint(tls.PublicKeySpki)),
|
|
["tls_pubkey"] = Convert.ToBase64String(impostor.PublicKeySpki),
|
|
["proof"] = Convert.ToBase64String(mine.Sign(Fleet.Identity.SignaturePurpose.Enroll, hash, pubkey)),
|
|
});
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
|
|
using HttpResponseMessage badRole = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "root" }));
|
|
Assert.True(badRole.StatusCode is HttpStatusCode.BadRequest or HttpStatusCode.UnprocessableEntity);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Enrollment_RejectsAProofSignedByAnotherKey()
|
|
{
|
|
string token = await _backend.CreateJoinTokenAsync();
|
|
using var other = Fleet.Identity.NodeIdentity.Generate();
|
|
using var mine = Fleet.Identity.NodeIdentity.Generate();
|
|
string pubkey = Convert.ToBase64String(mine.PublicKeySpki);
|
|
string hash = Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)));
|
|
string forged = Convert.ToBase64String(other.Sign(Fleet.Identity.SignaturePurpose.Enroll, hash, pubkey));
|
|
using HttpResponseMessage response = await PostEnrollAsync(new JsonObject
|
|
{
|
|
["join_token"] = token,
|
|
["name"] = _backend.UniqueName("forged"),
|
|
["pubkey"] = pubkey,
|
|
["tls_spki"] = Convert.ToBase64String(Fleet.Identity.NodeIdentity.Fingerprint(mine.PublicKeySpki)),
|
|
["tls_pubkey"] = pubkey,
|
|
["proof"] = forged,
|
|
});
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
Assert.Equal("fleet_proof_invalid", JsonNode.Parse(await response.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
|
|
|
// 令牌没有被消耗:用正确的证明仍然可以注册
|
|
await using AgentSimulator ok = await AgentSimulator.EnrollAsync(_backend.Anonymous, token, _backend.UniqueName("honest"));
|
|
Assert.Equal("active", ok.EnrollmentStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectionWithAWrongSignature_IsRejected_AndLeaksNothing()
|
|
{
|
|
await using AgentSimulator real = await _backend.EnrollOnlyAsync();
|
|
await using AgentSimulator stranger = await _backend.EnrollOnlyAsync();
|
|
|
|
// stranger 用自己的私钥冒充 real 的节点 ID:签名对不上
|
|
AuthAttempt impostor = await AgentWire.AuthenticateAsync(_backend.BaseAddress, real.NodeId, nonce => stranger.SignAuth(real.NodeId, nonce));
|
|
Assert.False(impostor.Ok);
|
|
Assert.Equal("fleet_unauthorized", impostor.ErrorCode);
|
|
|
|
// 节点不存在与签名错误返回同样的错误,不泄露节点是否存在
|
|
var ghost = new Fleet.Core.NodeId("agt_does_not_exist");
|
|
AuthAttempt unknown = await AgentWire.AuthenticateAsync(_backend.BaseAddress, ghost, nonce => stranger.SignAuth(ghost, nonce));
|
|
Assert.Equal(impostor.ErrorCode, unknown.ErrorCode);
|
|
|
|
// 正主仍然可以连接
|
|
AuthAttempt genuine = await AgentWire.AuthenticateAsync(_backend.BaseAddress, real.NodeId, nonce => real.SignAuth(real.NodeId, nonce));
|
|
Assert.True(genuine.Ok);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PendingAgent_ConnectsButGetsNoStateAndItsChunksAreIgnored_UntilApproved()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync(autoApprove: false);
|
|
Assert.Equal("pending", agent.EnrollmentStatus);
|
|
Assert.Equal("pending", agent.AuthStatus);
|
|
await Task.Delay(300);
|
|
Assert.Empty(agent.TaskSets);
|
|
|
|
RawChunk chunk = ChunkFactory.UdpEchoClean(agent.NodeId, TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds() - 30_000);
|
|
chunk.Header.AgentId = agent.NodeId;
|
|
await agent.SendBinaryAsync(Fleet.Rpc.OutboxFrame.Build(MoShiroChannels.RawChunk, chunk.Header.ChunkId.Value, RawChunkCodec.Encode(chunk)));
|
|
await Task.Delay(300);
|
|
JsonNode chunks = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/raw/chunks?agent_id=" + agent.NodeId.Value);
|
|
Assert.Empty(chunks["items"]!.AsArray());
|
|
|
|
using HttpResponseMessage approve = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.NodeId.Value}/approve", null);
|
|
Assert.Equal(HttpStatusCode.OK, approve.StatusCode);
|
|
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
Assert.NotNull(set);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DisablingAConnectedAgent_RevokesItAndRefusesFutureConnections()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
|
|
using HttpResponseMessage disable = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.NodeId.Value}/disable", null);
|
|
Assert.Equal(HttpStatusCode.OK, disable.StatusCode);
|
|
await agent.WaitUntilAsync(() => agent.Revoked);
|
|
await BackendFixture.EventuallyAsync(async () => !(await AgentAsync(agent.NodeId.Value))["online"]!.GetValue<bool>());
|
|
|
|
AuthAttempt again = await AgentWire.AuthenticateAsync(_backend.BaseAddress, agent.NodeId, nonce => agent.SignAuth(agent.NodeId, nonce));
|
|
Assert.False(again.Ok);
|
|
Assert.Equal("fleet_node_disabled", again.ErrorCode);
|
|
|
|
using HttpResponseMessage enable = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.NodeId.Value}/enable", null);
|
|
Assert.Equal(HttpStatusCode.OK, enable.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SameNameEnrollment_GoesPending_ApprovalConflicts_AndTakeoverReplacesTheKey()
|
|
{
|
|
string name = _backend.UniqueName("jp");
|
|
await using AgentSimulator original = await _backend.ConnectAgentAsync(name);
|
|
await using AgentSimulator replacement = await _backend.EnrollOnlyAsync(name);
|
|
Assert.Equal("pending", replacement.EnrollmentStatus);
|
|
|
|
using HttpResponseMessage approve = await _backend.Admin.PostAsync($"/api/admin/agents/{replacement.NodeId.Value}/approve", null);
|
|
Assert.Equal(HttpStatusCode.Conflict, approve.StatusCode);
|
|
Assert.Equal("name_conflict", JsonNode.Parse(await approve.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
|
|
|
using HttpResponseMessage takeover = await _backend.Admin.PostAsync($"/api/admin/agents/{replacement.NodeId.Value}/takeover", null);
|
|
Assert.Equal(HttpStatusCode.OK, takeover.StatusCode);
|
|
Assert.Equal(original.NodeId.Value, JsonNode.Parse(await takeover.Content.ReadAsStringAsync())!["id"]!.GetValue<string>()); // 旧 ID 保留,历史数据不丢
|
|
|
|
using HttpResponseMessage pendingGone = await _backend.Admin.GetAsync("/api/admin/agents/" + replacement.NodeId.Value);
|
|
Assert.Equal(HttpStatusCode.NotFound, pendingGone.StatusCode);
|
|
await original.WaitUntilAsync(() => original.Revoked); // 旧密钥的连接被断开
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Patch_RenamesWithConflictDetection_AndHonoursIfMatch()
|
|
{
|
|
await using AgentSimulator a = await _backend.ConnectAgentAsync();
|
|
await using AgentSimulator b = await _backend.ConnectAgentAsync();
|
|
JsonNode view = await AgentAsync(b.NodeId.Value);
|
|
string name = view["name"]!.GetValue<string>();
|
|
|
|
using HttpResponseMessage conflict = await _backend.Admin.PatchAsync(
|
|
$"/api/admin/agents/{a.NodeId.Value}", BackendFixture.Json(new JsonObject { ["name"] = name }, "application/merge-patch+json"));
|
|
Assert.Equal(HttpStatusCode.Conflict, conflict.StatusCode);
|
|
|
|
using HttpResponseMessage get = await _backend.Admin.GetAsync("/api/admin/agents/" + a.NodeId.Value);
|
|
string etag = get.Headers.ETag!.Tag;
|
|
using var stale = new HttpRequestMessage(HttpMethod.Patch, "/api/admin/agents/" + a.NodeId.Value) { Content = BackendFixture.Json(new JsonObject { ["labels"] = new JsonArray("hk") }, "application/merge-patch+json") };
|
|
stale.Headers.TryAddWithoutValidation("If-Match", "\"999999\"");
|
|
using HttpResponseMessage precondition = await _backend.Admin.SendAsync(stale);
|
|
Assert.Equal(HttpStatusCode.PreconditionFailed, precondition.StatusCode);
|
|
|
|
using var fresh = new HttpRequestMessage(HttpMethod.Patch, "/api/admin/agents/" + a.NodeId.Value) { Content = BackendFixture.Json(new JsonObject { ["labels"] = new JsonArray("hk"), ["public_visible"] = false }, "application/merge-patch+json") };
|
|
fresh.Headers.TryAddWithoutValidation("If-Match", etag);
|
|
using HttpResponseMessage ok = await _backend.Admin.SendAsync(fresh);
|
|
Assert.Equal(HttpStatusCode.OK, ok.StatusCode);
|
|
JsonNode updated = JsonNode.Parse(await ok.Content.ReadAsStringAsync())!;
|
|
Assert.Equal("hk", updated["labels"]![0]!.GetValue<string>());
|
|
Assert.False(updated["public_visible"]!.GetValue<bool>());
|
|
Assert.NotEqual(etag, ok.Headers.ETag!.Tag);
|
|
|
|
using HttpResponseMessage invalid = await _backend.Admin.PatchAsync(
|
|
$"/api/admin/agents/{a.NodeId.Value}", BackendFixture.Json(new JsonObject { ["name"] = "bad name!" }, "application/merge-patch+json"));
|
|
Assert.Equal(HttpStatusCode.BadRequest, invalid.StatusCode);
|
|
Assert.NotNull(JsonNode.Parse(await invalid.Content.ReadAsStringAsync())!["errors"]!["name"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TaskOverride_ChangesTheDesiredStateAndBumpsTheVersion()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
TaskSet initial = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
TaskSpec udp = initial.Tasks.Single(t => t.Kind == "udp_echo");
|
|
|
|
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "line",
|
|
["kind"] = "udp_echo",
|
|
["agent_id"] = agent.NodeId.Value,
|
|
["base_task_id"] = udp.Id.Value,
|
|
["params"] = new JsonObject { ["rate_hz"] = 20 },
|
|
}));
|
|
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
|
Assert.StartsWith("/api/admin/tasks/", created.Headers.Location!.ToString());
|
|
|
|
TaskSet updated = await agent.WaitForTaskSetAsync(s => s.Version > initial.Version && s.Tasks.Any(t => t.Kind == "udp_echo" && t.Params["rate_hz"].GetInt64() == 20));
|
|
Assert.Equal(Defaults.TaskCount, updated.Tasks.Count);
|
|
Assert.Equal(7443, updated.Tasks.Single(t => t.Kind == "udp_echo").Params["port"].GetInt64());
|
|
|
|
// 停用覆盖 → 该 Agent 不再运行这条任务(模板对其他 Agent 不受影响)
|
|
string overrideId = JsonNode.Parse(await created.Content.ReadAsStringAsync())!["id"]!.GetValue<string>();
|
|
using HttpResponseMessage disable = await _backend.Admin.PatchAsync("/api/admin/tasks/" + overrideId, BackendFixture.Json(new JsonObject { ["enabled"] = false }, "application/merge-patch+json"));
|
|
Assert.Equal(HttpStatusCode.OK, disable.StatusCode);
|
|
TaskSet disabled = await agent.WaitForTaskSetAsync(s => s.Version > updated.Version && s.Tasks.All(t => t.Kind != "udp_echo"));
|
|
Assert.Equal(Defaults.TaskCount - 1, disabled.Tasks.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Tasks_ValidateParamsKindsBudgetAndTargets()
|
|
{
|
|
using HttpResponseMessage badParams = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "line", ["kind"] = "udp_echo", ["params"] = new JsonObject { ["rate_hz"] = 5000, ["bogus"] = 1 },
|
|
}));
|
|
Assert.Equal(HttpStatusCode.BadRequest, badParams.StatusCode);
|
|
JsonNode problem = JsonNode.Parse(await badParams.Content.ReadAsStringAsync())!;
|
|
Assert.Contains("between 1 and 200", problem["errors"]!["params.rate_hz"]![0]!.GetValue<string>());
|
|
Assert.NotNull(problem["errors"]!["params.bogus"]);
|
|
|
|
using HttpResponseMessage unknownKind = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "warp_drive" }));
|
|
Assert.Equal(HttpStatusCode.NotFound, unknownKind.StatusCode);
|
|
Assert.Equal("unknown_kind", JsonNode.Parse(await unknownKind.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
|
|
|
using HttpResponseMessage overBudget = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "line", ["kind"] = "udp_echo", ["params"] = new JsonObject { ["rate_hz"] = 200, ["size"] = 1400 },
|
|
}));
|
|
Assert.Equal(HttpStatusCode.BadRequest, overBudget.StatusCode);
|
|
Assert.Equal("budget_exceeded", JsonNode.Parse(await overBudget.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
|
|
|
using HttpResponseMessage wrongScope = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "icmp_echo" }));
|
|
Assert.Equal(HttpStatusCode.BadRequest, wrongScope.StatusCode);
|
|
|
|
// 目标任务:只接受 IP(不做 DNS 解析),需要指定 Agent
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
using HttpResponseMessage dns = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "target", ["kind"] = "icmp_echo", ["agent_id"] = agent.NodeId.Value, ["target"] = "example.com",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.BadRequest, dns.StatusCode);
|
|
using HttpResponseMessage target = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "target", ["kind"] = "icmp_echo", ["agent_id"] = agent.NodeId.Value, ["target"] = "1.1.1.1",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.Created, target.StatusCode);
|
|
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Any(t => t.Scope == "target"));
|
|
Assert.Equal("1.1.1.1", set.Tasks.Single(t => t.Scope == "target").Target);
|
|
|
|
using HttpResponseMessage onceSchedule = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "udp_echo", ["schedule"] = "every:30" }));
|
|
Assert.Equal(HttpStatusCode.BadRequest, onceSchedule.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TaskSet_RejectedItemsReportedByTheAgent_AreVisibleToTheAdmin()
|
|
{
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
agent.RejectNextState = [new Fleet.Rpc.RejectedItem { Id = "tsk_x", Reason = "policy_denied" }];
|
|
|
|
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
|
|
{
|
|
["scope"] = "target", ["kind"] = "connect", ["agent_id"] = agent.NodeId.Value, ["target"] = "192.0.2.1:443",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
|
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(agent.NodeId.Value))["rejected"]!.AsArray().Count == 1);
|
|
Assert.Equal("policy_denied", (await AgentAsync(agent.NodeId.Value))["rejected"]![0]!["reason"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DialOnlyNode_EnrollDisableDelete_IsReflectedInTheListenersPeerList()
|
|
{
|
|
await using AgentSimulator listener = await _backend.ConnectAgentAsync();
|
|
await listener.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
|
|
|
string name = _backend.UniqueName("home");
|
|
using EnrolledDialer dialer = await _backend.CreateDialerAsync(name);
|
|
string id = dialer.DialerId;
|
|
Assert.StartsWith(AgentIds.Prefix, id, StringComparison.Ordinal);
|
|
string peerKey = Convert.ToBase64String(dialer.Identity.PeerIdentity.PublicKeySpki);
|
|
string backendKey = Convert.ToBase64String(dialer.Identity.Identity.PublicKeySpki);
|
|
Assert.NotEqual(backendKey, peerKey); // 连面板的和连监听方的是两把独立的密钥
|
|
|
|
// 监听方的名单里放的是对等面的公钥,不是连面板的那把
|
|
TaskSet withDialer = await listener.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == id && p.PublicKey == peerKey));
|
|
Assert.DoesNotContain(withDialer.Peers, p => p.PublicKey == backendKey);
|
|
|
|
// 重名节点进入待审批状态,尚未获得任务
|
|
string duplicateToken = await _backend.CreateJoinTokenAsync(role: "agent");
|
|
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
|
|
Fleet.Core.Result<Fleet.Enrollment.Client.EnrollmentResult> duplicate = await Fleet.Enrollment.Client.EnrollmentClient.EnrollAsync(
|
|
http, _backend.BaseAddress, duplicateToken, name, Path.Combine(Path.GetTempPath(), "moshiro-dup-" + Guid.NewGuid().ToString("N")), "0.0.0", null, TimeProvider.System, default);
|
|
Assert.True(duplicate.IsSuccess);
|
|
JsonNode pending = await AgentAsync(duplicate.Value.NodeId);
|
|
Assert.Equal("pending", pending["status"]!.GetValue<string>());
|
|
|
|
// 停用:从名单里消失,记录还在
|
|
using HttpResponseMessage disabled = await _backend.Admin.PostAsync($"/api/admin/agents/{id}/disable", BackendFixture.Json(new JsonObject()));
|
|
Assert.True(disabled.IsSuccessStatusCode);
|
|
await listener.WaitForTaskSetAsync(s => s.Peers.All(p => p.NodeId.Value != id));
|
|
|
|
// 删除
|
|
using HttpResponseMessage deleted = await _backend.Admin.DeleteAsync("/api/admin/agents/" + id);
|
|
Assert.Equal(HttpStatusCode.NoContent, deleted.StatusCode);
|
|
using HttpResponseMessage gone = await _backend.Admin.GetAsync("/api/admin/agents/" + id);
|
|
Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode);
|
|
}
|
|
|
|
private Task<HttpResponseMessage> PostEnrollAsync(JsonNode body) =>
|
|
_backend.Anonymous.PostAsync("/api/agent/v1/enroll", BackendFixture.Json(body));
|
|
}
|