using System.Net; using System.Text.Json.Nodes; namespace MoShiro.Backend.Tests; /// 审计日志:写操作、登录成败都会记一行;谁做的(用户名或 key:令牌名)、结果、细节。 public sealed class AuditTests : IClassFixture { private readonly BackendFixture _backend; public AuditTests(BackendFixture backend) { _backend = backend; } /// 审计在响应发完之后才写,轮询一下。 private async Task FindAsync(string query, Func 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().Contains(note, StringComparison.Ordinal) == true); Assert.NotNull(row); Assert.StartsWith("key:", row["actor"]!.GetValue(), StringComparison.Ordinal); Assert.Equal("key", row["method"]!.GetValue()); Assert.True(row["success"]!.GetValue()); Assert.False(string.IsNullOrEmpty(row["action"]!.GetValue())); } [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() == name); Assert.NotNull(row); Assert.False(row["success"]!.GetValue()); 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); } }