diff --git a/src/core/Deal.Api/Services/AuditViewFactory.cs b/src/core/Deal.Api/Services/AuditViewFactory.cs index c24a679..ea53b20 100644 --- a/src/core/Deal.Api/Services/AuditViewFactory.cs +++ b/src/core/Deal.Api/Services/AuditViewFactory.cs @@ -52,33 +52,38 @@ public static class AuditViewFactory return view; } - // Заменяет идентификаторы ссылок изменения значениями из карты имён. + // Заменяет идентификаторы ссылок изменения значениями из карты имён и убирает пустые изменения. // record: Запись аудита. // references: Имена по идентификаторам ссылок. - // Возвращает: Изменения с читаемыми значениями ссылок. + // Возвращает: Изменения с читаемыми значениями ссылок без пар «без изменения». private static IReadOnlyList ResolveChanges( AuditRecordDto record, IReadOnlyDictionary references) { IReadOnlyList changes = record.AuditChanges(); - if (references.Count == 0 || changes.Count == 0) - { - return changes; - } - var resolved = new List(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: Значение изменения. diff --git a/src/core/tests/Deal.Tests.Unit/Api/AuditViewFactoryTests.cs b/src/core/tests/Deal.Tests.Unit/Api/AuditViewFactoryTests.cs new file mode 100644 index 0000000..27f8b35 --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Api/AuditViewFactoryTests.cs @@ -0,0 +1,59 @@ +using Deal.Api.Services; +using Deal.Modules.Tenants.Application.Models; +using Deal.Tests.Unit.Support; + +namespace Deal.Tests.Unit.Api; + +/// +/// Юнит-тесты проекции записей аудита +/// +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 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 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); + } +}