using ItWorx.KnowledgeHub.Domain.Automation; namespace ItWorx.KnowledgeHub.Domain.Tests; public sealed class ServiceAccountTests { private static readonly DateTimeOffset Now = new(2026, 8, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public void Create_starts_enabled() { var account = ServiceAccount.Create(Guid.NewGuid(), "CI-bot", "Beschrijving.", Guid.NewGuid(), Now); Assert.True(account.Enabled); Assert.Null(account.DisabledAt); } [Fact] public void Disable_is_idempotent_and_records_the_first_disable_time_only() { var account = ServiceAccount.Create(Guid.NewGuid(), "CI-bot", "B.", Guid.NewGuid(), Now); account.Disable(Now); account.Disable(Now.AddHours(1)); Assert.False(account.Enabled); Assert.Equal(Now, account.DisabledAt); } } public sealed class ServiceAccountTokenTests { private static readonly DateTimeOffset Now = new(2026, 8, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public void Create_rejects_a_token_with_no_valid_scopes() { var exception = Record.Exception(() => ServiceAccountToken.Create(Guid.NewGuid(), "pub123", "hash", ["not-a-real-scope"], null, Now)); Assert.IsType(exception); } [Fact] public void Create_normalizes_and_deduplicates_scopes() { var token = ServiceAccountToken.Create( Guid.NewGuid(), "pub123", "hash", [Scopes.PagesRead, Scopes.PagesRead, "bogus"], null, Now); Assert.Equal([Scopes.PagesRead], token.Scopes); } [Fact] public void A_never_expiring_never_revoked_token_is_active() { var token = ServiceAccountToken.Create(Guid.NewGuid(), "pub", "hash", [Scopes.PagesRead], null, Now); Assert.True(token.IsActive(Now.AddYears(10))); } [Fact] public void An_expired_token_is_not_active() { var token = ServiceAccountToken.Create(Guid.NewGuid(), "pub", "hash", [Scopes.PagesRead], Now.AddDays(1), Now); Assert.False(token.IsActive(Now.AddDays(2))); } [Fact] public void A_revoked_token_is_not_active_even_before_its_expiry() { var token = ServiceAccountToken.Create(Guid.NewGuid(), "pub", "hash", [Scopes.PagesRead], Now.AddYears(1), Now); token.Revoke(Now.AddHours(1)); Assert.False(token.IsActive(Now.AddHours(2))); } [Fact] public void HasScope_is_true_only_for_a_granted_scope() { var token = ServiceAccountToken.Create(Guid.NewGuid(), "pub", "hash", [Scopes.PagesRead], null, Now); Assert.True(token.HasScope(Scopes.PagesRead)); Assert.False(token.HasScope(Scopes.PagesWrite)); } } public sealed class OutboxMessageTests { private static readonly DateTimeOffset Now = new(2026, 8, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public void Create_starts_unprocessed_and_immediately_available() { var message = OutboxMessage.Create("page.created.v1", "page", Guid.NewGuid().ToString(), "{}", Now); Assert.False(message.IsProcessed); Assert.Equal(Now, message.AvailableAt); Assert.Equal(0, message.Attempts); } [Fact] public void MarkFailed_increments_attempts_and_delays_the_next_availability() { var message = OutboxMessage.Create("page.created.v1", "page", Guid.NewGuid().ToString(), "{}", Now); message.MarkFailed("transient error", TimeSpan.FromMinutes(5), Now); Assert.Equal(1, message.Attempts); Assert.Equal(Now.AddMinutes(5), message.AvailableAt); Assert.False(message.IsProcessed); } [Fact] public void MarkProcessed_sets_the_processed_timestamp() { var message = OutboxMessage.Create("page.created.v1", "page", Guid.NewGuid().ToString(), "{}", Now); message.MarkProcessed(Now.AddSeconds(1)); Assert.True(message.IsProcessed); } }