837dbeb794
Implements pure domain logic for security master synchronization: - Conflict resolution (last-write-wins by PublishedAt) - Idempotency key generation - Rollback detection - Rule validation and active-time checking - 13 unit tests: 13/13 PASS AGENTS.md v16.0 compliance: ✅ Necessity: WBS VS-02 DOMAIN phase ✅ Simplicity: Pure logic, no I/O, deterministic ✅ SOLID: Single responsibility (policy only) ✅ Guardrails: Idempotent, versioned, rollback-safe Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
250 lines
7.3 KiB
C#
250 lines
7.3 KiB
C#
using KArtSell.Modules.ModelOperations.Domain;
|
|
using Xunit;
|
|
|
|
namespace KArtSell.ModelOperations.UnitTests;
|
|
|
|
public class VS02_SecurityMasterPolicyTests
|
|
{
|
|
[Fact]
|
|
public void ResolveSyncConflict_LocalVersionAhead_ReturnsIdempotent()
|
|
{
|
|
var state = new SyncState(
|
|
LocalVersion: 5,
|
|
RemoteVersion: 3,
|
|
LocalRules: new(),
|
|
RemoteRules: new(),
|
|
IdempotencyKey: "key-123",
|
|
CorrelationId: "corr-456");
|
|
|
|
var result = SecurityMasterPolicy.ResolveSyncConflict(state);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(5, result.NewVersion);
|
|
Assert.Empty(result.AppliedRules);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveSyncConflict_VersionsMatch_ReturnsIdempotent()
|
|
{
|
|
var state = new SyncState(
|
|
LocalVersion: 5,
|
|
RemoteVersion: 5,
|
|
LocalRules: new(),
|
|
RemoteRules: new(),
|
|
IdempotencyKey: "key-123",
|
|
CorrelationId: "corr-456");
|
|
|
|
var result = SecurityMasterPolicy.ResolveSyncConflict(state);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(5, result.NewVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveSyncConflict_RemoteAhead_AppliesNewRules()
|
|
{
|
|
var remoteRule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow,
|
|
ExpiresAt: null,
|
|
PublishedAt: DateTime.UtcNow,
|
|
CorrelationId: "corr-456");
|
|
|
|
var state = new SyncState(
|
|
LocalVersion: 1,
|
|
RemoteVersion: 2,
|
|
LocalRules: new(),
|
|
RemoteRules: new() { remoteRule },
|
|
IdempotencyKey: "key-123",
|
|
CorrelationId: "corr-456");
|
|
|
|
var result = SecurityMasterPolicy.ResolveSyncConflict(state);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(2, result.NewVersion);
|
|
Assert.Single(result.AppliedRules);
|
|
Assert.Equal(remoteRule.RuleId, result.AppliedRules[0].RuleId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveSyncConflict_LastWriteWins_UsesNewerTimestamp()
|
|
{
|
|
var ruleId = Guid.NewGuid();
|
|
var olderTime = DateTime.UtcNow.AddMinutes(-5);
|
|
var newerTime = DateTime.UtcNow;
|
|
|
|
var localRule = new SecurityRule(
|
|
RuleId: ruleId,
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow,
|
|
ExpiresAt: null,
|
|
PublishedAt: olderTime,
|
|
CorrelationId: "corr-456");
|
|
|
|
var remoteRule = new SecurityRule(
|
|
RuleId: ruleId,
|
|
ResourceName: "api/users",
|
|
Action: "write",
|
|
Version: 2,
|
|
EffectiveAt: DateTime.UtcNow,
|
|
ExpiresAt: null,
|
|
PublishedAt: newerTime,
|
|
CorrelationId: "corr-456");
|
|
|
|
var state = new SyncState(
|
|
LocalVersion: 1,
|
|
RemoteVersion: 2,
|
|
LocalRules: new() { localRule },
|
|
RemoteRules: new() { remoteRule },
|
|
IdempotencyKey: "key-123",
|
|
CorrelationId: "corr-456");
|
|
|
|
var result = SecurityMasterPolicy.ResolveSyncConflict(state);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Single(result.AppliedRules);
|
|
Assert.Equal("write", result.AppliedRules[0].Action);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateRule_ValidRule_ReturnsTrue()
|
|
{
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow,
|
|
ExpiresAt: DateTime.UtcNow.AddDays(30),
|
|
PublishedAt: DateTime.UtcNow,
|
|
CorrelationId: "corr-456");
|
|
|
|
var (isValid, errors) = SecurityMasterPolicy.ValidateRule(rule);
|
|
|
|
Assert.True(isValid);
|
|
Assert.Empty(errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateRule_InvalidAction_ReturnsFalse()
|
|
{
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "DELETE",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow,
|
|
ExpiresAt: null,
|
|
PublishedAt: DateTime.UtcNow,
|
|
CorrelationId: "corr-456");
|
|
|
|
var (isValid, errors) = SecurityMasterPolicy.ValidateRule(rule);
|
|
|
|
Assert.False(isValid);
|
|
Assert.Contains("Action must be one of", errors[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateRule_ExpiresBeforeEffective_ReturnsFalse()
|
|
{
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow.AddDays(10),
|
|
ExpiresAt: DateTime.UtcNow.AddDays(5),
|
|
PublishedAt: DateTime.UtcNow,
|
|
CorrelationId: "corr-456");
|
|
|
|
var (isValid, errors) = SecurityMasterPolicy.ValidateRule(rule);
|
|
|
|
Assert.False(isValid);
|
|
Assert.Contains("EffectiveAt must be before", errors[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsRuleActive_BeforeEffectiveTime_ReturnsFalse()
|
|
{
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow.AddDays(1),
|
|
ExpiresAt: null,
|
|
PublishedAt: DateTime.UtcNow,
|
|
CorrelationId: "corr-456");
|
|
|
|
var isActive = SecurityMasterPolicy.IsRuleActive(rule, DateTime.UtcNow);
|
|
|
|
Assert.False(isActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsRuleActive_AfterExpiryTime_ReturnsFalse()
|
|
{
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: DateTime.UtcNow.AddDays(-1),
|
|
ExpiresAt: DateTime.UtcNow.AddMinutes(-1),
|
|
PublishedAt: DateTime.UtcNow.AddDays(-1),
|
|
CorrelationId: "corr-456");
|
|
|
|
var isActive = SecurityMasterPolicy.IsRuleActive(rule, DateTime.UtcNow);
|
|
|
|
Assert.False(isActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsRuleActive_WithinWindow_ReturnsTrue()
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var rule = new SecurityRule(
|
|
RuleId: Guid.NewGuid(),
|
|
ResourceName: "api/users",
|
|
Action: "read",
|
|
Version: 1,
|
|
EffectiveAt: now.AddHours(-1),
|
|
ExpiresAt: now.AddHours(1),
|
|
PublishedAt: now.AddDays(-1),
|
|
CorrelationId: "corr-456");
|
|
|
|
var isActive = SecurityMasterPolicy.IsRuleActive(rule, now);
|
|
|
|
Assert.True(isActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateIdempotencyKey_FormatsCorrectly()
|
|
{
|
|
var key = SecurityMasterPolicy.CreateIdempotencyKey(5, "corr-123");
|
|
|
|
Assert.Equal("sync-5-corr-123", key);
|
|
}
|
|
|
|
[Fact]
|
|
public void RequiresRollback_NoRulesAppliedButVersionIncremented_ReturnsTrue()
|
|
{
|
|
var requiresRollback = SecurityMasterPolicy.RequiresRollback(0, 1);
|
|
|
|
Assert.True(requiresRollback);
|
|
}
|
|
|
|
[Fact]
|
|
public void RequiresRollback_RulesApplied_ReturnsFalse()
|
|
{
|
|
var requiresRollback = SecurityMasterPolicy.RequiresRollback(5, 1);
|
|
|
|
Assert.False(requiresRollback);
|
|
}
|
|
}
|