using Socialize.Api.Modules.Feedback.Data; using Socialize.Api.Modules.Feedback.Contracts; using Socialize.Api.Modules.Feedback.Services; using System.Text.Json; namespace Socialize.Tests.Feedback; public class FeedbackRulesTests { [Theory] [InlineData("Bug", FeedbackType.Bug)] [InlineData("suggestion", FeedbackType.Suggestion)] [InlineData("Request", FeedbackType.Request)] public void TryParseType_accepts_supported_types(string value, FeedbackType expected) { bool parsed = FeedbackRules.TryParseType(value, out FeedbackType type); Assert.True(parsed); Assert.Equal(expected, type); } [Theory] [InlineData("")] [InlineData("Question")] [InlineData("Incident")] public void TryParseType_rejects_unsupported_types(string value) { bool parsed = FeedbackRules.TryParseType(value, out _); Assert.False(parsed); } [Theory] [InlineData("New", FeedbackStatus.New)] [InlineData("Planned", FeedbackStatus.Planned)] [InlineData("Resolved", FeedbackStatus.Resolved)] [InlineData("Won't Do", FeedbackStatus.WontDo)] [InlineData("WontDo", FeedbackStatus.WontDo)] [InlineData("Cancelled", FeedbackStatus.Cancelled)] public void TryParseStatus_accepts_supported_statuses(string value, FeedbackStatus expected) { bool parsed = FeedbackRules.TryParseStatus(value, out FeedbackStatus status); Assert.True(parsed); Assert.Equal(expected, status); } [Fact] public void CanDeveloperSetStatus_rejects_cancelled_destination() { bool allowed = FeedbackRules.CanDeveloperSetStatus(FeedbackStatus.New, FeedbackStatus.Cancelled); Assert.False(allowed); } [Fact] public void CanDeveloperSetStatus_rejects_changes_after_cancelled() { bool allowed = FeedbackRules.CanDeveloperSetStatus(FeedbackStatus.Cancelled, FeedbackStatus.Planned); Assert.False(allowed); } [Fact] public void CanReporterCancel_rejects_cancelled_report() { bool allowed = FeedbackRules.CanReporterCancel(FeedbackStatus.Cancelled); Assert.False(allowed); } [Fact] public void CanReporterAccess_allows_report_owner() { Guid reporterUserId = Guid.NewGuid(); FeedbackReport report = new() { ReporterUserId = reporterUserId }; bool allowed = FeedbackAccessRules.CanReporterAccess(report, reporterUserId); Assert.True(allowed); } [Fact] public void CanReporterAccess_rejects_other_users() { FeedbackReport report = new() { ReporterUserId = Guid.NewGuid() }; bool allowed = FeedbackAccessRules.CanReporterAccess(report, Guid.NewGuid()); Assert.False(allowed); } [Fact] public void CanReporterCancel_requires_owner_and_non_final_status() { Guid reporterUserId = Guid.NewGuid(); FeedbackReport report = new() { ReporterUserId = reporterUserId, Status = FeedbackStatus.New, }; bool ownerAllowed = FeedbackAccessRules.CanReporterCancel(report, reporterUserId); bool otherUserAllowed = FeedbackAccessRules.CanReporterCancel(report, Guid.NewGuid()); Assert.True(ownerAllowed); Assert.False(otherUserAllowed); } [Fact] public void CanReporterComment_requires_report_owner() { Guid reporterUserId = Guid.NewGuid(); FeedbackReport report = new() { ReporterUserId = reporterUserId }; bool ownerAllowed = FeedbackAccessRules.CanReporterComment(report, reporterUserId); bool otherUserAllowed = FeedbackAccessRules.CanReporterComment(report, Guid.NewGuid()); Assert.True(ownerAllowed); Assert.False(otherUserAllowed); } [Fact] public void CanDeveloperComment_requires_developer_role() { Assert.True(FeedbackAccessRules.CanDeveloperComment(isDeveloper: true)); Assert.False(FeedbackAccessRules.CanDeveloperComment(isDeveloper: false)); } [Fact] public void CanAccessScreenshot_allows_report_owner() { Guid reporterUserId = Guid.NewGuid(); FeedbackReport report = new() { ReporterUserId = reporterUserId }; bool allowed = FeedbackAccessRules.CanAccessScreenshot(report, reporterUserId, isDeveloper: false); Assert.True(allowed); } [Fact] public void CanAccessScreenshot_allows_developer() { FeedbackReport report = new() { ReporterUserId = Guid.NewGuid() }; bool allowed = FeedbackAccessRules.CanAccessScreenshot(report, Guid.NewGuid(), isDeveloper: true); Assert.True(allowed); } [Fact] public void CanAccessScreenshot_rejects_unrelated_non_developer() { FeedbackReport report = new() { ReporterUserId = Guid.NewGuid() }; bool allowed = FeedbackAccessRules.CanAccessScreenshot(report, Guid.NewGuid(), isDeveloper: false); Assert.False(allowed); } [Theory] [InlineData("image/png")] [InlineData("image/jpeg")] [InlineData("image/jpg")] public void Screenshot_content_type_allows_png_and_jpeg(string contentType) { bool allowed = FeedbackScreenshotRules.IsAllowedContentType(contentType); Assert.True(allowed); } [Theory] [InlineData("text/html")] [InlineData("application/pdf")] [InlineData("")] public void Screenshot_content_type_rejects_non_images(string contentType) { bool allowed = FeedbackScreenshotRules.IsAllowedContentType(contentType); Assert.False(allowed); } [Theory] [InlineData(1)] [InlineData(FeedbackScreenshotRules.MaxScreenshotBytes)] public void Screenshot_size_allows_non_empty_files_up_to_limit(long sizeBytes) { bool allowed = FeedbackScreenshotRules.IsAllowedSize(sizeBytes); Assert.True(allowed); } [Theory] [InlineData(0)] [InlineData(FeedbackScreenshotRules.MaxScreenshotBytes + 1)] public void Screenshot_size_rejects_empty_and_oversized_files(long sizeBytes) { bool allowed = FeedbackScreenshotRules.IsAllowedSize(sizeBytes); Assert.False(allowed); } [Fact] public void NormalizeTags_trims_deduplicates_and_orders() { IReadOnlyCollection tags = FeedbackRules.NormalizeTags([" mobile ", "bug", "Mobile", ""]); Assert.Equal(["bug", "mobile"], tags); } [Fact] public void Feedback_report_dto_returns_mixed_timeline_in_created_order() { Guid reportId = Guid.NewGuid(); DateTimeOffset now = DateTimeOffset.UtcNow; FeedbackReport report = new() { Id = reportId, Type = FeedbackType.Bug, Status = FeedbackStatus.New, Description = "Broken layout", ReporterUserId = Guid.NewGuid(), ReporterDisplayName = "Reporter", ReporterEmail = "reporter@example.com", SubmittedPath = "/app/example", CreatedAt = now, LastActivityAt = now.AddMinutes(2), }; report.ActivityEntries.Add(new FeedbackActivityEntry { Id = Guid.NewGuid(), FeedbackReportId = reportId, ActorUserId = Guid.NewGuid(), ActorDisplayName = "Developer", ActorEmail = "dev@example.com", ActivityType = FeedbackActivityTypes.StatusChanged, FromValue = "New", ToValue = "Planned", CreatedAt = now.AddMinutes(2), }); report.Comments.Add(new FeedbackComment { Id = Guid.NewGuid(), FeedbackReportId = reportId, AuthorUserId = report.ReporterUserId, AuthorDisplayName = "Reporter", AuthorEmail = "reporter@example.com", AuthorRole = "Reporter", Body = "More context", CreatedAt = now.AddMinutes(1), }); FeedbackReportDto dto = report.ToDto(); Assert.Equal(["Comment", "Activity"], dto.Timeline.Select(item => item.Kind).ToArray()); Assert.Equal("More context", dto.Timeline.First().Body); Assert.Equal(FeedbackActivityTypes.StatusChanged, dto.Timeline.Last().ActivityType); } [Fact] public void Feedback_notification_metadata_includes_target_route() { Guid reportId = Guid.NewGuid(); string developerMetadata = FeedbackNotificationRoutes.BuildMetadataJson(reportId, developerRoute: true); string reporterMetadata = FeedbackNotificationRoutes.BuildMetadataJson(reportId, developerRoute: false); using JsonDocument developerDocument = JsonDocument.Parse(developerMetadata); using JsonDocument reporterDocument = JsonDocument.Parse(reporterMetadata); Assert.Equal($"/app/feedback/{reportId}", developerDocument.RootElement.GetProperty("route").GetString()); Assert.Equal($"/app/my-feedback/{reportId}", reporterDocument.RootElement.GetProperty("route").GetString()); Assert.True(developerDocument.RootElement.GetProperty("isFeedbackNotification").GetBoolean()); } }