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.
68 lines
2.8 KiB
C#
68 lines
2.8 KiB
C#
using System.Net;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
/// <summary>审计日志:写操作、登录成败都会记一行;谁做的(用户名或 key:令牌名)、结果、细节。</summary>
|
|
public sealed class AuditTests : IClassFixture<BackendFixture>
|
|
{
|
|
private readonly BackendFixture _backend;
|
|
|
|
public AuditTests(BackendFixture backend)
|
|
{
|
|
_backend = backend;
|
|
}
|
|
|
|
/// <summary>审计在响应发完之后才写,轮询一下。</summary>
|
|
private async Task<JsonNode?> FindAsync(string query, Func<JsonNode, bool> match)
|
|
{
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
JsonNode page = JsonNode.Parse(await _backend.Admin.GetStringAsync("/api/admin/audit?" + query))!;
|
|
if (page["items"]!.AsArray().FirstOrDefault(x => x is not null && match(x)) is { } found)
|
|
{
|
|
return found;
|
|
}
|
|
|
|
await Task.Delay(100);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WriteActions_AreRecorded_WithWhoAndTheDetail()
|
|
{
|
|
string note = "audit-" + Guid.NewGuid().ToString("N")[..8];
|
|
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["note"] = note }));
|
|
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
|
|
|
JsonNode? row = await FindAsync("limit=100", x => x["detail"]?.GetValue<string>().Contains(note, StringComparison.Ordinal) == true);
|
|
Assert.NotNull(row);
|
|
Assert.StartsWith("key:", row["actor"]!.GetValue<string>(), StringComparison.Ordinal);
|
|
Assert.Equal("key", row["method"]!.GetValue<string>());
|
|
Assert.True(row["success"]!.GetValue<bool>());
|
|
Assert.False(string.IsNullOrEmpty(row["action"]!.GetValue<string>()));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FailedSignIn_IsRecorded_WithTheAttemptedName_AndWithoutThePassword()
|
|
{
|
|
string name = "nobody-" + Guid.NewGuid().ToString("N")[..8];
|
|
using HttpResponseMessage denied = await _backend.Anonymous.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = name, ["password"] = "secret-pw-123" }));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, denied.StatusCode);
|
|
|
|
JsonNode? row = await FindAsync("action=auth.login&limit=100", x => x["actor"]!.GetValue<string>() == name);
|
|
Assert.NotNull(row);
|
|
Assert.False(row["success"]!.GetValue<bool>());
|
|
Assert.DoesNotContain("secret-pw-123", row.ToJsonString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Audit_NeedsItsOwnCapability()
|
|
{
|
|
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/api/admin/audit");
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
}
|