using Socialize.Api.Modules.ReleaseCommunications.Contracts; using Socialize.Api.Modules.ReleaseCommunications.Data; using Socialize.Api.Modules.ReleaseCommunications.Services; namespace Socialize.Tests.ReleaseCommunications; public class ReleaseUpdateRulesTests { [Theory] [InlineData("Feature", ReleaseUpdateCategory.Feature)] [InlineData("improvement", ReleaseUpdateCategory.Improvement)] [InlineData("Breaking Change", ReleaseUpdateCategory.BreakingChange)] [InlineData("BreakingChange", ReleaseUpdateCategory.BreakingChange)] internal void TryParseCategory_accepts_supported_categories(string value, ReleaseUpdateCategory expected) { bool parsed = ReleaseUpdateRules.TryParseCategory(value, out ReleaseUpdateCategory category); Assert.True(parsed); Assert.Equal(expected, category); } [Theory] [InlineData("")] [InlineData("Security")] [InlineData("Maintenance")] public void TryParseCategory_rejects_unsupported_categories(string value) { bool parsed = ReleaseUpdateRules.TryParseCategory(value, out _); Assert.False(parsed); } [Theory] [InlineData("Normal", ReleaseUpdateImportance.Normal)] [InlineData("important", ReleaseUpdateImportance.Important)] internal void TryParseImportance_accepts_supported_importance(string value, ReleaseUpdateImportance expected) { bool parsed = ReleaseUpdateRules.TryParseImportance(value, out ReleaseUpdateImportance importance); Assert.True(parsed); Assert.Equal(expected, importance); } [Theory] [InlineData("Everyone", ReleaseUpdateAudience.Everyone)] [InlineData("Organization Owners", ReleaseUpdateAudience.OrganizationOwners)] [InlineData("developers", ReleaseUpdateAudience.Developers)] internal void TryParseAudience_accepts_supported_audiences(string value, ReleaseUpdateAudience expected) { bool parsed = ReleaseUpdateRules.TryParseAudience(value, out ReleaseUpdateAudience audience); Assert.True(parsed); Assert.Equal(expected, audience); } [Fact] public void ToDto_formats_breaking_change_category_for_display() { ReleaseUpdate update = new() { Id = Guid.NewGuid(), Title = "API change", Summary = "A workflow API changed.", Category = ReleaseUpdateCategory.BreakingChange, Importance = ReleaseUpdateImportance.Important, Audience = ReleaseUpdateAudience.Developers, Status = ReleaseUpdateStatus.Published, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, PublishedAt = DateTimeOffset.UtcNow, CreatedByUserId = Guid.NewGuid(), }; ReleaseUpdateDto dto = update.ToDto(isRead: true); Assert.Equal("Breaking Change", dto.Category); Assert.True(dto.IsRead); } [Fact] public void VisibleTo_returns_everyone_updates_for_any_authenticated_user() { ReleaseUpdate update = NewPublishedUpdate(ReleaseUpdateAudience.Everyone); List visibleUpdates = new[] { update } .AsQueryable() .VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: false, IsOrganizationOwner: false)) .ToList(); Assert.Same(update, Assert.Single(visibleUpdates)); } [Fact] public void VisibleTo_rejects_unpublished_updates() { ReleaseUpdate update = NewPublishedUpdate(ReleaseUpdateAudience.Everyone); update.Status = ReleaseUpdateStatus.Draft; List visibleUpdates = new[] { update } .AsQueryable() .VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: true, IsOrganizationOwner: true)) .ToList(); Assert.Empty(visibleUpdates); } [Fact] public void VisibleTo_requires_matching_restricted_audience() { ReleaseUpdate ownerUpdate = NewPublishedUpdate(ReleaseUpdateAudience.OrganizationOwners); ReleaseUpdate developerUpdate = NewPublishedUpdate(ReleaseUpdateAudience.Developers); List ownerVisibleUpdates = new[] { ownerUpdate, developerUpdate } .AsQueryable() .VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: false, IsOrganizationOwner: true)) .ToList(); List developerVisibleUpdates = new[] { ownerUpdate, developerUpdate } .AsQueryable() .VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: true, IsOrganizationOwner: false)) .ToList(); Assert.Same(ownerUpdate, Assert.Single(ownerVisibleUpdates)); Assert.Same(developerUpdate, Assert.Single(developerVisibleUpdates)); } [Fact] public void CreateMissingReadReceipts_creates_receipts_only_for_unread_visible_updates() { Guid userId = Guid.NewGuid(); Guid unreadUpdateId = Guid.NewGuid(); Guid readUpdateId = Guid.NewGuid(); DateTimeOffset readAt = DateTimeOffset.UtcNow; IReadOnlyCollection receipts = ReleaseUpdateReadState.CreateMissingReadReceipts( userId, [unreadUpdateId, readUpdateId], new HashSet { readUpdateId }, readAt); ReleaseUpdateReadReceipt receipt = Assert.Single(receipts); Assert.Equal(unreadUpdateId, receipt.ReleaseUpdateId); Assert.Equal(userId, receipt.UserId); Assert.Equal(readAt, receipt.ReadAt); } [Fact] public void IsInactive_allows_never_authenticated_and_old_activity() { DateTimeOffset inactiveBefore = DateTimeOffset.UtcNow.AddHours(-24); Assert.True(ReleaseUpdateEmailRules.IsInactive(null, inactiveBefore)); Assert.True(ReleaseUpdateEmailRules.IsInactive(inactiveBefore.AddMinutes(-1), inactiveBefore)); } [Fact] public void IsInactive_rejects_recent_activity() { DateTimeOffset inactiveBefore = DateTimeOffset.UtcNow.AddHours(-24); Assert.False(ReleaseUpdateEmailRules.IsInactive(inactiveBefore.AddMinutes(1), inactiveBefore)); } [Fact] public void CanSendDigest_enforces_send_interval() { DateTimeOffset lastSentBefore = DateTimeOffset.UtcNow.AddHours(-24); Assert.True(ReleaseUpdateEmailRules.CanSendDigest(null, lastSentBefore)); Assert.True(ReleaseUpdateEmailRules.CanSendDigest(lastSentBefore.AddMinutes(-1), lastSentBefore)); Assert.False(ReleaseUpdateEmailRules.CanSendDigest(lastSentBefore.AddMinutes(1), lastSentBefore)); } private static ReleaseUpdate NewPublishedUpdate(ReleaseUpdateAudience audience) { return new ReleaseUpdate { Id = Guid.NewGuid(), Title = "Update", Summary = "Something changed.", Category = ReleaseUpdateCategory.Improvement, Importance = ReleaseUpdateImportance.Normal, Audience = audience, Status = ReleaseUpdateStatus.Published, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, PublishedAt = DateTimeOffset.UtcNow, CreatedByUserId = Guid.NewGuid(), }; } }