Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,13 @@ public class DownlinkReceivedNotificationHandlerTests
{
static DownlinkReceivedNotificationHandler BuildHandler(
TestAircraftRepository aircraftRepository,
TestClientManager clientManager,
TestMessageIdProvider messageIdProvider,
IMediator mediator,
TestClock clock,
TestControllerRepository controllerRepository,
TestDialogueRepository dialogueRepository,
IHubContext<ControllerHub> hubContext) =>
new(
aircraftRepository,
clientManager,
messageIdProvider,
mediator,
clock,
controllerRepository,
Expand Down Expand Up @@ -65,10 +61,8 @@ public async Task Handle_PublishesDialogueChangedNotification()
hubContext.Clients.Clients(Arg.Any<IReadOnlyList<string>>()).Returns(clientProxy);

var dialogueRepository = new TestDialogueRepository();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftManager, clientManager, messageIdProvider, mediator, clock, controllerManager, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftManager, mediator, clock, controllerManager, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -124,10 +118,8 @@ public async Task Handle_StillCreatesDialogueWhenNoControllersMatch()
hubContext.Clients.Clients(Arg.Any<IReadOnlyList<string>>()).Returns(clientProxy);

var dialogueRepository = new TestDialogueRepository();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftManager, clientManager, messageIdProvider, mediator, clock, controllerManager, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftManager, mediator, clock, controllerManager, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -179,10 +171,8 @@ public async Task Handle_PromotesAircraftToCurrentDataAuthorityOnFirstDownlink()
hubContext.Clients.Clients(Arg.Any<IReadOnlyList<string>>()).Returns(clientProxy);

var dialogueRepository = new TestDialogueRepository();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftManager, clientManager, messageIdProvider, mediator, clock, controllerManager, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftManager, mediator, clock, controllerManager, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -253,10 +243,8 @@ public async Task Handle_DoesNotPromoteToCurrentDataAuthority_WhenNotCurrentData
hubContext.Clients.Clients(Arg.Any<IReadOnlyList<string>>()).Returns(clientProxy);

var dialogueRepository = new TestDialogueRepository();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftManager, clientManager, messageIdProvider, mediator, clock, controllerManager, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftManager, mediator, clock, controllerManager, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -321,10 +309,8 @@ public async Task Handle_UpdatesLastSeen()
clock.SetUtcNow(expectedLastSeen);

var dialogueRepository = new TestDialogueRepository();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftManager, clientManager, messageIdProvider, mediator, clock, controllerManager, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftManager, mediator, clock, controllerManager, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -365,10 +351,8 @@ public async Task Handle_CreatesNewDialogue_ForDownlinkWithNoReference()
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -409,8 +393,6 @@ public async Task Handle_AppendsToExistingDialogue_ForDownlinkWithReference()
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

// Create existing dialogue with an uplink
var existingDialogue = new Dialogue("UAL123");
Expand All @@ -425,7 +407,7 @@ public async Task Handle_AppendsToExistingDialogue_ForDownlinkWithReference()
clock.UtcNow());
await dialogueRepository.Add(existingDialogue, CancellationToken.None);

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
10,
Expand Down Expand Up @@ -468,10 +450,8 @@ public async Task Handle_FallsBackToNewDialogue_WhenNoOpenDialogueMatchesReferen
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

// Downlink has a MessageReference (uplink ID=99) but no open dialogue exists for it
var downlink = new ReceivedDownlink(
Expand Down Expand Up @@ -518,8 +498,6 @@ public async Task Handle_DoesNotAppendToClosedDialogue_WhenAircraftReusesMessage
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

// Prior session: uplink ID=5 was sent and fully replied to (dialogue is now closed)
var priorDialogue = new Dialogue("UAL123");
Expand All @@ -535,7 +513,7 @@ public async Task Handle_DoesNotAppendToClosedDialogue_WhenAircraftReusesMessage

Assert.True(priorDialogue.IsClosed);

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

// New session: aircraft re-uses MessageReference=5 (same uplink ID as in the old session)
var downlink = new ReceivedDownlink(
Expand Down Expand Up @@ -578,10 +556,8 @@ public async Task Handle_LogoffMessage_TerminatesConnectionAndCreatesDialogue()
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1, null, "UAL123",
Expand Down Expand Up @@ -620,10 +596,8 @@ public async Task Handle_LogonRequest_DispatchesLogonCommand()
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
1,
Expand Down Expand Up @@ -655,21 +629,19 @@ await mediator.Received(1).Send(
}

[Fact]
public async Task Handle_UnknownAircraft_CreatesDialogueAndTransmitsError()
public async Task Handle_UnknownAircraft_IgnoresDownlink()
{
// Branch 2: a downlink arrives from an aircraft we have no connection for. The handler
// creates a dialogue (downlink + system error uplink) and transmits the error via ACARS.
// ignores it. We do not send messages to an aircraft that has no connection.
// Arrange
var clock = new TestClock();
var aircraftRepository = new TestAircraftRepository();
var controllerRepository = new TestControllerRepository();
var dialogueRepository = new TestDialogueRepository();
var mediator = Substitute.For<IMediator>();
var hubContext = Substitute.For<IHubContext<ControllerHub>>();
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();

var handler = BuildHandler(aircraftRepository, clientManager, messageIdProvider, mediator, clock, controllerRepository, dialogueRepository, hubContext);
var handler = BuildHandler(aircraftRepository, mediator, clock, controllerRepository, dialogueRepository, hubContext);

var downlink = new ReceivedDownlink(
7,
Expand All @@ -685,25 +657,10 @@ public async Task Handle_UnknownAircraft_CreatesDialogueAndTransmitsError()
// Act
await handler.Handle(notification, CancellationToken.None);

// Assert - dialogue contains both the original downlink and the error uplink
// Assert - no dialogue created and nothing published
var dialogues = await dialogueRepository.All(CancellationToken.None);
Assert.Single(dialogues);
Assert.Equal("GHOST1", dialogues[0].AircraftCallsign);
Assert.Equal(2, dialogues[0].Messages.Count);

var receivedDownlink = Assert.IsType<DownlinkMessage>(dialogues[0].Messages[0]);
Assert.Equal(7, receivedDownlink.MessageId);

var errorUplink = Assert.IsType<UplinkMessage>(dialogues[0].Messages[1]);
Assert.Equal("ERROR. CONNECTION NOT ESTABLISHED.", errorUplink.Content);
Assert.Equal("GHOST1", errorUplink.Recipient);
Assert.Equal(7, errorUplink.MessageReference);

// Assert - error uplink transmitted via the ACARS client
var client = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ybbb", CancellationToken.None);
Assert.Single(client.SentMessages);
Assert.Equal("ERROR. CONNECTION NOT ESTABLISHED.", client.SentMessages[0].Content);
Assert.Empty(dialogues);

await mediator.Received(1).Publish(Arg.Any<DialogueChangedNotification>(), Arg.Any<CancellationToken>());
await mediator.DidNotReceive().Publish(Arg.Any<DialogueChangedNotification>(), Arg.Any<CancellationToken>());
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
using CPDLCServer.Clients;
using CPDLCServer.Hubs;
using CPDLCServer.Infrastructure;
using CPDLCServer.Messages;
using CPDLCServer.Model;
using CPDLCServer.Persistence;
using CPDLCServer.Services;
using MediatR;
using Microsoft.AspNetCore.SignalR;

namespace CPDLCServer.Handlers;

public class DownlinkReceivedNotificationHandler(
IAircraftRepository aircraftRepository,
IClientManager clientManager,
IMessageIdProvider messageIdProvider,
IMediator mediator,
IClock clock,
IControllerRepository controllerRepository,
Expand Down Expand Up @@ -51,8 +47,7 @@ await mediator.Send(
// Scenario 2: Unknown aircraft
if (aircraftConnection is null)
{
logger.Information("{Callsign} is not known by this ATSU, sending error uplink", downlink.Sender);
await SendUnknownAircraftError(notification, downlink, cancellationToken);
logger.Information("{Callsign} is not known by this ATSU, ignoring downlink", downlink.Sender);
return;
}

Expand Down Expand Up @@ -143,44 +138,4 @@ await hubContext.Clients
await mediator.Publish(new DialogueChangedNotification(dialogue), cancellationToken);
}
}

async Task SendUnknownAircraftError(
DownlinkReceivedNotification notification,
ReceivedDownlink downlink,
CancellationToken cancellationToken)
{
var dialogue = new Dialogue(downlink.Sender);
dialogue.AddDownlink(
downlink.MessageId,
downlink.MessageReference,
downlink.Sender,
downlink.ResponseType,
downlink.AlertType,
downlink.Content,
downlink.Received);

var messageId = await messageIdProvider.GetNextMessageId(
notification.AcarsClientId,
downlink.Sender,
cancellationToken);

var uplinkMessage = dialogue.AddUplink(
messageId,
downlink.MessageId,
downlink.Sender,
"SYSTEM",
CpdlcUplinkResponseType.NoResponse,
AlertType.None,
"ERROR. CONNECTION NOT ESTABLISHED.",
clock.UtcNow());

await dialogueRepository.Add(dialogue, cancellationToken);
logger.Information("Dialogue {DialogueId} created for unknown aircraft {Callsign}", dialogue.Id, downlink.Sender);

await mediator.Publish(new DialogueChangedNotification(dialogue), cancellationToken);

var client = await clientManager.GetAcarsClient(notification.AcarsClientId, cancellationToken);
await client.Send(uplinkMessage, cancellationToken);
logger.Information("Sent CPDLC message from SYSTEM to {Callsign}", downlink.Sender);
}
}
Loading