Implement caching for transport connection results with IMemoryCache (#26)
* Initial plan * Implement caching for transport connection results with IMemoryCache Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Change cache duration from minutes to days Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Implement CachedTransportService design with interface separation Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Implement persistent file-based cache to survive application restarts Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Revert persistent file-based cache implementation, restore IMemoryCache usage Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Code cleanup --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> Co-authored-by: Claudio Schaad <c.schaad@pog.ch>
This commit is contained in:
parent
68e2fa4927
commit
73abfac988
10 changed files with 504 additions and 321 deletions
|
|
@ -11,14 +11,14 @@ namespace ShiftScheduler.Server.Controllers
|
||||||
private readonly ShiftService _shiftService;
|
private readonly ShiftService _shiftService;
|
||||||
private readonly IcsExportService _icsService;
|
private readonly IcsExportService _icsService;
|
||||||
private readonly PdfExportService _pdfExportService;
|
private readonly PdfExportService _pdfExportService;
|
||||||
private readonly TransportService _transportService;
|
private readonly ITransportService _transportService;
|
||||||
private readonly TransportConfiguration _transportConfig;
|
private readonly TransportConfiguration _transportConfig;
|
||||||
|
|
||||||
public ShiftController(
|
public ShiftController(
|
||||||
ShiftService shiftService,
|
ShiftService shiftService,
|
||||||
IcsExportService icsService,
|
IcsExportService icsService,
|
||||||
PdfExportService pdfExportService,
|
PdfExportService pdfExportService,
|
||||||
TransportService transportService,
|
ITransportService transportService,
|
||||||
TransportConfiguration transportConfig)
|
TransportConfiguration transportConfig)
|
||||||
{
|
{
|
||||||
_shiftService = shiftService;
|
_shiftService = shiftService;
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ var transportConfig = builder.Configuration.GetSection("Transport").Get<Transpor
|
||||||
// Register services
|
// Register services
|
||||||
builder.Services.AddSingleton(shifts);
|
builder.Services.AddSingleton(shifts);
|
||||||
builder.Services.AddSingleton(transportConfig);
|
builder.Services.AddSingleton(transportConfig);
|
||||||
builder.Services.AddHttpClient<TransportService>();
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddHttpClient<TransportApiService>();
|
||||||
builder.Services.AddSingleton<ShiftService>();
|
builder.Services.AddSingleton<ShiftService>();
|
||||||
builder.Services.AddSingleton<IcsExportService>();
|
builder.Services.AddSingleton<IcsExportService>();
|
||||||
builder.Services.AddSingleton<PdfExportService>();
|
builder.Services.AddSingleton<PdfExportService>();
|
||||||
builder.Services.AddSingleton<TransportService>();
|
builder.Services.AddSingleton<ITransportApiService, TransportApiService>();
|
||||||
|
builder.Services.AddSingleton<ITransportService, TransportService>();
|
||||||
|
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
"SafetyBufferMinutes": 30,
|
"SafetyBufferMinutes": 30,
|
||||||
"MinBreakMinutes": 60,
|
"MinBreakMinutes": 60,
|
||||||
"MaxEarlyArrivalMinutes": 60,
|
"MaxEarlyArrivalMinutes": 60,
|
||||||
"MaxLateArrivalMinutes": 15
|
"MaxLateArrivalMinutes": 15,
|
||||||
|
"CacheDurationDays": 1
|
||||||
},
|
},
|
||||||
|
|
||||||
"Shifts": [
|
"Shifts": [
|
||||||
|
|
|
||||||
14
Services/ITransportService.cs
Normal file
14
Services/ITransportService.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using ShiftScheduler.Shared;
|
||||||
|
|
||||||
|
namespace ShiftScheduler.Services
|
||||||
|
{
|
||||||
|
public interface ITransportService
|
||||||
|
{
|
||||||
|
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ITransportApiService
|
||||||
|
{
|
||||||
|
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ical.Net" Version="5.1.0" />
|
<PackageReference Include="Ical.Net" Version="5.1.0" />
|
||||||
<PackageReference Include="QuestPDF" Version="2025.7.0" />
|
<PackageReference Include="QuestPDF" Version="2025.7.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
133
Services/TransportApiService.cs
Normal file
133
Services/TransportApiService.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using ShiftScheduler.Shared;
|
||||||
|
|
||||||
|
namespace ShiftScheduler.Services
|
||||||
|
{
|
||||||
|
public class TransportApiService(HttpClient httpClient, TransportConfiguration config) : ITransportApiService
|
||||||
|
{
|
||||||
|
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
||||||
|
{
|
||||||
|
// To allow connections that arrive after shift starts, we search from earlier time
|
||||||
|
// and request more connections to cover the full range
|
||||||
|
var searchDate = shiftStartTime.ToString("yyyy-MM-dd");
|
||||||
|
var searchTime = shiftStartTime.AddMinutes(config.MaxLateArrivalMinutes).ToString("HH:mm");
|
||||||
|
|
||||||
|
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<TransportApiResponse>(response, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (apiResponse?.Connections.Count > 0)
|
||||||
|
{
|
||||||
|
var allConnections = apiResponse.Connections.Select(MapToTransportConnection).ToList();
|
||||||
|
return TransportConnectionCalculator.FindBestConnection(
|
||||||
|
allConnections,
|
||||||
|
shiftStartTime,
|
||||||
|
config.SafetyBufferMinutes,
|
||||||
|
config.MaxEarlyArrivalMinutes,
|
||||||
|
config.MaxLateArrivalMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportConnection MapToTransportConnection(TransportApiConnection? apiConnection)
|
||||||
|
{
|
||||||
|
if (apiConnection == null)
|
||||||
|
return new TransportConnection();
|
||||||
|
|
||||||
|
return new TransportConnection
|
||||||
|
{
|
||||||
|
DepartureTime = apiConnection.From?.Departure ?? string.Empty,
|
||||||
|
ArrivalTime = apiConnection.To?.Arrival ?? string.Empty,
|
||||||
|
Duration = apiConnection.Duration,
|
||||||
|
Platform = apiConnection.From?.Platform ?? string.Empty,
|
||||||
|
Sections = apiConnection.Sections.Select(s => new TransportSection
|
||||||
|
{
|
||||||
|
Journey = s.Journey != null ? new TransportJourney
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
Id = s.Departure.Station.Id
|
||||||
|
} : null,
|
||||||
|
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,
|
||||||
|
Id = s.Arrival.Station.Id
|
||||||
|
} : null,
|
||||||
|
Departure = s.Arrival.Departure,
|
||||||
|
Arrival = s.Arrival.Arrival,
|
||||||
|
Platform = s.Arrival.Platform
|
||||||
|
} : null
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Response models for OpenData CH Transport
|
||||||
|
public class TransportApiResponse
|
||||||
|
{
|
||||||
|
public List<TransportApiConnection> 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<TransportApiSection> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,135 +1,43 @@
|
||||||
using System.Text.Json;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
|
|
||||||
namespace ShiftScheduler.Services
|
namespace ShiftScheduler.Services
|
||||||
{
|
{
|
||||||
public class TransportService(HttpClient httpClient, TransportConfiguration config)
|
public class TransportService(ITransportApiService transportService, TransportConfiguration config, IMemoryCache cache) : ITransportService
|
||||||
{
|
{
|
||||||
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
||||||
{
|
{
|
||||||
var latestArrivalTime = shiftStartTime.AddMinutes(-config.SafetyBufferMinutes);
|
|
||||||
var searchDate = shiftStartTime.ToString("yyyy-MM-dd");
|
var searchDate = shiftStartTime.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
// To allow connections that arrive after shift starts, we search from earlier time
|
|
||||||
// and request more connections to cover the full range
|
|
||||||
var searchTime = shiftStartTime.AddMinutes(config.MaxLateArrivalMinutes).ToString("HH:mm");
|
var searchTime = shiftStartTime.AddMinutes(config.MaxLateArrivalMinutes).ToString("HH:mm");
|
||||||
|
|
||||||
var url = $"{config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(config.StartStation)}&to={Uri.EscapeDataString(config.EndStation)}&date={searchDate}&time={searchTime}&isArrivalTime=1&limit=5";
|
// Generate cache key based on request parameters
|
||||||
var response = await httpClient.GetStringAsync(url);
|
var cacheKey = GenerateCacheKey(searchDate, searchTime);
|
||||||
var apiResponse = JsonSerializer.Deserialize<TransportApiResponse>(response, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (apiResponse?.Connections.Count > 0)
|
// Try to get from cache first
|
||||||
|
if (cache.TryGetValue(cacheKey, out TransportConnection? cachedConnection) && cachedConnection != null)
|
||||||
{
|
{
|
||||||
var allConnections = apiResponse.Connections.Select(MapToTransportConnection).ToList();
|
return cachedConnection;
|
||||||
return TransportConnectionCalculator.FindBestConnection(
|
|
||||||
allConnections,
|
|
||||||
shiftStartTime,
|
|
||||||
config.SafetyBufferMinutes,
|
|
||||||
config.MaxEarlyArrivalMinutes,
|
|
||||||
config.MaxLateArrivalMinutes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// Not in cache, call the underlying transport service
|
||||||
}
|
var connection = await transportService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
private TransportConnection MapToTransportConnection(TransportApiConnection? apiConnection)
|
// Cache the connection result if valid
|
||||||
|
if (connection != null)
|
||||||
{
|
{
|
||||||
if (apiConnection == null)
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
return new TransportConnection();
|
|
||||||
|
|
||||||
return new TransportConnection
|
|
||||||
{
|
{
|
||||||
DepartureTime = apiConnection.From?.Departure ?? string.Empty,
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(config.CacheDurationDays)
|
||||||
ArrivalTime = apiConnection.To?.Arrival ?? string.Empty,
|
|
||||||
Duration = apiConnection.Duration,
|
|
||||||
Platform = apiConnection.From?.Platform ?? string.Empty,
|
|
||||||
Sections = apiConnection.Sections.Select(s => new TransportSection
|
|
||||||
{
|
|
||||||
Journey = s.Journey != null ? new TransportJourney
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
Id = s.Departure.Station.Id
|
|
||||||
} : null,
|
|
||||||
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,
|
|
||||||
Id = s.Arrival.Station.Id
|
|
||||||
} : null,
|
|
||||||
Departure = s.Arrival.Departure,
|
|
||||||
Arrival = s.Arrival.Arrival,
|
|
||||||
Platform = s.Arrival.Platform
|
|
||||||
} : null
|
|
||||||
}).ToList()
|
|
||||||
};
|
};
|
||||||
}
|
cache.Set(cacheKey, connection, cacheOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Response models for OpenData CH Transport
|
return connection;
|
||||||
public class TransportApiResponse
|
|
||||||
{
|
|
||||||
public List<TransportApiConnection> Connections { get; set; } = new();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TransportApiConnection
|
private string GenerateCacheKey(string searchDate, string searchTime)
|
||||||
{
|
{
|
||||||
public TransportApiCheckpoint? From { get; set; }
|
return $"transport_{config.StartStation}_{config.EndStation}_{searchDate}_{searchTime}";
|
||||||
public TransportApiCheckpoint? To { get; set; }
|
|
||||||
public string Duration { get; set; } = string.Empty;
|
|
||||||
public List<TransportApiSection> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ namespace ShiftScheduler.Shared
|
||||||
public int MinBreakMinutes { get; set; } = 60;
|
public int MinBreakMinutes { get; set; } = 60;
|
||||||
public int MaxEarlyArrivalMinutes { get; set; } = 60;
|
public int MaxEarlyArrivalMinutes { get; set; } = 60;
|
||||||
public int MaxLateArrivalMinutes { get; set; } = 15;
|
public int MaxLateArrivalMinutes { get; set; } = 15;
|
||||||
|
public int CacheDurationDays { get; set; } = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TransportConnection
|
public class TransportConnection
|
||||||
|
|
|
||||||
242
ShiftScheduler.Services.Tests/TransportApiServiceTests.cs
Normal file
242
ShiftScheduler.Services.Tests/TransportApiServiceTests.cs
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Moq;
|
||||||
|
using Moq.Protected;
|
||||||
|
using ShiftScheduler.Services;
|
||||||
|
using ShiftScheduler.Shared;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace ShiftScheduler.Services.Tests;
|
||||||
|
|
||||||
|
public class TransportApiServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly TransportApiService _transportApiService;
|
||||||
|
|
||||||
|
public TransportApiServiceTests()
|
||||||
|
{
|
||||||
|
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
||||||
|
_httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
||||||
|
|
||||||
|
var config = new TransportConfiguration
|
||||||
|
{
|
||||||
|
StartStation = "Zurich HB",
|
||||||
|
EndStation = "Bern",
|
||||||
|
ApiBaseUrl = "https://transport.opendata.ch/v1",
|
||||||
|
SafetyBufferMinutes = 30,
|
||||||
|
MinBreakMinutes = 60,
|
||||||
|
MaxEarlyArrivalMinutes = 60,
|
||||||
|
MaxLateArrivalMinutes = 15,
|
||||||
|
CacheDurationDays = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
_transportApiService = new TransportApiService(_httpClient, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithValidApiResponse_ShouldReturnMappedConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
var apiResponse = CreateValidApiResponse();
|
||||||
|
var jsonResponse = JsonSerializer.Serialize(apiResponse);
|
||||||
|
|
||||||
|
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _transportApiService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.ShouldNotBeNull();
|
||||||
|
result.DepartureTime.ShouldBe("2023-12-15T06:45:00");
|
||||||
|
result.ArrivalTime.ShouldBe("2023-12-15T07:30:00");
|
||||||
|
result.Duration.ShouldBe("00:45:00");
|
||||||
|
result.Platform.ShouldBe("5");
|
||||||
|
result.Sections.ShouldNotBeEmpty();
|
||||||
|
result.Sections.Count.ShouldBe(1);
|
||||||
|
|
||||||
|
var section = result.Sections.First();
|
||||||
|
section.Journey?.Name.ShouldBe("IC 1");
|
||||||
|
section.Journey?.Category.ShouldBe("IC");
|
||||||
|
section.Journey?.Number.ShouldBe("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithEmptyApiResponse_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
var emptyResponse = new TransportApiResponse { Connections = new List<TransportApiConnection>() };
|
||||||
|
var jsonResponse = JsonSerializer.Serialize(emptyResponse);
|
||||||
|
|
||||||
|
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _transportApiService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithNullApiResponse_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
|
||||||
|
SetupHttpMockResponse(HttpStatusCode.OK, "null");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _transportApiService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithMultipleConnections_ShouldReturnBestConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
var apiResponse = CreateMultipleConnectionsResponse();
|
||||||
|
var jsonResponse = JsonSerializer.Serialize(apiResponse);
|
||||||
|
|
||||||
|
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _transportApiService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.ShouldNotBeNull();
|
||||||
|
// Now with proper datetime formats, the algorithm should work correctly
|
||||||
|
// Latest acceptable arrival: 8:00 - 30 min = 7:30
|
||||||
|
// Valid connections: 07:15 and 07:25 (both arrive before 7:30)
|
||||||
|
// Algorithm should return the latest valid: 07:25
|
||||||
|
result.ArrivalTime.ShouldBe("2023-12-15T07:25:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupHttpMockResponse(HttpStatusCode statusCode, string content)
|
||||||
|
{
|
||||||
|
_httpMessageHandlerMock.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>(
|
||||||
|
"SendAsync",
|
||||||
|
ItExpr.IsAny<HttpRequestMessage>(),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(() => new HttpResponseMessage(statusCode)
|
||||||
|
{
|
||||||
|
Content = new StringContent(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TransportApiResponse CreateValidApiResponse()
|
||||||
|
{
|
||||||
|
return new TransportApiResponse
|
||||||
|
{
|
||||||
|
Connections = new List<TransportApiConnection>
|
||||||
|
{
|
||||||
|
new TransportApiConnection
|
||||||
|
{
|
||||||
|
From = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
||||||
|
Departure = "2023-12-15T06:45:00",
|
||||||
|
Platform = "5"
|
||||||
|
},
|
||||||
|
To = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
||||||
|
Arrival = "2023-12-15T07:30:00",
|
||||||
|
Platform = "3"
|
||||||
|
},
|
||||||
|
Duration = "00:45:00",
|
||||||
|
Sections = new List<TransportApiSection>
|
||||||
|
{
|
||||||
|
new TransportApiSection
|
||||||
|
{
|
||||||
|
Journey = new TransportApiJourney
|
||||||
|
{
|
||||||
|
Name = "IC 1",
|
||||||
|
Category = "IC",
|
||||||
|
Number = "1"
|
||||||
|
},
|
||||||
|
Departure = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
||||||
|
Departure = "2023-12-15T06:45:00",
|
||||||
|
Platform = "5"
|
||||||
|
},
|
||||||
|
Arrival = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
||||||
|
Arrival = "2023-12-15T07:30:00",
|
||||||
|
Platform = "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TransportApiResponse CreateMultipleConnectionsResponse()
|
||||||
|
{
|
||||||
|
return new TransportApiResponse
|
||||||
|
{
|
||||||
|
Connections = new List<TransportApiConnection>
|
||||||
|
{
|
||||||
|
// Early connection - valid
|
||||||
|
new TransportApiConnection
|
||||||
|
{
|
||||||
|
From = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
||||||
|
Departure = "2023-12-15T06:30:00",
|
||||||
|
Platform = "5"
|
||||||
|
},
|
||||||
|
To = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
||||||
|
Arrival = "2023-12-15T07:15:00",
|
||||||
|
Platform = "3"
|
||||||
|
},
|
||||||
|
Duration = "00:45:00"
|
||||||
|
},
|
||||||
|
// Later valid connection - should be selected as best
|
||||||
|
new TransportApiConnection
|
||||||
|
{
|
||||||
|
From = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
||||||
|
Departure = "2023-12-15T06:40:00",
|
||||||
|
Platform = "4"
|
||||||
|
},
|
||||||
|
To = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
||||||
|
Arrival = "2023-12-15T07:25:00",
|
||||||
|
Platform = "2"
|
||||||
|
},
|
||||||
|
Duration = "00:45:00"
|
||||||
|
},
|
||||||
|
// Too late connection - invalid (arrives after 07:30 which is shift start - 30 min buffer)
|
||||||
|
new TransportApiConnection
|
||||||
|
{
|
||||||
|
From = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
||||||
|
Departure = "2023-12-15T06:50:00",
|
||||||
|
Platform = "6"
|
||||||
|
},
|
||||||
|
To = new TransportApiCheckpoint
|
||||||
|
{
|
||||||
|
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
||||||
|
Arrival = "2023-12-15T07:35:00",
|
||||||
|
Platform = "1"
|
||||||
|
},
|
||||||
|
Duration = "00:45:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System.Net;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System.Text.Json;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using Moq.Protected;
|
|
||||||
using ShiftScheduler.Services;
|
using ShiftScheduler.Services;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
|
@ -10,16 +8,17 @@ namespace ShiftScheduler.Services.Tests;
|
||||||
|
|
||||||
public class TransportServiceTests
|
public class TransportServiceTests
|
||||||
{
|
{
|
||||||
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
private readonly Mock<ITransportApiService> _transportServiceMock;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly TransportService _transportService;
|
private readonly TransportService _transportService;
|
||||||
|
private readonly TransportConfiguration _config;
|
||||||
|
|
||||||
public TransportServiceTests()
|
public TransportServiceTests()
|
||||||
{
|
{
|
||||||
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
_transportServiceMock = new Mock<ITransportApiService>();
|
||||||
_httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
|
||||||
var config = new TransportConfiguration
|
_config = new TransportConfiguration
|
||||||
{
|
{
|
||||||
StartStation = "Zurich HB",
|
StartStation = "Zurich HB",
|
||||||
EndStation = "Bern",
|
EndStation = "Bern",
|
||||||
|
|
@ -27,217 +26,99 @@ public class TransportServiceTests
|
||||||
SafetyBufferMinutes = 30,
|
SafetyBufferMinutes = 30,
|
||||||
MinBreakMinutes = 60,
|
MinBreakMinutes = 60,
|
||||||
MaxEarlyArrivalMinutes = 60,
|
MaxEarlyArrivalMinutes = 60,
|
||||||
MaxLateArrivalMinutes = 15
|
MaxLateArrivalMinutes = 15,
|
||||||
|
CacheDurationDays = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
_transportService = new TransportService(_httpClient, config);
|
_transportService = new TransportService(_transportServiceMock.Object, _config, _memoryCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetConnectionAsync_WithValidApiResponse_ShouldReturnMappedConnection()
|
public async Task GetConnectionAsync_WithValidConnection_ShouldCacheResult()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
var apiResponse = CreateValidApiResponse();
|
var connection = new TransportConnection
|
||||||
var jsonResponse = JsonSerializer.Serialize(apiResponse);
|
|
||||||
|
|
||||||
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _transportService.GetConnectionAsync(shiftStartTime);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
result.DepartureTime.ShouldBe("2023-12-15T06:45:00");
|
|
||||||
result.ArrivalTime.ShouldBe("2023-12-15T07:30:00");
|
|
||||||
result.Duration.ShouldBe("00:45:00");
|
|
||||||
result.Platform.ShouldBe("5");
|
|
||||||
result.Sections.ShouldNotBeEmpty();
|
|
||||||
result.Sections.Count.ShouldBe(1);
|
|
||||||
|
|
||||||
var section = result.Sections.First();
|
|
||||||
section.Journey?.Name.ShouldBe("IC 1");
|
|
||||||
section.Journey?.Category.ShouldBe("IC");
|
|
||||||
section.Journey?.Number.ShouldBe("1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConnectionAsync_WithEmptyApiResponse_ShouldReturnNull()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
DepartureTime = "2023-12-15T06:45:00",
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
ArrivalTime = "2023-12-15T07:30:00",
|
||||||
var emptyResponse = new TransportApiResponse { Connections = new List<TransportApiConnection>() };
|
|
||||||
var jsonResponse = JsonSerializer.Serialize(emptyResponse);
|
|
||||||
|
|
||||||
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _transportService.GetConnectionAsync(shiftStartTime);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.ShouldBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConnectionAsync_WithNullApiResponse_ShouldReturnNull()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
|
||||||
|
|
||||||
SetupHttpMockResponse(HttpStatusCode.OK, "null");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _transportService.GetConnectionAsync(shiftStartTime);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.ShouldBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConnectionAsync_WithMultipleConnections_ShouldReturnBestConnection()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
|
||||||
var apiResponse = CreateMultipleConnectionsResponse();
|
|
||||||
var jsonResponse = JsonSerializer.Serialize(apiResponse);
|
|
||||||
|
|
||||||
SetupHttpMockResponse(HttpStatusCode.OK, jsonResponse);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _transportService.GetConnectionAsync(shiftStartTime);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
// Now with proper datetime formats, the algorithm should work correctly
|
|
||||||
// Latest acceptable arrival: 8:00 - 30 min = 7:30
|
|
||||||
// Valid connections: 07:15 and 07:25 (both arrive before 7:30)
|
|
||||||
// Algorithm should return the latest valid: 07:25
|
|
||||||
result.ArrivalTime.ShouldBe("2023-12-15T07:25:00");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupHttpMockResponse(HttpStatusCode statusCode, string content)
|
|
||||||
{
|
|
||||||
var response = new HttpResponseMessage(statusCode)
|
|
||||||
{
|
|
||||||
Content = new StringContent(content)
|
|
||||||
};
|
|
||||||
|
|
||||||
_httpMessageHandlerMock.Protected()
|
|
||||||
.Setup<Task<HttpResponseMessage>>(
|
|
||||||
"SendAsync",
|
|
||||||
ItExpr.IsAny<HttpRequestMessage>(),
|
|
||||||
ItExpr.IsAny<CancellationToken>())
|
|
||||||
.ReturnsAsync(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TransportApiResponse CreateValidApiResponse()
|
|
||||||
{
|
|
||||||
return new TransportApiResponse
|
|
||||||
{
|
|
||||||
Connections = new List<TransportApiConnection>
|
|
||||||
{
|
|
||||||
new TransportApiConnection
|
|
||||||
{
|
|
||||||
From = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
|
||||||
Departure = "2023-12-15T06:45:00",
|
|
||||||
Platform = "5"
|
|
||||||
},
|
|
||||||
To = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
|
||||||
Arrival = "2023-12-15T07:30:00",
|
|
||||||
Platform = "3"
|
|
||||||
},
|
|
||||||
Duration = "00:45:00",
|
Duration = "00:45:00",
|
||||||
Sections = new List<TransportApiSection>
|
|
||||||
{
|
|
||||||
new TransportApiSection
|
|
||||||
{
|
|
||||||
Journey = new TransportApiJourney
|
|
||||||
{
|
|
||||||
Name = "IC 1",
|
|
||||||
Category = "IC",
|
|
||||||
Number = "1"
|
|
||||||
},
|
|
||||||
Departure = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
|
||||||
Departure = "2023-12-15T06:45:00",
|
|
||||||
Platform = "5"
|
Platform = "5"
|
||||||
},
|
|
||||||
Arrival = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
|
||||||
Arrival = "2023-12-15T07:30:00",
|
|
||||||
Platform = "3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_transportServiceMock
|
||||||
|
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
||||||
|
.ReturnsAsync(connection);
|
||||||
|
|
||||||
|
// Act - First call should hit the transport service
|
||||||
|
var result1 = await _transportService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Act - Second call should use cache
|
||||||
|
var result2 = await _transportService.GetConnectionAsync(shiftStartTime);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result1.ShouldNotBeNull();
|
||||||
|
result2.ShouldNotBeNull();
|
||||||
|
result1.ArrivalTime.ShouldBe(result2.ArrivalTime);
|
||||||
|
result1.DepartureTime.ShouldBe(result2.DepartureTime);
|
||||||
|
|
||||||
|
// Verify that transport service was called only once
|
||||||
|
_transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TransportApiResponse CreateMultipleConnectionsResponse()
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithNullConnection_ShouldNotCache()
|
||||||
{
|
{
|
||||||
return new TransportApiResponse
|
// Arrange
|
||||||
{
|
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
Connections = new List<TransportApiConnection>
|
|
||||||
{
|
_transportServiceMock
|
||||||
// Early connection - valid
|
.Setup(x => x.GetConnectionAsync(shiftStartTime))
|
||||||
new TransportApiConnection
|
.ReturnsAsync((TransportConnection?)null);
|
||||||
{
|
|
||||||
From = new TransportApiCheckpoint
|
// Act - First call
|
||||||
{
|
var result1 = await _transportService.GetConnectionAsync(shiftStartTime);
|
||||||
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
|
||||||
Departure = "2023-12-15T06:30:00",
|
// Act - Second call should call transport service again since null wasn't cached
|
||||||
Platform = "5"
|
var result2 = await _transportService.GetConnectionAsync(shiftStartTime);
|
||||||
},
|
|
||||||
To = new TransportApiCheckpoint
|
// Assert
|
||||||
{
|
result1.ShouldBeNull();
|
||||||
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
result2.ShouldBeNull();
|
||||||
Arrival = "2023-12-15T07:15:00",
|
|
||||||
Platform = "3"
|
// Verify that transport service was called twice (no caching for null)
|
||||||
},
|
_transportServiceMock.Verify(x => x.GetConnectionAsync(shiftStartTime), Times.Exactly(2));
|
||||||
Duration = "00:45:00"
|
|
||||||
},
|
|
||||||
// Later valid connection - should be selected as best
|
|
||||||
new TransportApiConnection
|
|
||||||
{
|
|
||||||
From = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
|
||||||
Departure = "2023-12-15T06:40:00",
|
|
||||||
Platform = "4"
|
|
||||||
},
|
|
||||||
To = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
|
||||||
Arrival = "2023-12-15T07:25:00",
|
|
||||||
Platform = "2"
|
|
||||||
},
|
|
||||||
Duration = "00:45:00"
|
|
||||||
},
|
|
||||||
// Too late connection - invalid (arrives after 07:30 which is shift start - 30 min buffer)
|
|
||||||
new TransportApiConnection
|
|
||||||
{
|
|
||||||
From = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Zurich HB", Id = "zurich" },
|
|
||||||
Departure = "2023-12-15T06:50:00",
|
|
||||||
Platform = "6"
|
|
||||||
},
|
|
||||||
To = new TransportApiCheckpoint
|
|
||||||
{
|
|
||||||
Station = new TransportApiStation { Name = "Bern", Id = "bern" },
|
|
||||||
Arrival = "2023-12-15T07:35:00",
|
|
||||||
Platform = "1"
|
|
||||||
},
|
|
||||||
Duration = "00:45:00"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
[Fact]
|
||||||
|
public async Task GetConnectionAsync_WithDifferentDates_ShouldCreateSeparateCacheEntries()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var shiftStartTime1 = new DateTime(2023, 12, 15, 8, 0, 0);
|
||||||
|
var shiftStartTime2 = new DateTime(2023, 12, 16, 8, 0, 0);
|
||||||
|
|
||||||
|
var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" };
|
||||||
|
var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" };
|
||||||
|
|
||||||
|
_transportServiceMock
|
||||||
|
.Setup(x => x.GetConnectionAsync(shiftStartTime1))
|
||||||
|
.ReturnsAsync(connection1);
|
||||||
|
|
||||||
|
_transportServiceMock
|
||||||
|
.Setup(x => x.GetConnectionAsync(shiftStartTime2))
|
||||||
|
.ReturnsAsync(connection2);
|
||||||
|
|
||||||
|
// Act - Different dates should result in different cache keys
|
||||||
|
var result1 = await _transportService.GetConnectionAsync(shiftStartTime1);
|
||||||
|
var result2 = await _transportService.GetConnectionAsync(shiftStartTime2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result1.ShouldNotBeNull();
|
||||||
|
result2.ShouldNotBeNull();
|
||||||
|
result1.ArrivalTime.ShouldBe("2023-12-15T07:30:00");
|
||||||
|
result2.ArrivalTime.ShouldBe("2023-12-16T07:30:00");
|
||||||
|
|
||||||
|
// Verify that transport service was called twice (different cache keys)
|
||||||
|
_transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTime>()), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue