@@ -638,4 +638,183 @@ public async Task FindTrainConnectionsAsync_WhenNoConnectionsFound_ReturnsNoConn
638638 Assert . Contains ( "- Trains may require a transfer" , result ) ;
639639 Assert . Contains ( "- Try a different time or date" , result ) ;
640640 }
641+
642+ [ Fact ]
643+ public async Task GetStationBoardAsync_WithProvidedDate_UsesDateDirectlyWithoutTimezoneConversion ( )
644+ {
645+ // Arrange
646+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
647+ mockHandler . Protected ( )
648+ . Setup < Task < HttpResponseMessage > > (
649+ "SendAsync" ,
650+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
651+ ItExpr . IsAny < CancellationToken > ( ) )
652+ . ReturnsAsync ( new HttpResponseMessage
653+ {
654+ StatusCode = HttpStatusCode . OK ,
655+ Content = new StringContent ( _testXmlResponse )
656+ } ) ;
657+
658+ var httpClient = new HttpClient ( mockHandler . Object )
659+ {
660+ BaseAddress = new Uri ( _config . BaseUrl )
661+ } ;
662+ var service = new TimeTableService ( httpClient , _mockOptions . Object ) ;
663+ var evaNo = "8000105" ;
664+
665+ // Provide a specific German time: 2025-11-06 14:30
666+ var germanTime = new DateTime ( 2025 , 11 , 6 , 14 , 30 , 0 ) ;
667+
668+ // Act
669+ var result = await service . GetStationBoardAsync ( evaNo , germanTime ) ;
670+
671+ // Assert
672+ Assert . Equal ( _testXmlResponse , result ) ;
673+
674+ // Verify the request path contains the exact date and hour from the provided date
675+ // without any timezone conversion
676+ mockHandler . Protected ( ) . Verify (
677+ "SendAsync" ,
678+ Times . Once ( ) ,
679+ ItExpr . Is < HttpRequestMessage > ( req =>
680+ req . RequestUri ! . PathAndQuery . Contains ( "plan/8000105/251106/14" ) ) ,
681+ ItExpr . IsAny < CancellationToken > ( ) ) ;
682+ }
683+
684+ [ Fact ]
685+ public async Task GetStationBoardAsync_WithoutDate_UsesGermanTimezone ( )
686+ {
687+ // Arrange
688+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
689+ string ? capturedPath = null ;
690+ mockHandler . Protected ( )
691+ . Setup < Task < HttpResponseMessage > > (
692+ "SendAsync" ,
693+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
694+ ItExpr . IsAny < CancellationToken > ( ) )
695+ . ReturnsAsync ( ( HttpRequestMessage req , CancellationToken ct ) =>
696+ {
697+ capturedPath = req . RequestUri ? . PathAndQuery ;
698+ return new HttpResponseMessage
699+ {
700+ StatusCode = HttpStatusCode . OK ,
701+ Content = new StringContent ( _testXmlResponse )
702+ } ;
703+ } ) ;
704+
705+ var httpClient = new HttpClient ( mockHandler . Object )
706+ {
707+ BaseAddress = new Uri ( _config . BaseUrl )
708+ } ;
709+ var service = new TimeTableService ( httpClient , _mockOptions . Object ) ;
710+ var evaNo = "8000105" ;
711+
712+ // Calculate expected German time
713+ var berlinTz = TimeZoneInfo . FindSystemTimeZoneById ( "Europe/Berlin" ) ;
714+ var expectedGermanTime = TimeZoneInfo . ConvertTimeFromUtc ( DateTime . UtcNow , berlinTz ) ;
715+ var expectedDate = expectedGermanTime . ToString ( "yyMMdd" ) ;
716+ var expectedHour = expectedGermanTime . ToString ( "HH" ) ;
717+
718+ // Act
719+ var result = await service . GetStationBoardAsync ( evaNo ) ;
720+
721+ // Assert
722+ Assert . Equal ( _testXmlResponse , result ) ;
723+ Assert . NotNull ( capturedPath ) ;
724+ Assert . Contains ( $ "plan/{ evaNo } /{ expectedDate } /{ expectedHour } ", capturedPath ) ;
725+ }
726+
727+ [ Fact ]
728+ public async Task GetStationBoardAsync_WhenTimezoneNotAvailable_UsesFallback ( )
729+ {
730+ // This test verifies the fallback behavior when Europe/Berlin timezone cannot be found
731+ // Note: This is difficult to test directly since we can't easily mock TimeZoneInfo.FindSystemTimeZoneById
732+ // In actual deployment, the Europe/Berlin timezone should always be available on the system
733+ // The fallback to DateTime.Now is a defensive measure for edge cases
734+
735+ // Arrange
736+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
737+ string ? capturedPath = null ;
738+ mockHandler . Protected ( )
739+ . Setup < Task < HttpResponseMessage > > (
740+ "SendAsync" ,
741+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
742+ ItExpr . IsAny < CancellationToken > ( ) )
743+ . ReturnsAsync ( ( HttpRequestMessage req , CancellationToken ct ) =>
744+ {
745+ capturedPath = req . RequestUri ? . PathAndQuery ;
746+ return new HttpResponseMessage
747+ {
748+ StatusCode = HttpStatusCode . OK ,
749+ Content = new StringContent ( _testXmlResponse )
750+ } ;
751+ } ) ;
752+
753+ var httpClient = new HttpClient ( mockHandler . Object )
754+ {
755+ BaseAddress = new Uri ( _config . BaseUrl )
756+ } ;
757+ var service = new TimeTableService ( httpClient , _mockOptions . Object ) ;
758+ var evaNo = "8000105" ;
759+
760+ // Act
761+ var result = await service . GetStationBoardAsync ( evaNo ) ;
762+
763+ // Assert
764+ // The method should succeed and return a result regardless of timezone availability
765+ Assert . Equal ( _testXmlResponse , result ) ;
766+ Assert . NotNull ( capturedPath ) ;
767+ // Verify that a valid path was created with date and hour components
768+ Assert . Matches ( @"plan/\d+/\d{6}/\d{2}" , capturedPath ) ;
769+ }
770+
771+ [ Fact ]
772+ public async Task GetStationBoardAsync_WithMultipleCalls_UsesConsistentTimezoneLogic ( )
773+ {
774+ // Arrange
775+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
776+ var capturedPaths = new List < string > ( ) ;
777+ mockHandler . Protected ( )
778+ . Setup < Task < HttpResponseMessage > > (
779+ "SendAsync" ,
780+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
781+ ItExpr . IsAny < CancellationToken > ( ) )
782+ . ReturnsAsync ( ( HttpRequestMessage req , CancellationToken ct ) =>
783+ {
784+ if ( req . RequestUri ? . PathAndQuery != null )
785+ capturedPaths . Add ( req . RequestUri . PathAndQuery ) ;
786+ return new HttpResponseMessage
787+ {
788+ StatusCode = HttpStatusCode . OK ,
789+ Content = new StringContent ( _testXmlResponse )
790+ } ;
791+ } ) ;
792+
793+ var httpClient = new HttpClient ( mockHandler . Object )
794+ {
795+ BaseAddress = new Uri ( _config . BaseUrl )
796+ } ;
797+ var service = new TimeTableService ( httpClient , _mockOptions . Object ) ;
798+ var evaNo = "8000105" ;
799+ var specificDate = new DateTime ( 2025 , 12 , 25 , 10 , 15 , 0 ) ;
800+
801+ // Act
802+ // First call without date (should use German timezone)
803+ await service . GetStationBoardAsync ( evaNo ) ;
804+ // Second call with specific date (should use provided date directly)
805+ await service . GetStationBoardAsync ( evaNo , specificDate ) ;
806+ // Third call without date again (should use German timezone)
807+ await service . GetStationBoardAsync ( evaNo ) ;
808+
809+ // Assert
810+ Assert . Equal ( 3 , capturedPaths . Count ) ;
811+
812+ // Second call should use the exact provided date
813+ Assert . Contains ( "plan/8000105/251225/10" , capturedPaths [ 1 ] ) ;
814+
815+ // First and third calls should use current German time (so they should be similar)
816+ // Both should contain valid date patterns
817+ Assert . Matches ( @"plan/\d+/\d{6}/\d{2}" , capturedPaths [ 0 ] ) ;
818+ Assert . Matches ( @"plan/\d+/\d{6}/\d{2}" , capturedPaths [ 2 ] ) ;
819+ }
641820}
0 commit comments