Skip to content

Commit aef1848

Browse files
authored
Merge pull request #11 from abeckDev/copilot/add-mcp-server-tool
Add FindTrainConnections MCP tool for train route analysis with real-time status
2 parents 2308401 + ed1a02c commit aef1848

6 files changed

Lines changed: 923 additions & 1 deletion

File tree

AbeckDev.DbTimetable.Mcp.Test/TimeTableServiceTests.cs

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,282 @@ 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 full 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_WithLiveDelayData_ReturnsDelayInformation()
415+
{
416+
// Arrange
417+
var stationXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
418+
<stations>
419+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
420+
</stations>";
421+
422+
var timetableXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
423+
<timetable station=""Frankfurt Hbf"">
424+
<s id=""123456"">
425+
<tl c=""ICE"" n=""123"" f=""Berlin Hbf""/>
426+
<dp pt=""2511061430"" pp=""7"" ppth=""Frankfurt Hbf|Mannheim|Berlin Hbf""/>
427+
</s>
428+
</timetable>";
429+
430+
// Changes XML with live delay data - train delayed by 10 minutes
431+
var changesXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
432+
<timetable>
433+
<s id=""123456"">
434+
<tl c=""ICE"" n=""123"" f=""Berlin Hbf""/>
435+
<dp pt=""2511061430"" ct=""2511061440"" pp=""7"" cp=""8""/>
436+
<m t=""Train delayed due to technical issues""/>
437+
</s>
438+
</timetable>";
439+
440+
var mockHandler = new Mock<HttpMessageHandler>();
441+
var requestCount = 0;
442+
mockHandler.Protected()
443+
.Setup<Task<HttpResponseMessage>>(
444+
"SendAsync",
445+
ItExpr.IsAny<HttpRequestMessage>(),
446+
ItExpr.IsAny<CancellationToken>())
447+
.ReturnsAsync(() =>
448+
{
449+
requestCount++;
450+
// First two calls are station lookups, third is timetable, fourth is full changes
451+
if (requestCount <= 2)
452+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationXml) };
453+
if (requestCount == 3)
454+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(timetableXml) };
455+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(changesXml) };
456+
});
457+
458+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
459+
var service = new TimeTableService(httpClient, _mockOptions.Object);
460+
461+
// Act
462+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "Berlin");
463+
464+
// Assert
465+
Assert.Contains("Train Connection Analysis", result);
466+
Assert.Contains("Delay: +10 minutes", result);
467+
Assert.Contains("Train delayed due to technical issues", result);
468+
Assert.Contains("Platform: 8", result); // Changed platform
469+
}
470+
471+
[Fact]
472+
public async Task FindTrainConnectionsAsync_WithInvalidStationA_ReturnsError()
473+
{
474+
// Arrange
475+
var mockHandler = new Mock<HttpMessageHandler>();
476+
mockHandler.Protected()
477+
.Setup<Task<HttpResponseMessage>>(
478+
"SendAsync",
479+
ItExpr.IsAny<HttpRequestMessage>(),
480+
ItExpr.IsAny<CancellationToken>())
481+
.ReturnsAsync(new HttpResponseMessage
482+
{
483+
StatusCode = HttpStatusCode.NotFound,
484+
Content = new StringContent("Not Found")
485+
});
486+
487+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
488+
var service = new TimeTableService(httpClient, _mockOptions.Object);
489+
490+
// Act
491+
var result = await service.FindTrainConnectionsAsync("InvalidStation", "Berlin");
492+
493+
// Assert
494+
Assert.Contains("Could not find station 'InvalidStation'", result);
495+
}
496+
497+
[Fact]
498+
public async Task FindTrainConnectionsAsync_WithInvalidStationB_ReturnsError()
499+
{
500+
// Arrange
501+
var stationAXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
502+
<stations>
503+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
504+
</stations>";
505+
506+
var mockHandler = new Mock<HttpMessageHandler>();
507+
var requestCount = 0;
508+
mockHandler.Protected()
509+
.Setup<Task<HttpResponseMessage>>(
510+
"SendAsync",
511+
ItExpr.IsAny<HttpRequestMessage>(),
512+
ItExpr.IsAny<CancellationToken>())
513+
.ReturnsAsync(() =>
514+
{
515+
requestCount++;
516+
if (requestCount == 1)
517+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationAXml) };
518+
return new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound, Content = new StringContent("Not Found") };
519+
});
520+
521+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
522+
var service = new TimeTableService(httpClient, _mockOptions.Object);
523+
524+
// Act
525+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "InvalidStation");
526+
527+
// Assert
528+
Assert.Contains("Could not find station 'InvalidStation'", result);
529+
}
530+
531+
[Fact]
532+
public async Task FindTrainConnectionsAsync_WhenNoChangesAvailable_ReturnsWarning()
533+
{
534+
// Arrange
535+
var stationXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
536+
<stations>
537+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
538+
</stations>";
539+
540+
var timetableXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
541+
<timetable station=""Frankfurt Hbf"">
542+
<s id=""123456"">
543+
<tl c=""ICE"" n=""123"" f=""Berlin Hbf""/>
544+
<dp pt=""2511061430"" pp=""7"" ppth=""Frankfurt Hbf|Mannheim|Berlin Hbf""/>
545+
</s>
546+
</timetable>";
547+
548+
var mockHandler = new Mock<HttpMessageHandler>();
549+
var requestCount = 0;
550+
mockHandler.Protected()
551+
.Setup<Task<HttpResponseMessage>>(
552+
"SendAsync",
553+
ItExpr.IsAny<HttpRequestMessage>(),
554+
ItExpr.IsAny<CancellationToken>())
555+
.ReturnsAsync(() =>
556+
{
557+
requestCount++;
558+
// First two calls are station lookups
559+
if (requestCount <= 2)
560+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationXml) };
561+
// Third is timetable
562+
if (requestCount == 3)
563+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(timetableXml) };
564+
// Fourth is full changes - simulate failure
565+
return new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError, Content = new StringContent("Server Error") };
566+
});
567+
568+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
569+
var service = new TimeTableService(httpClient, _mockOptions.Object);
570+
571+
// Act
572+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "Berlin");
573+
574+
// Assert
575+
Assert.Contains("Train Connection Analysis", result);
576+
Assert.Contains("⚠ No changes available", result);
577+
}
578+
579+
[Fact]
580+
public async Task FindTrainConnectionsAsync_WhenNoConnectionsFound_ReturnsNoConnectionsMessage()
581+
{
582+
// Arrange
583+
var stationXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
584+
<stations>
585+
<station name=""Frankfurt Hbf"" eva=""8000105"" ds100=""FF""/>
586+
</stations>";
587+
588+
var stationBXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
589+
<stations>
590+
<station name=""Konstanz"" eva=""8003400"" ds100=""KN""/>
591+
</stations>";
592+
593+
// Timetable with a train that doesn't go to Konstanz (only to Munich)
594+
var timetableXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
595+
<timetable station=""Frankfurt Hbf"">
596+
<s id=""123456"">
597+
<tl c=""ICE"" n=""123"" f=""Munich Hbf""/>
598+
<dp pt=""2511061430"" pp=""7"" ppth=""Frankfurt Hbf|Stuttgart|Munich Hbf""/>
599+
</s>
600+
</timetable>";
601+
602+
var changesXml = @"<?xml version=""1.0"" encoding=""UTF-8""?><timetable/>";
603+
604+
var mockHandler = new Mock<HttpMessageHandler>();
605+
var requestCount = 0;
606+
mockHandler.Protected()
607+
.Setup<Task<HttpResponseMessage>>(
608+
"SendAsync",
609+
ItExpr.IsAny<HttpRequestMessage>(),
610+
ItExpr.IsAny<CancellationToken>())
611+
.ReturnsAsync(() =>
612+
{
613+
requestCount++;
614+
// First station lookup (Frankfurt)
615+
if (requestCount == 1)
616+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationXml) };
617+
// Second station lookup (Konstanz)
618+
if (requestCount == 2)
619+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(stationBXml) };
620+
// Third is timetable
621+
if (requestCount == 3)
622+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(timetableXml) };
623+
// Fourth is full changes
624+
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(changesXml) };
625+
});
626+
627+
var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri(_config.BaseUrl) };
628+
var service = new TimeTableService(httpClient, _mockOptions.Object);
629+
630+
// Act
631+
var result = await service.FindTrainConnectionsAsync("Frankfurt", "Konstanz");
632+
633+
// Assert
634+
Assert.Contains("Train Connection Analysis", result);
635+
Assert.Contains("⚠ No direct connections found in the current timetable.", result);
636+
Assert.Contains("This could mean:", result);
637+
Assert.Contains("- No direct trains operate between these stations", result);
638+
Assert.Contains("- Trains may require a transfer", result);
639+
Assert.Contains("- Try a different time or date", result);
640+
}
363641
}

0 commit comments

Comments
 (0)