Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/Grace.Server.Unit.Tests/Eventing.Server.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,41 @@ type AutomationEventingTests() =
let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.ReferenceEvent referenceEvent)
Assert.That(envelope.IsNone, Is.True)

/// Verifies that current public visibility never replays old private terminal reference creation automation.
[<Test>]
member _.CurrentPublicTerminalReferenceDoesNotReplayPrivateCreatedAutomationProjection() =
let repositoryId = Guid.NewGuid()
let referenceId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let branchId = Guid.NewGuid()

let referenceEvent: ReferenceEvent =
{
Event =
ReferenceEventType.Created(
referenceId,
Guid.NewGuid(),
Guid.NewGuid(),
repositoryId,
branchId,
Guid.NewGuid(),
Sha256Hash String.Empty,
Blake3Hash String.Empty,
ReferenceType.Promotion,
"promotion",
[
ReferenceLinkType.IncludedInPromotionSet promotionSetId
ReferenceLinkType.PromotionSetTerminal promotionSetId
]
)
Metadata =
metadata "corr-current-public-private-terminal" repositoryId
|> markPrivateInheritedReference
}

let envelope = EventingPublisher.tryCreateEnvelopeWithCurrentVisibility (Some true) (GraceEvent.ReferenceEvent referenceEvent)
Assert.That(envelope.IsNone, Is.True)

/// Verifies that terminal reference reveal maps to public PromotionSet applied automation.
[<Test>]
member _.TerminalReferenceRevealMapsToPromotionSetApplied() =
Expand Down Expand Up @@ -410,6 +445,50 @@ type AutomationEventingTests() =
let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.PromotionSetEvent promotionSetEvent)
Assert.That(envelope.IsNone, Is.True)

/// Verifies that current source visibility can suppress stale public event metadata before automation fanout.
[<Test>]
member _.CurrentHiddenPromotionSetSuppressesPublicAutomationProjection() =
let repositoryId = Guid.NewGuid()
let metadata = metadata "corr-current-hidden-apply" repositoryId
metadata.Properties[ "ActorId" ] <- $"{Guid.NewGuid()}"

let promotionSetEvent: PromotionSetEvent = { Event = PromotionSetEventType.Applied(Guid.NewGuid()); Metadata = metadata }

let envelope = EventingPublisher.tryCreateEnvelopeWithCurrentVisibility (Some false) (GraceEvent.PromotionSetEvent promotionSetEvent)

Assert.That(envelope.IsNone, Is.True)

/// Verifies that missing current PromotionSet snapshots fall back to durable public event metadata.
[<Test>]
member _.MissingCurrentPromotionSetSnapshotAllowsPublicPromotionSetAppliedMetadata() =
let repositoryId = Guid.NewGuid()
let metadata = metadata "corr-missing-current-public-apply" repositoryId
metadata.Properties[ "ActorId" ] <- $"{Guid.NewGuid()}"

let promotionSetEvent: PromotionSetEvent = { Event = PromotionSetEventType.Applied(Guid.NewGuid()); Metadata = metadata }

let envelope = EventingPublisher.tryCreateEnvelopeWithCurrentVisibility Option.None (GraceEvent.PromotionSetEvent promotionSetEvent)

Assert.That(envelope.IsSome, Is.True)
Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.PromotionSetApplied))

/// Verifies that current public visibility never replays old private PromotionSet apply metadata.
[<Test>]
member _.CurrentPublicPromotionSetDoesNotReplayPrivateAutomationProjection() =
let repositoryId = Guid.NewGuid()

let promotionSetEvent: PromotionSetEvent =
{
Event = PromotionSetEventType.Applied(Guid.NewGuid())
Metadata =
metadata "corr-current-public-private-apply" repositoryId
|> markPrivateContributorOwned
}

let envelope = EventingPublisher.tryCreateEnvelopeWithCurrentVisibility (Some true) (GraceEvent.PromotionSetEvent promotionSetEvent)

Assert.That(envelope.IsNone, Is.True)

/// Verifies that private validation results do not emit public automation events.
[<Test>]
member _.PrivateValidationResultDoesNotMapToAutomationEvent() =
Expand Down Expand Up @@ -480,6 +559,20 @@ type AutomationEventingTests() =
Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.AgentSummaryAdded))
Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))

/// Verifies that add-summary artifact links can inherit current PromotionSet suppression.
[<Test>]
member _.WorkItemArtifactLinkedWithPromotionSetScopeCanBeSuppressedByCurrentHiddenProjection() =
let repositoryId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let eventMetadata = metadata "corr-work-item-summary-hidden-promotion-set" repositoryId
eventMetadata.Properties[ nameof PromotionSetId ] <- $"{promotionSetId}"

let workItemEvent: WorkItemEvent = { Event = WorkItemEventType.ArtifactLinked(Guid.NewGuid()); Metadata = eventMetadata }

let envelope = EventingPublisher.tryCreateEnvelopeWithCurrentVisibility (Some false) (GraceEvent.WorkItemEvent workItemEvent)

Assert.That(envelope.IsNone, Is.True)

/// Verifies that private work-item PromotionSet link events do not emit public automation events.
[<Test>]
member _.PrivateWorkItemPromotionSetLinkDoesNotMapToAutomationEvent() =
Expand Down
135 changes: 135 additions & 0 deletions src/Grace.Server.Unit.Tests/Notification.Server.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,13 +1,40 @@
namespace Grace.Server.Tests

open Grace.Server.Notification
open Grace.Types
open Grace.Types.Events
open Grace.Types.Queue
open Grace.Types.Common
open Grace.Types.Reference
open Grace.Types.Visibility
open NodaTime
open NUnit.Framework
open System
open System.Collections.Generic

/// Covers notification Server behavior in no-Aspire server unit tests.
[<Parallelizable(ParallelScope.All)>]
type NotificationServerTests() =

/// Constructs metadata fixtures used by the notification projection assertions.
let metadata correlationId repositoryId =
let properties = Dictionary<string, string>()
properties[nameof RepositoryId] <- $"{repositoryId}"

{
Timestamp = Instant.FromUtc(2026, 7, 9, 10, 30)
CorrelationId = correlationId
Principal = "tester"
ClientType = Microsoft.FSharp.Core.Option.None
Properties = properties
}

/// Marks an event fixture as private at creation time.
let markPrivateContributorOwned (metadata: EventMetadata) =
metadata.Properties[ "Visibility" ] <- $"{ResourceVisibility.Private}"
metadata.Properties[ "Ownership" ] <- $"{ResourceOwnership.ContributorOwned}"
metadata

/// Verifies that branch Name Glob Matching Is Case Insensitive And Supports Wildcard.
[<TestCase("main", "main", true)>]
[<TestCase("MAIN", "main", true)>]
Expand All @@ -19,3 +46,111 @@ type NotificationServerTests() =
member _.BranchNameGlobMatchingIsCaseInsensitiveAndSupportsWildcard(branchName: string, glob: string, expected: bool) =
let actual = Subscriber.matchesBranchGlob (BranchName branchName) glob
Assert.That(actual, Is.EqualTo(expected))

/// Verifies that SignalR reference notification fanout is suppressed for currently hidden references.
[<Test>]
member _.ReferenceNotificationProjectionSuppressesCurrentHiddenReference() = Assert.That(Subscriber.shouldNotifyReferenceProjection (Some false), Is.False)

/// Verifies that SignalR reference notification fanout remains enabled when no current source override is available.
[<Test>]
member _.ReferenceNotificationProjectionAllowsUnscopedLegacyReference() = Assert.That(Subscriber.shouldNotifyReferenceProjection None, Is.True)

/// Verifies that deleted references are treated as hidden for public projection fanout.
[<Test>]
member _.ReferenceProjectionSuppressesDeletedPublicReference() =
let referenceDto =
{ ReferenceDto.Default with
ReferenceId = System.Guid.NewGuid()
Visibility = ResourceVisibility.Public
Ownership = ResourceOwnership.RepositoryOwned
DeletedAt = Some(Instant.FromUtc(2026, 7, 9, 10, 0))
}

Assert.That(Subscriber.referenceDtoAllowsPublicProjection referenceDto, Is.False)

/// Verifies that derived quick-scan fanout observes the current reference projection decision.
[<Test>]
member _.DerivedReferenceProjectionSuppressesCurrentHiddenReference() =
Assert.That(Subscriber.shouldRecordDerivedReferenceProjection (Some false), Is.False)

/// Verifies that derived quick-scan fanout remains available when no current reference override is available.
[<Test>]
member _.DerivedReferenceProjectionAllowsUnscopedReference() = Assert.That(Subscriber.shouldRecordDerivedReferenceProjection None, Is.True)

/// Verifies that private reference creation metadata stays suppressed even after the current reference is public.
[<Test>]
member _.PrivateReferenceCreatedProjectionStaysSuppressedAfterReveal() =
let eventMetadata =
metadata "corr-private-created-after-reveal" (Guid.NewGuid())
|> markPrivateContributorOwned

Assert.That(Subscriber.shouldNotifyReferenceCreatedProjection eventMetadata (Some true), Is.False)
Assert.That(Subscriber.shouldRecordDerivedReferenceCreatedProjection eventMetadata (Some true), Is.False)

/// Verifies that public reference creation still publishes when the current reference is public.
[<Test>]
member _.PublicReferenceCreatedProjectionAllowsCurrentPublicReference() =
let eventMetadata = metadata "corr-public-created" (Guid.NewGuid())

Assert.That(Subscriber.shouldNotifyReferenceCreatedProjection eventMetadata (Some true), Is.True)
Assert.That(Subscriber.shouldRecordDerivedReferenceCreatedProjection eventMetadata (Some true), Is.True)

/// Verifies that PromotionSet created events carry their own source id for projection rechecks.
[<Test>]
member _.PromotionSetCreatedProjectionIdUsesCreatedEventSource() =
let repositoryId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()

let promotionSetEvent: PromotionSet.PromotionSetEvent =
{
Event =
PromotionSet.PromotionSetEventType.Created(
promotionSetId,
Guid.NewGuid(),
Guid.NewGuid(),
repositoryId,
Guid.NewGuid(),
ResourceVisibility.Public,
ResourceOwnership.RepositoryOwned,
Some(UserId "creator@example.test")
)
Metadata = metadata "corr-promotion-set-created" repositoryId
}

let projectionId = Subscriber.tryGetPromotionSetProjectionId (GraceEvent.PromotionSetEvent promotionSetEvent)
Assert.That(projectionId, Is.EqualTo(Some promotionSetId))

/// Verifies that non-create PromotionSet events derive source scope from actor identity metadata.
[<Test>]
member _.PromotionSetNonCreateProjectionIdUsesActorIdWhenScopeMetadataIsMissing() =
let repositoryId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let eventMetadata = metadata "corr-promotion-set-applied" repositoryId
eventMetadata.Properties[ "ActorId" ] <- $"{promotionSetId}"

let promotionSetEvent: PromotionSet.PromotionSetEvent = { Event = PromotionSet.PromotionSetEventType.Applied(Guid.NewGuid()); Metadata = eventMetadata }

let projectionId = Subscriber.tryGetPromotionSetProjectionId (GraceEvent.PromotionSetEvent promotionSetEvent)
Assert.That(projectionId, Is.EqualTo(Some promotionSetId))

/// Verifies that non-create PromotionSet events without source scope remain unscoped for suppression.
[<Test>]
member _.PromotionSetNonCreateProjectionIdIsMissingWithoutScopeMetadata() =
let repositoryId = Guid.NewGuid()
let eventMetadata = metadata "corr-promotion-set-missing-scope" repositoryId

let promotionSetEvent: PromotionSet.PromotionSetEvent = { Event = PromotionSet.PromotionSetEventType.ApplyStarted; Metadata = eventMetadata }

let projectionId = Subscriber.tryGetPromotionSetProjectionId (GraceEvent.PromotionSetEvent promotionSetEvent)
Assert.That(projectionId, Is.EqualTo(Option.None))

/// Verifies that retry-like queue events carry PromotionSet scope directly from the event payload.
[<Test>]
member _.PromotionQueueProjectionIdUsesEventPromotionSetId() =
let promotionSetId = Guid.NewGuid()

let queueEvent: PromotionQueueEvent =
{ Event = PromotionQueueEventType.PromotionSetEnqueued promotionSetId; Metadata = metadata "corr-queue-scope" (Guid.NewGuid()) }

let projectionId = Subscriber.tryGetPromotionSetProjectionId (GraceEvent.QueueEvent queueEvent)
Assert.That(projectionId, Is.EqualTo(Some promotionSetId))
99 changes: 99 additions & 0 deletions src/Grace.Server.Unit.Tests/WebhookDispatch.Server.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ module private WebhookDispatchTestHelpers =
let dispatch transport graceEvent =
WebhookDispatch.dispatchCommittedEventAsync logger configuration hostEnvironment transport graceEvent CancellationToken.None

/// Builds dispatch test data with a current source visibility decision for stale-projection assertions.
let dispatchWithCurrentVisibility currentSourceAllowsProjection transport graceEvent =
WebhookDispatch.dispatchCommittedEventAsyncWithCurrentVisibility
logger
configuration
hostEnvironment
transport
currentSourceAllowsProjection
graceEvent
CancellationToken.None

/// Covers webhook Dispatch Unit behavior in no-Aspire server unit tests.
[<NonParallelizable>]
type WebhookDispatchUnitTests() =
Expand Down Expand Up @@ -355,6 +366,94 @@ type WebhookDispatchUnitTests() =
Assert.That(deliveries, Is.Empty)
}

/// Verifies that stale public apply metadata cannot publish a webhook when the source is currently hidden.
[<Test>]
member _.CurrentHiddenPromotionSetAppliedDoesNotCreateWebhookDelivery() =
task {
let ownerId = Guid.NewGuid()
let organizationId = Guid.NewGuid()
let repositoryId = Guid.NewGuid()
let targetBranchId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let terminalReferenceId = Guid.NewGuid()
let rule = WebhookDispatchTestHelpers.rule (Guid.NewGuid()) ownerId organizationId repositoryId (Option.Some targetBranchId)
WebhookStore.upsertRule rule |> ignore

let transport = WebhookDispatchTestHelpers.RecordingTransport([ OutboundWebhookResult.Succeeded 202 ])

let! result =
WebhookDispatchTestHelpers.dispatchWithCurrentVisibility
(Some false)
transport
(WebhookDispatchTestHelpers.appliedEvent ownerId organizationId repositoryId targetBranchId promotionSetId terminalReferenceId)

Assert.That(result.DeliveryCount, Is.EqualTo(0))
Assert.That(result.DeliveredCount, Is.EqualTo(0))
Assert.That(transport.Requests, Is.Empty)

let deliveries = WebhookStore.listDeliveries rule.Scope rule.WebhookRuleId true
Assert.That(deliveries, Is.Empty)
}

/// Verifies that missing current PromotionSet snapshots still allow public apply metadata to create webhook delivery.
[<Test>]
member _.MissingCurrentPromotionSetSnapshotAllowsPublicWebhookDelivery() =
task {
let ownerId = Guid.NewGuid()
let organizationId = Guid.NewGuid()
let repositoryId = Guid.NewGuid()
let targetBranchId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let terminalReferenceId = Guid.NewGuid()
let rule = WebhookDispatchTestHelpers.rule (Guid.NewGuid()) ownerId organizationId repositoryId (Option.Some targetBranchId)
WebhookStore.upsertRule rule |> ignore

let transport = WebhookDispatchTestHelpers.RecordingTransport([ OutboundWebhookResult.Succeeded 202 ])

let! result =
WebhookDispatchTestHelpers.dispatchWithCurrentVisibility
Option.None
transport
(WebhookDispatchTestHelpers.appliedEvent ownerId organizationId repositoryId targetBranchId promotionSetId terminalReferenceId)

Assert.That(result.DeliveryCount, Is.EqualTo(1))
Assert.That(result.DeliveredCount, Is.EqualTo(1))
Assert.That(transport.Requests, Has.Length.EqualTo(1))

let deliveries = WebhookStore.listDeliveries rule.Scope rule.WebhookRuleId true
Assert.That(deliveries, Has.Count.EqualTo(1))
Assert.That(deliveries[0].Status, Is.EqualTo(WebhookDeliveryStatus.Succeeded))
}

/// Verifies that current public visibility does not replay a private apply event as a public webhook.
[<Test>]
member _.CurrentPublicPromotionSetDoesNotReplayPrivateWebhookDelivery() =
task {
let ownerId = Guid.NewGuid()
let organizationId = Guid.NewGuid()
let repositoryId = Guid.NewGuid()
let targetBranchId = Guid.NewGuid()
let promotionSetId = Guid.NewGuid()
let terminalReferenceId = Guid.NewGuid()
let rule = WebhookDispatchTestHelpers.rule (Guid.NewGuid()) ownerId organizationId repositoryId (Option.Some targetBranchId)
WebhookStore.upsertRule rule |> ignore

let transport = WebhookDispatchTestHelpers.RecordingTransport([ OutboundWebhookResult.Succeeded 202 ])

let! result =
WebhookDispatchTestHelpers.dispatchWithCurrentVisibility
(Some true)
transport
(WebhookDispatchTestHelpers.privateAppliedEvent ownerId organizationId repositoryId targetBranchId promotionSetId terminalReferenceId)

Assert.That(result.DeliveryCount, Is.EqualTo(0))
Assert.That(result.DeliveredCount, Is.EqualTo(0))
Assert.That(transport.Requests, Is.Empty)

let deliveries = WebhookStore.listDeliveries rule.Scope rule.WebhookRuleId true
Assert.That(deliveries, Is.Empty)
}

/// Verifies that terminal reference reveal publishes private PromotionSet apply exactly once.
[<Test>]
member _.TerminalReferenceRevealPublishesPrivatePromotionSetAppliedOnce() =
Expand Down
Loading