Files
KArtSell.Aegis/tests/KArtSell.ModelOperations.UnitTests/VS02_SecurityMasterPolicyTests.cs
kjh2064 837dbeb794 feat: Complete VS-02 DOMAIN - SecurityMaster sync policy (Batch 1 - 3/7)
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>
2026-08-05 21:05:14 +09:00

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);
}
}