using FluentAssertions; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class SentryPipelineContractTests { private static string RepoRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Api.SeaHavenIndustries", "Program.cs"))) { directory = directory.Parent; } if (directory is null) throw new InvalidOperationException("Could not locate the repository root from " + AppContext.BaseDirectory); return directory.FullName; } [Fact] public void Program_MiddlewareOrder_IsRouting_Authentication_SentryMetadata_Authorization() { var program = File.ReadAllText(Path.Combine(RepoRoot(), "Api.SeaHavenIndustries", "Program.cs")); var routing = program.IndexOf("app.UseRouting()", StringComparison.Ordinal); var authentication = program.IndexOf("app.UseAuthentication()", StringComparison.Ordinal); var sentry = program.IndexOf("app.UseMiddleware()", StringComparison.Ordinal); var authorization = program.IndexOf("app.UseAuthorization()", StringComparison.Ordinal); routing.Should().BeGreaterThan(-1, "UseRouting must be explicit"); authentication.Should().BeGreaterThan(-1); sentry.Should().BeGreaterThan(-1); authorization.Should().BeGreaterThan(-1); routing.Should().BeLessThan(authentication, "routing must precede authentication"); authentication.Should().BeLessThan(sentry, "Sentry request metadata must be applied after authentication"); sentry.Should().BeLessThan(authorization, "Sentry request metadata must be applied before authorization"); } [Fact] public void Program_ConfiguresReleaseAndDefaultTags() { var program = File.ReadAllText(Path.Combine(RepoRoot(), "Api.SeaHavenIndustries", "Program.cs")); program.Should().Contain("SentryObservability.ResolveRelease"); program.Should().Contain("options.Release = release"); program.Should().Contain("\"service\", SentryObservability.ServiceName"); program.Should().Contain("\"app.commit\""); program.Should().Contain("MaxRequestBodySize = RequestSize.None"); program.Should().Contain("SetBeforeSend(SentryTelemetryScrubber.Scrub)"); program.Should().Contain("SetBeforeSendTransaction(SentryTelemetryScrubber.Scrub)"); } [Theory] [InlineData("Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs", "vendor-documents.scan-pending")] [InlineData("Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs", "workorders.reconciliation-trigger")] [InlineData("Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs", "workorders.reconciliation-run-pending")] [InlineData("Api.SeaHavenIndustries/HostedServices/WorkOrderWeekRolledHostedService.cs", "workorders.week-rolled-job")] [InlineData("Api.SeaHavenIndustries/HostedServices/PastDueCacheHostedService.cs", "workorders.past-due-cache-job")] [InlineData("Api.SeaHavenIndustries/HostedServices/UpliftLifecycleHostedService.cs", "uplifts.lifecycle-sweep")] public void Workers_UseSharedBackgroundTransactionHelper_WithStableNames(string relativePath, string transactionName) { var source = File.ReadAllText(Path.Combine(RepoRoot(), relativePath)); source.Should().Contain($"BeginBackgroundTransaction", "worker transactions must go through the shared helper"); source.Should().Contain($"\"{transactionName}\""); source.Should().NotContain("PushScope", "scope pushing is owned by the shared helper"); } }