Skip to content

Commit 04641b2

Browse files
CopilotabeckDev
andcommitted
Add FindTrainConnections MCP tool with tests
Co-authored-by: abeckDev <8720854+abeckDev@users.noreply.github.com>
1 parent acb3473 commit 04641b2

5 files changed

Lines changed: 616 additions & 0 deletions

File tree

AbeckDev.DbTimetable.Mcp.Test/TimeTableServiceTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,113 @@ public async Task GetStationInformation_SetsCorrectHeaders()
360360
// Assert
361361
VerifyHttpRequest(mockHandler, $"station/{pattern}", _config.ClientId, _config.ApiKey);
362362
}
363+
364+
[Fact]
365+
public async Task FindTrainConnectionsAsync_WithValidStations_ReturnsAnalysisReport()
366+
{
367+
// Arrange
368+
var stationXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
369+
<stations>
370+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
371+
</stations>";
372+
373+
var timetableXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
374+
<timetable station=""Frankfurt Hbf"">
375+
<s id=""1"">
376+
<tl c=""ICE"" n=""123"" f=""Berlin Hbf""/>
377+
<dp pt=""2511061430"" pp=""7"" ppth=""Frankfurt Hbf|Mannheim|Heidelberg|Berlin Hbf""/>
378+
</s>
379+
</timetable>";
380+
381+
var changesXml = @"<?xml version=""1.0"" encoding=""UTF-8""?><timetable/>";
382+
383+
var mockHandler = new Mock<HttpMessageHandler>();
384+
var requestCount = 0;
385+
mockHandler.Protected()
386+
.Setup<Task<HttpResponseMessage>>(
387+
"SendAsync",
388+
ItExpr.IsAny<HttpRequestMessage>(),
389+
ItExpr.IsAny<CancellationToken>())
390+
.ReturnsAsync(() =>
391+
{
392+
requestCount++;
393+
// First two calls are station lookups, third is timetable, fourth is changes
394+
if (requestCount <= 2)
395+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationXml) };
396+
if (requestCount == 3)
397+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(timetableXml) };
398+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(changesXml) };
399+
});
400+
401+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
402+
var service = new TimeTableService(httpClient, _mockOptions.Object);
403+
404+
// Act
405+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "Berlin");
406+
407+
// Assert
408+
Assert.Contains("Train Connection Analysis", result);
409+
Assert.Contains("Frankfurt Hbf", result);
410+
Assert.Contains("EVA: 8000105", result);
411+
}
412+
413+
[Fact]
414+
public async Task FindTrainConnectionsAsync_WithInvalidStationA_ReturnsError()
415+
{
416+
// Arrange
417+
var mockHandler = new Mock<HttpMessageHandler>();
418+
mockHandler.Protected()
419+
.Setup<Task<HttpResponseMessage>>(
420+
"SendAsync",
421+
ItExpr.IsAny<HttpRequestMessage>(),
422+
ItExpr.IsAny<CancellationToken>())
423+
.ReturnsAsync(new HttpResponseMessage
424+
{
425+
StatusCode = HttpStatusCode.NotFound,
426+
Content = new StringContent("Not Found")
427+
});
428+
429+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
430+
var service = new TimeTableService(httpClient, _mockOptions.Object);
431+
432+
// Act
433+
var result = await service.FindTrainConnectionsAsync("InvalidStation", "Berlin");
434+
435+
// Assert
436+
Assert.Contains("Could not find station 'InvalidStation'", result);
437+
}
438+
439+
[Fact]
440+
public async Task FindTrainConnectionsAsync_WithInvalidStationB_ReturnsError()
441+
{
442+
// Arrange
443+
var stationAXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
444+
<stations>
445+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
446+
</stations>";
447+
448+
var mockHandler = new Mock<HttpMessageHandler>();
449+
var requestCount = 0;
450+
mockHandler.Protected()
451+
.Setup<Task<HttpResponseMessage>>(
452+
"SendAsync",
453+
ItExpr.IsAny<HttpRequestMessage>(),
454+
ItExpr.IsAny<CancellationToken>())
455+
.ReturnsAsync(() =>
456+
{
457+
requestCount++;
458+
if (requestCount == 1)
459+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationAXml) };
460+
return new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound, Content = new StringContent("Not Found") };
461+
});
462+
463+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
464+
var service = new TimeTableService(httpClient, _mockOptions.Object);
465+
466+
// Act
467+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "InvalidStation");
468+
469+
// Assert
470+
Assert.Contains("Could not find station 'InvalidStation'", result);
471+
}
363472
}

AbeckDev.DbTimetable.Mcp.Test/TimetableToolsTests.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,129 @@ public async Task GetStationDetails_WithGeneralException_ReturnsUnexpectedErrorM
292292
Assert.Contains("Unexpected error:", result);
293293
Assert.Contains(exceptionMessage, result);
294294
}
295+
296+
[Fact]
297+
public async Task FindTrainConnections_WithValidStations_ReturnsConnectionAnalysis()
298+
{
299+
// Arrange
300+
var stationA = "Frankfurt";
301+
var stationB = "Berlin";
302+
var expectedResult = "=== Train Connection Analysis ===\nConnections found";
303+
304+
_mockService.Setup(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default))
305+
.ReturnsAsync(expectedResult);
306+
307+
var tools = new Tools.TimetableTools(_mockService.Object);
308+
309+
// Act
310+
var result = await tools.FindTrainConnections(stationA, stationB, null);
311+
312+
// Assert
313+
Assert.Equal(expectedResult, result);
314+
_mockService.Verify(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default), Times.Once);
315+
}
316+
317+
[Fact]
318+
public async Task FindTrainConnections_WithValidDateTime_ParsesAndCallsService()
319+
{
320+
// Arrange
321+
var stationA = "Frankfurt";
322+
var stationB = "Berlin";
323+
var dateTimeString = "2025-11-06 14:30";
324+
var parsedDateTime = DateTime.Parse(dateTimeString);
325+
var expectedResult = "Connections found";
326+
327+
_mockService.Setup(s => s.FindTrainConnectionsAsync(stationA, stationB, parsedDateTime, default))
328+
.ReturnsAsync(expectedResult);
329+
330+
var tools = new Tools.TimetableTools(_mockService.Object);
331+
332+
// Act
333+
var result = await tools.FindTrainConnections(stationA, stationB, dateTimeString);
334+
335+
// Assert
336+
Assert.Equal(expectedResult, result);
337+
_mockService.Verify(s => s.FindTrainConnectionsAsync(stationA, stationB, parsedDateTime, default), Times.Once);
338+
}
339+
340+
[Fact]
341+
public async Task FindTrainConnections_WithInvalidDateTimeFormat_ReturnsErrorMessage()
342+
{
343+
// Arrange
344+
var stationA = "Frankfurt";
345+
var stationB = "Berlin";
346+
var invalidDateTime = "invalid-date-format";
347+
348+
var tools = new Tools.TimetableTools(_mockService.Object);
349+
350+
// Act
351+
var result = await tools.FindTrainConnections(stationA, stationB, invalidDateTime);
352+
353+
// Assert
354+
Assert.Contains("Error: Invalid date format", result);
355+
Assert.Contains("yyyy-MM-dd HH:mm", result);
356+
}
357+
358+
[Fact]
359+
public async Task FindTrainConnections_WithEmptyDateTime_CallsServiceWithNull()
360+
{
361+
// Arrange
362+
var stationA = "Frankfurt";
363+
var stationB = "Berlin";
364+
var expectedResult = "Connections found";
365+
366+
_mockService.Setup(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default))
367+
.ReturnsAsync(expectedResult);
368+
369+
var tools = new Tools.TimetableTools(_mockService.Object);
370+
371+
// Act
372+
var result = await tools.FindTrainConnections(stationA, stationB, "");
373+
374+
// Assert
375+
Assert.Equal(expectedResult, result);
376+
_mockService.Verify(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default), Times.Once);
377+
}
378+
379+
[Fact]
380+
public async Task FindTrainConnections_WithHttpRequestException_ReturnsErrorMessage()
381+
{
382+
// Arrange
383+
var stationA = "Frankfurt";
384+
var stationB = "Berlin";
385+
var exceptionMessage = "Network error";
386+
387+
_mockService.Setup(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default))
388+
.ThrowsAsync(new HttpRequestException(exceptionMessage));
389+
390+
var tools = new Tools.TimetableTools(_mockService.Object);
391+
392+
// Act
393+
var result = await tools.FindTrainConnections(stationA, stationB, null);
394+
395+
// Assert
396+
Assert.Contains("Error finding train connections:", result);
397+
Assert.Contains(exceptionMessage, result);
398+
}
399+
400+
[Fact]
401+
public async Task FindTrainConnections_WithGeneralException_ReturnsUnexpectedErrorMessage()
402+
{
403+
// Arrange
404+
var stationA = "Frankfurt";
405+
var stationB = "Berlin";
406+
var exceptionMessage = "Unexpected error";
407+
408+
_mockService.Setup(s => s.FindTrainConnectionsAsync(stationA, stationB, null, default))
409+
.ThrowsAsync(new InvalidOperationException(exceptionMessage));
410+
411+
var tools = new Tools.TimetableTools(_mockService.Object);
412+
413+
// Act
414+
var result = await tools.FindTrainConnections(stationA, stationB, null);
415+
416+
// Assert
417+
Assert.Contains("Unexpected error:", result);
418+
Assert.Contains(exceptionMessage, result);
419+
}
295420
}

AbeckDev.DbTimetable.Mcp/Services/ITimeTableService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ public interface ITimeTableService
2121
/// Get information about stations given either a station name (prefix), eva number, ds100/rl100 code, wildcard (*); doesn't seem to work with umlauten in station name (prefix)
2222
/// </summary>
2323
Task<string> GetStationInformation(string pattern, CancellationToken cancellationToken = default);
24+
25+
/// <summary>
26+
/// Find train connections between two stations and assess their current status including delays and disruptions
27+
/// </summary>
28+
Task<string> FindTrainConnectionsAsync(string stationA, string stationB, DateTime? dateTime = null, CancellationToken cancellationToken = default);
2429
}

0 commit comments

Comments
 (0)