From 5aea13e3f51bb36aa891ae1c716e4d15d092a2ed Mon Sep 17 00:00:00 2001 From: clayschaad Date: Sat, 23 Aug 2025 19:06:38 +0200 Subject: [PATCH] Cleanup transport service --- Client/Pages/Index.razor.cs | 2 +- Server/Controllers/ShiftController.cs | 2 +- Server/Program.cs | 2 +- Services/IcsExportService.cs | 2 +- Services/PdfExportService.cs | 2 +- Services/ShiftService.cs | 2 +- Services/TransportConnectionCalculator.cs | 36 ++ Services/TransportService.cs | 228 ++++-------- Shared/Shift.cs | 2 +- Shared/ShiftWithTransport.cs | 2 +- Shared/TransportModels.cs | 56 +-- .../TransportServiceTests.cs | 341 +----------------- 12 files changed, 128 insertions(+), 549 deletions(-) create mode 100644 Services/TransportConnectionCalculator.cs diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index 8535be2..c1f9f75 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; -using ShiftScheduler.Shared.Models; using System.Net.Http.Json; using System.Text.Json; +using ShiftScheduler.Shared; namespace ShiftScheduler.Client.Pages { diff --git a/Server/Controllers/ShiftController.cs b/Server/Controllers/ShiftController.cs index 6664d70..7678ce0 100644 --- a/Server/Controllers/ShiftController.cs +++ b/Server/Controllers/ShiftController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using ShiftScheduler.Services; -using ShiftScheduler.Shared.Models; +using ShiftScheduler.Shared; namespace ShiftScheduler.Server.Controllers { diff --git a/Server/Program.cs b/Server/Program.cs index 80455b3..5f549ff 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,5 +1,5 @@ -using ShiftScheduler.Shared.Models; using ShiftScheduler.Services; +using ShiftScheduler.Shared; var builder = WebApplication.CreateBuilder(args); diff --git a/Services/IcsExportService.cs b/Services/IcsExportService.cs index 98217f9..d725df4 100644 --- a/Services/IcsExportService.cs +++ b/Services/IcsExportService.cs @@ -2,7 +2,7 @@ using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; -using ShiftScheduler.Shared.Models; +using ShiftScheduler.Shared; namespace ShiftScheduler.Services { diff --git a/Services/PdfExportService.cs b/Services/PdfExportService.cs index 2a211df..c0ab460 100644 --- a/Services/PdfExportService.cs +++ b/Services/PdfExportService.cs @@ -1,8 +1,8 @@ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; -using ShiftScheduler.Shared.Models; using System.Globalization; +using ShiftScheduler.Shared; namespace ShiftScheduler.Services { diff --git a/Services/ShiftService.cs b/Services/ShiftService.cs index 3727b85..a2cc40d 100644 --- a/Services/ShiftService.cs +++ b/Services/ShiftService.cs @@ -1,5 +1,5 @@  -using ShiftScheduler.Shared.Models; +using ShiftScheduler.Shared; namespace ShiftScheduler.Services { diff --git a/Services/TransportConnectionCalculator.cs b/Services/TransportConnectionCalculator.cs new file mode 100644 index 0000000..874cec2 --- /dev/null +++ b/Services/TransportConnectionCalculator.cs @@ -0,0 +1,36 @@ +using ShiftScheduler.Shared; + +namespace ShiftScheduler.Services; + +public static class TransportConnectionCalculator +{ + public static TransportConnection? FindBestConnection(IReadOnlyList connections, DateTime latestArrivalTime) + { + var validConnections = new List(); + + foreach (var connection in connections) + { + if (DateTime.TryParse(connection.ArrivalTime, out var arrivalTime)) + { + if (arrivalTime <= latestArrivalTime) + { + validConnections.Add(connection); + } + } + } + + if (validConnections.Count > 0) + { + var sortedValid = validConnections + .OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "00:00")) + .ToList(); + + return sortedValid.Last(); + } + + // If no valid connections, return the earliest available + return connections + .OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59")) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/Services/TransportService.cs b/Services/TransportService.cs index b388beb..c6f4a54 100644 --- a/Services/TransportService.cs +++ b/Services/TransportService.cs @@ -1,136 +1,30 @@ -using ShiftScheduler.Shared.Models; using System.Text.Json; +using ShiftScheduler.Shared; namespace ShiftScheduler.Services { - public class TransportService + public class TransportService(HttpClient httpClient, TransportConfiguration config) { - private readonly HttpClient _httpClient; - private readonly TransportConfiguration _config; - - public TransportService(HttpClient httpClient, TransportConfiguration config) + public async Task GetConnectionAsync(DateTime shiftStartTime) { - _httpClient = httpClient; - _config = config; - } + var latestArrivalTime = shiftStartTime.AddMinutes(-config.SafetyBufferMinutes); + var searchDate = shiftStartTime.ToString("yyyy-MM-dd"); + var searchTime = shiftStartTime.ToString("HH:mm"); - public async Task GetConnectionAsync(DateTime shiftStartTime, string? customEndStation = null) - { - try + var url = $"{config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(config.StartStation)}&to={Uri.EscapeDataString(config.EndStation)}&date={searchDate}&time={searchTime}&isArrivalTime=1&limit=5"; + var response = await httpClient.GetStringAsync(url); + var apiResponse = JsonSerializer.Deserialize(response, new JsonSerializerOptions { - var endStation = customEndStation ?? _config.EndStation; - - // Calculate the latest acceptable arrival time (shift start time - safety buffer) - var latestArrivalTime = shiftStartTime.AddMinutes(-_config.SafetyBufferMinutes); - - // Search for arrival time - var searchTime = shiftStartTime; - var searchTimeStr = searchTime.ToString("yyyy-MM-dd"); - var searchHourStr = searchTime.ToString("HH:mm"); + PropertyNameCaseInsensitive = true + }); - var url = $"{_config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(_config.StartStation)}&to={Uri.EscapeDataString(endStation)}&date={searchTimeStr}&time={searchHourStr}&isArrivalTime=1&limit=5"; - - var response = await _httpClient.GetStringAsync(url); - var apiResponse = JsonSerializer.Deserialize(response, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (apiResponse?.Connections?.Count > 0) - { - // Find the best connection that arrives before the required time - var bestConnection = FindBestConnection(apiResponse.Connections, latestArrivalTime); - return MapToTransportConnection(bestConnection); - } - - return null; - } - catch (Exception ex) + if (apiResponse?.Connections.Count > 0) { - // Log the exception and return a mock connection for demonstration - Console.WriteLine($"Error fetching transport connection: {ex.Message}"); - return CreateMockConnection(shiftStartTime); - } - } - - private TransportConnection CreateMockConnection(DateTime shiftStartTime) - { - // Create a realistic mock connection that arrives before shift start with safety buffer - var latestArrivalTime = shiftStartTime.AddMinutes(-_config.SafetyBufferMinutes); - var arrivalTime = latestArrivalTime.AddMinutes(-5); // Arrive 5 minutes before the latest acceptable time - var departureTime = arrivalTime.AddMinutes(-45); // 45 minutes journey time - - return new TransportConnection - { - DepartureTime = departureTime.ToString("HH:mm"), - ArrivalTime = arrivalTime.ToString("HH:mm"), - Duration = "00:45:00", - Platform = "3", - Sections = new List - { - new TransportSection - { - Journey = new TransportJourney - { - Name = "S1", - Category = "S", - Number = "1" - }, - Departure = new TransportCheckpoint - { - Station = new TransportStation - { - Name = _config.StartStation, - Id = "start" - }, - Departure = departureTime.ToString("HH:mm"), - Platform = "3" - }, - Arrival = new TransportCheckpoint - { - Station = new TransportStation - { - Name = _config.EndStation, - Id = "end" - }, - Arrival = arrivalTime.ToString("HH:mm"), - Platform = "1" - } - } - } - }; - } - - private TransportApiConnection? FindBestConnection(List connections, DateTime latestArrivalTime) - { - // Filter connections that arrive before the latest acceptable time - var validConnections = new List(); - - foreach (var connection in connections) - { - if (DateTime.TryParse(connection.To?.Arrival, out var arrivalTime)) - { - // Compare arrival time with latest acceptable time - if (arrivalTime <= latestArrivalTime) - { - validConnections.Add(connection); - } - } + var allConnections = apiResponse.Connections.Select(MapToTransportConnection).ToList(); + return TransportConnectionCalculator.FindBestConnection(allConnections, latestArrivalTime); } - if (validConnections.Count > 0) - { - var sortedValid = validConnections - .OrderBy(c => DateTime.Parse(c.To?.Arrival ?? "00:00")) - .ToList(); - - return sortedValid.Last(); - } - - // If no valid connections, return the earliest available - return connections - .OrderBy(c => DateTime.Parse(c.To?.Arrival ?? "23:59")) - .FirstOrDefault(); + return null; } private TransportConnection MapToTransportConnection(TransportApiConnection? apiConnection) @@ -142,54 +36,92 @@ namespace ShiftScheduler.Services { DepartureTime = apiConnection.From?.Departure ?? string.Empty, ArrivalTime = apiConnection.To?.Arrival ?? string.Empty, - Duration = apiConnection.Duration ?? string.Empty, + Duration = apiConnection.Duration, Platform = apiConnection.From?.Platform ?? string.Empty, - Sections = apiConnection.Sections?.Select(s => new TransportSection + Sections = apiConnection.Sections.Select(s => new TransportSection { Journey = s.Journey != null ? new TransportJourney { - Name = s.Journey.Name ?? string.Empty, - Category = s.Journey.Category ?? string.Empty, - Number = s.Journey.Number ?? string.Empty + Name = s.Journey.Name, + Category = s.Journey.Category, + Number = s.Journey.Number } : null, Departure = s.Departure != null ? new TransportCheckpoint { Station = s.Departure.Station != null ? new TransportStation { - Name = s.Departure.Station.Name ?? string.Empty, - Id = s.Departure.Station.Id ?? string.Empty + Name = s.Departure.Station.Name, + Id = s.Departure.Station.Id } : null, - Departure = s.Departure.Departure ?? string.Empty, - Arrival = s.Departure.Arrival ?? string.Empty, - Platform = s.Departure.Platform ?? string.Empty + Departure = s.Departure.Departure, + Arrival = s.Departure.Arrival, + Platform = s.Departure.Platform } : null, Arrival = s.Arrival != null ? new TransportCheckpoint { Station = s.Arrival.Station != null ? new TransportStation { - Name = s.Arrival.Station.Name ?? string.Empty, - Id = s.Arrival.Station.Id ?? string.Empty + Name = s.Arrival.Station.Name, + Id = s.Arrival.Station.Id } : null, - Departure = s.Arrival.Departure ?? string.Empty, - Arrival = s.Arrival.Arrival ?? string.Empty, - Platform = s.Arrival.Platform ?? string.Empty + Departure = s.Arrival.Departure, + Arrival = s.Arrival.Arrival, + Platform = s.Arrival.Platform } : null - }).ToList() ?? new List() + }).ToList() }; } + } + + // API Response models for OpenData CH Transport + public class TransportApiResponse + { + public List Connections { get; set; } = new(); + } - public string FormatConnectionSummary(TransportConnection? connection) + public class TransportApiConnection + { + public TransportApiCheckpoint? From { get; set; } + public TransportApiCheckpoint? To { get; set; } + public string Duration { get; set; } = string.Empty; + public List Sections { get; set; } = new(); + + public override string ToString() { - if (connection == null || string.IsNullOrEmpty(connection.DepartureTime)) - return "No transport info"; - - var departure = DateTime.TryParse(connection.DepartureTime, out var dep) ? dep.ToString("HH:mm") : connection.DepartureTime; - var arrival = DateTime.TryParse(connection.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : connection.ArrivalTime; - - var mainJourney = connection.Sections?.FirstOrDefault()?.Journey; - var trainInfo = mainJourney != null ? $"{mainJourney.Category} {mainJourney.Number}" : "Train"; - - return $"{trainInfo}: {departure} → {arrival}"; + return $"{From} - {To}"; } } + + public class TransportApiCheckpoint + { + public TransportApiStation? Station { get; set; } + public string Departure { get; set; } = string.Empty; + public string Arrival { get; set; } = string.Empty; + public string Platform { get; set; } = string.Empty; + + public override string ToString() + { + return $"{Station?.Name}: {Departure} - {Arrival}"; + } + } + + public class TransportApiStation + { + public string Name { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + } + + public class TransportApiSection + { + public TransportApiJourney? Journey { get; set; } + public TransportApiCheckpoint? Departure { get; set; } + public TransportApiCheckpoint? Arrival { get; set; } + } + + public class TransportApiJourney + { + public string Name { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public string Number { get; set; } = string.Empty; + } } \ No newline at end of file diff --git a/Shared/Shift.cs b/Shared/Shift.cs index 37a45da..8029e6c 100644 --- a/Shared/Shift.cs +++ b/Shared/Shift.cs @@ -1,4 +1,4 @@ -namespace ShiftScheduler.Shared.Models +namespace ShiftScheduler.Shared { public class Shift { diff --git a/Shared/ShiftWithTransport.cs b/Shared/ShiftWithTransport.cs index 5bfa2b7..ad11ace 100644 --- a/Shared/ShiftWithTransport.cs +++ b/Shared/ShiftWithTransport.cs @@ -1,4 +1,4 @@ -namespace ShiftScheduler.Shared.Models +namespace ShiftScheduler.Shared { // View model that combines shift information with transport data for a specific date public record ShiftWithTransport diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index c826316..aa7de4d 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -1,4 +1,4 @@ -namespace ShiftScheduler.Shared.Models +namespace ShiftScheduler.Shared { public class TransportConfiguration { @@ -45,58 +45,4 @@ namespace ShiftScheduler.Shared.Models public string Name { get; set; } = string.Empty; public string Id { get; set; } = string.Empty; } - - // API Response models for OpenData CH Transport - public class TransportApiResponse - { - public List Connections { get; set; } = new(); - } - - public class TransportApiConnection - { - public TransportApiCheckpoint? From { get; set; } - public TransportApiCheckpoint? To { get; set; } - public string Duration { get; set; } = string.Empty; - public List Sections { get; set; } = new(); - - public override string ToString() - { - return $"{From} - {To}"; - } - } - - public class TransportApiCheckpoint - { - public TransportApiStation? Station { get; set; } - public string Departure { get; set; } = string.Empty; - public string Arrival { get; set; } = string.Empty; - public string Platform { get; set; } = string.Empty; - - public override string ToString() - { - return $"{Station?.Name}: {Departure} - {Arrival}"; - } - } - - public class TransportApiStation - { - public string Name { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - } - - public class TransportApiSection - { - public TransportApiJourney? Journey { get; set; } - public TransportApiCheckpoint? Departure { get; set; } - public TransportApiCheckpoint? Arrival { get; set; } - } - - public class TransportApiJourney - { - public string Name { get; set; } = string.Empty; - public string Category { get; set; } = string.Empty; - public string Number { get; set; } = string.Empty; - } - - } \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/TransportServiceTests.cs b/ShiftScheduler.Services.Tests/TransportServiceTests.cs index 1ed050d..005ecdc 100644 --- a/ShiftScheduler.Services.Tests/TransportServiceTests.cs +++ b/ShiftScheduler.Services.Tests/TransportServiceTests.cs @@ -3,7 +3,7 @@ using System.Text.Json; using Moq; using Moq.Protected; using ShiftScheduler.Services; -using ShiftScheduler.Shared.Models; +using ShiftScheduler.Shared; using Shouldly; namespace ShiftScheduler.Services.Tests; @@ -12,7 +12,6 @@ public class TransportServiceTests { private readonly Mock _httpMessageHandlerMock; private readonly HttpClient _httpClient; - private readonly TransportConfiguration _config; private readonly TransportService _transportService; public TransportServiceTests() @@ -20,7 +19,7 @@ public class TransportServiceTests _httpMessageHandlerMock = new Mock(); _httpClient = new HttpClient(_httpMessageHandlerMock.Object); - _config = new TransportConfiguration + var config = new TransportConfiguration { StartStation = "Zurich HB", EndStation = "Bern", @@ -29,7 +28,7 @@ public class TransportServiceTests MinBreakMinutes = 60 }; - _transportService = new TransportService(_httpClient, _config); + _transportService = new TransportService(_httpClient, config); } [Fact] @@ -60,47 +59,6 @@ public class TransportServiceTests section.Journey?.Number.ShouldBe("1"); } - [Fact] - public async Task GetConnectionAsync_WithCustomEndStation_ShouldUseCustomStation() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - var customEndStation = "Basel"; - var apiResponse = CreateValidApiResponse(); - var jsonResponse = JsonSerializer.Serialize(apiResponse); - - SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse); - - // Act - await _transportService.GetConnectionAsync(shiftStartTime, customEndStation); - - // Assert - _httpMessageHandlerMock.Protected() - .Verify("SendAsync", Times.Once(), - ItExpr.Is(req => - req.RequestUri!.ToString().Contains(Uri.EscapeDataString(customEndStation))), - ItExpr.IsAny()); - } - - [Fact] - public async Task GetConnectionAsync_WithHttpException_ShouldReturnMockConnection() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - - SetupHttpMockResponse(HttpStatusCode.InternalServerError, "Server Error"); - - // Act - var result = await _transportService.GetConnectionAsync(shiftStartTime); - - // Assert - result.ShouldNotBeNull(); - result.DepartureTime.ShouldNotBeNullOrEmpty(); - result.ArrivalTime.ShouldNotBeNullOrEmpty(); - result.Duration.ShouldBe("00:45:00"); - result.Platform.ShouldBe("3"); - } - [Fact] public async Task GetConnectionAsync_WithEmptyApiResponse_ShouldReturnNull() { @@ -155,299 +113,6 @@ public class TransportServiceTests result.ArrivalTime.ShouldBe("2023-12-15T07:25:00"); } - [Fact] - public void FormatConnectionSummary_WithValidConnection_ShouldReturnFormattedString() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "06:45", - ArrivalTime = "07:30", - Sections = new List - { - new TransportSection - { - Journey = new TransportJourney - { - Category = "IC", - Number = "1" - } - } - } - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe("IC 1: 06:45 → 07:30"); - } - - [Fact] - public void FormatConnectionSummary_WithNullConnection_ShouldReturnNoTransportInfo() - { - // Act - var result = _transportService.FormatConnectionSummary(null); - - // Assert - result.ShouldBe("No transport info"); - } - - [Fact] - public void FormatConnectionSummary_WithEmptyDepartureTime_ShouldReturnNoTransportInfo() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "", - ArrivalTime = "07:30" - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe("No transport info"); - } - - [Fact] - public void FormatConnectionSummary_WithNoJourney_ShouldUseDefaultTrainLabel() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "06:45", - ArrivalTime = "07:30", - Sections = new List() - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe("Train: 06:45 → 07:30"); - } - - [Fact] - public void FormatConnectionSummary_WithDateTimeParsing_ShouldFormatCorrectly() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "2023-12-15T06:45:00", - ArrivalTime = "2023-12-15T07:30:00", - Sections = new List - { - new TransportSection - { - Journey = new TransportJourney - { - Category = "S", - Number = "3" - } - } - } - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe("S 3: 06:45 → 07:30"); - } - - [Theory] - [InlineData(30, "07:25")] // Should arrive 5 minutes before latest acceptable time - [InlineData(60, "06:55")] // Should arrive 5 minutes before latest acceptable time with 60 min buffer - [InlineData(15, "07:40")] // Should arrive 5 minutes before latest acceptable time with 15 min buffer - public async Task GetConnectionAsync_WithDifferentSafetyBuffers_ShouldCreateCorrectMockConnection( - int safetyBufferMinutes, string expectedArrivalTime) - { - // Arrange - var config = new TransportConfiguration - { - StartStation = "Test Start", - EndStation = "Test End", - ApiBaseUrl = "https://test.api", - SafetyBufferMinutes = safetyBufferMinutes - }; - var service = new TransportService(_httpClient, config); - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - - SetupHttpMockResponse(HttpStatusCode.InternalServerError, "Error"); - - // Act - var result = await service.GetConnectionAsync(shiftStartTime); - - // Assert - result.ShouldNotBeNull(); - result.ArrivalTime.ShouldBe(expectedArrivalTime); - } - - [Fact] - public async Task GetConnectionAsync_WithInvalidJson_ShouldReturnMockConnection() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - - SetupHttpMockResponse(HttpStatusCode.OK, "invalid json {[}"); - - // Act - var result = await _transportService.GetConnectionAsync(shiftStartTime); - - // Assert - result.ShouldNotBeNull(); - result.Duration.ShouldBe("00:45:00"); - result.Platform.ShouldBe("3"); - } - - [Fact] - public async Task GetConnectionAsync_WithNetworkTimeout_ShouldReturnMockConnection() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - - _httpMessageHandlerMock.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new HttpRequestException("Network timeout")); - - // Act - var result = await _transportService.GetConnectionAsync(shiftStartTime); - - // Assert - result.ShouldNotBeNull(); - result.Duration.ShouldBe("00:45:00"); - result.Platform.ShouldBe("3"); - } - - [Fact] - public async Task GetConnectionAsync_WithConnectionsWithInvalidTimes_ShouldFallbackToMockConnection() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - var apiResponse = new TransportApiResponse - { - Connections = new List - { - new TransportApiConnection - { - From = new TransportApiCheckpoint - { - Station = new TransportApiStation { Name = "Start", Id = "start" }, - Departure = "invalid-time", - Platform = "1" - }, - To = new TransportApiCheckpoint - { - Station = new TransportApiStation { Name = "End", Id = "end" }, - Arrival = "not-a-time", - Platform = "2" - }, - Duration = "00:30:00" - } - } - }; - var jsonResponse = JsonSerializer.Serialize(apiResponse); - - SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse); - - // Act - var result = await _transportService.GetConnectionAsync(shiftStartTime); - - // Assert - // When API contains invalid time formats, the service should fall back to mock connection - // because the FindBestConnection method can't parse invalid times - result.ShouldNotBeNull(); - result.Duration.ShouldBe("00:45:00"); // Mock connection duration - result.Platform.ShouldBe("3"); // Mock connection platform - } - - [Fact] - public async Task GetConnectionAsync_WithNullStations_ShouldHandleGracefully() - { - // Arrange - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); - var apiResponse = new TransportApiResponse - { - Connections = new List - { - new TransportApiConnection - { - From = null, - To = null, - Duration = "00:30:00" - } - } - }; - var jsonResponse = JsonSerializer.Serialize(apiResponse); - - SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse); - - // Act - var result = await _transportService.GetConnectionAsync(shiftStartTime); - - // Assert - result.ShouldNotBeNull(); - result.DepartureTime.ShouldBe(string.Empty); - result.ArrivalTime.ShouldBe(string.Empty); - result.Platform.ShouldBe(string.Empty); - } - - [Fact] - public void FormatConnectionSummary_WithNullJourneySection_ShouldUseDefaultTrainLabel() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "06:45", - ArrivalTime = "07:30", - Sections = new List - { - new TransportSection - { - Journey = null - } - } - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe("Train: 06:45 → 07:30"); - } - - [Fact] - public void FormatConnectionSummary_WithEmptyJourneyFields_ShouldUseTrainLabel() - { - // Arrange - var connection = new TransportConnection - { - DepartureTime = "06:45", - ArrivalTime = "07:30", - Sections = new List - { - new TransportSection - { - Journey = new TransportJourney - { - Category = "", - Number = "" - } - } - } - }; - - // Act - var result = _transportService.FormatConnectionSummary(connection); - - // Assert - result.ShouldBe(" : 06:45 → 07:30"); - } - private void SetupHttpMockResponse(HttpStatusCode statusCode, string content) { var response = new HttpResponseMessage(statusCode)