Аудит: читаемые имена и «было → стало» вместо сырых данных #28

Merged
rust merged 8 commits from t25_audit_readable into main 2026-09-14 23:36:11 +03:00
2 changed files with 75 additions and 11 deletions
Showing only changes of commit 16f4270995 - Show all commits
+16 -11
View File
@@ -52,33 +52,38 @@ public static class AuditViewFactory
return view;
}
// Заменяет идентификаторы ссылок изменения значениями из карты имён.
// Заменяет идентификаторы ссылок изменения значениями из карты имён и убирает пустые изменения.
// record: Запись аудита.
// references: Имена по идентификаторам ссылок.
// Возвращает: Изменения с читаемыми значениями ссылок.
// Возвращает: Изменения с читаемыми значениями ссылок без пар «без изменения».
private static IReadOnlyList<AuditChangeDto> ResolveChanges(
AuditRecordDto record,
IReadOnlyDictionary<string, string> references)
{
IReadOnlyList<AuditChangeDto> changes = record.AuditChanges();
if (references.Count == 0 || changes.Count == 0)
{
return changes;
}
var resolved = new List<AuditChangeDto>(changes.Count);
foreach (AuditChangeDto change in changes)
{
resolved.Add(change with
string? from = ResolveReference(references, change.From);
string? to = ResolveReference(references, change.To);
if (IsUnchanged(from, to))
{
From = ResolveReference(references, change.From),
To = ResolveReference(references, change.To),
});
continue;
}
resolved.Add(change with { From = from, To = to });
}
return resolved;
}
// Сравнивает значения «было» и «стало».
// from: Значение до изменения.
// to: Значение после изменения.
// Возвращает: True — значения заданы и совпадают.
private static bool IsUnchanged(string? from, string? to) =>
from is not null && to is not null && string.Equals(from, to, StringComparison.Ordinal);
// Подставляет имя вместо идентификатора ссылки, если оно известно.
// references: Имена по идентификаторам ссылок.
// value: Значение изменения.
@@ -0,0 +1,59 @@
using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Models;
using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Api;
/// <summary>
/// Юнит-тесты проекции записей аудита
/// </summary>
public sealed class AuditViewFactoryTests
{
[Fact]
public async Task Project_DropsUnchangedValues()
{
var record = new AuditRecordDto(
AuditEvents.TenantLimitChanged,
AuditActorTypes.Operator,
ActorId: null,
TenantId: null,
Ip: null,
DetailJson: """{"tenantId":"00000000-0000-0000-0000-000000000001","oldBudget":10000000,"oldPeriod":"month","budgetTokens":100000,"period":"month"}""");
IReadOnlyList<AuditRecordViewDto> view = await AuditViewFactory.ProjectAsync(
[record],
new TestTenantRepository().Repository,
new TestAuthStore().Store,
new TestAuditReferenceResolver(),
CancellationToken.None);
AuditRecordViewDto row = Assert.Single(view);
AuditChangeDto budget = Assert.Single(row.Changes);
Assert.Equal("budget", budget.Field);
Assert.Equal("10000000", budget.From);
Assert.Equal("100000", budget.To);
}
[Fact]
public async Task Project_KeepsChangedValues()
{
var record = new AuditRecordDto(
AuditEvents.SettingsUpdated,
AuditActorTypes.Tenant,
ActorId: null,
TenantId: null,
Ip: null,
DetailJson: """{"changes":[{"field":"minLen","from":"24","to":"42"}]}""");
IReadOnlyList<AuditRecordViewDto> view = await AuditViewFactory.ProjectAsync(
[record],
new TestTenantRepository().Repository,
new TestAuthStore().Store,
new TestAuditReferenceResolver(),
CancellationToken.None);
AuditChangeDto change = Assert.Single(Assert.Single(view).Changes);
Assert.Equal("24", change.From);
Assert.Equal("42", change.To);
}
}