-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathtest_irail.py
497 lines (424 loc) · 20.4 KB
/
test_irail.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
"""Unit tests for the iRail API wrapper."""
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, patch
from aiohttp import ClientSession
import pytest
from pyrail.irail import iRail
from pyrail.models import (
Alert,
ApiResponse,
CompositionApiResponse,
ConnectionDetails,
ConnectionsApiResponse,
Disturbance,
DisturbancesApiResponse,
DisturbanceType,
LiveboardApiResponse,
LiveboardDeparture,
Occupancy,
PlatformInfo,
StationDetails,
StationsApiResponse,
VehicleApiResponse,
VehicleInfo,
_str_to_bool,
_timestamp_to_datetime,
)
@pytest.mark.asyncio
@patch("pyrail.irail.ClientSession.get")
async def test_successful_request(mock_get):
"""Test a successful API request by mocking the iRail response."""
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={"data": "some_data"})
mock_get.return_value.__aenter__.return_value = mock_response
async with iRail() as api:
assert api.session is not None
response = await api._do_request("stations")
mock_get.assert_called_once_with(
"https://api.irail.be/stations/",
params={"format": "json", "lang": "en"},
headers={"User-Agent": "pyRail (https://github.com/tjorim/pyrail; tielemans.jorim@gmail.com)"},
)
assert response == {"data": "some_data"}
assert mock_response.status == 200
@pytest.mark.asyncio
async def test_irail_context_manager():
"""Ensure that the async context manager sets up and tears down the session properly."""
async with iRail() as irail:
assert irail.session is not None
assert isinstance(irail.session, ClientSession)
@pytest.mark.asyncio
async def test_get_stations():
"""Test the get_stations endpoint.
Verifies that:
- The response is not None
- The response is a dictionary
- The response contains a 'station' key
- The station list is non-empty
"""
async with iRail() as api:
stations = await api.get_stations()
# Ensure the response is not None
assert stations is not None, "The response should not be None"
# Validate that the response is a StationsApiResponse object
assert isinstance(stations, StationsApiResponse), "Expected the response to be a StationsApiResponse object"
# Validate the structure of station data
station_list = stations.stations
assert isinstance(station_list, list), "Expected 'station' to be a list"
assert len(station_list) > 0, "Expected at least one station in the response"
assert isinstance(station_list[0], StationDetails), "Expected the first station to be a StationDetails object"
@pytest.mark.asyncio
async def test_get_liveboard():
"""Test the get_liveboard endpoint.
Verifies that:
- The response is not None
- The response is a LiveboardApiResponse object
- The response contains a 'departures' key
- The departure list is non-empty
"""
async with iRail() as api:
liveboard = await api.get_liveboard("Brussels-Central")
# Ensure the response is not None
assert liveboard is not None, "The response should not be None"
# Validate that the response is a LiveboardApiResponse object
assert isinstance(liveboard, LiveboardApiResponse), "Expected response to be a dictionary"
# Validate the structure of departure data
departure_list = liveboard.departures
assert isinstance(departure_list, list), "Expected 'departure' to be a list"
assert len(departure_list) > 0, "Expected at least one departure in the response"
assert isinstance(departure_list[0], LiveboardDeparture), (
"Expected the first departure to be a LiveboardDeparture object"
)
# Test VehicleInfo dataclass
assert isinstance(departure_list[0].vehicle_info, VehicleInfo), (
"Expected vehicle_info to be a VehicleInfo object"
)
@pytest.mark.asyncio
async def test_get_connections():
"""Test the get_connections endpoint.
Verifies that:
- The response is not None
- The response is a ConnectionsApiResponse object
- The response contains a 'connections' key
- The connection list is non-empty
"""
async with iRail() as api:
connections = await api.get_connections("Antwerpen-Centraal", "Brussel-Centraal")
# Ensure the response is not None
assert connections is not None, "The response should not be None"
# Validate that the response is a ConnectionsApiResponse object
assert isinstance(connections, ConnectionsApiResponse), "Expected response to be a dictionary"
# Validate the structure of connection data
connection_list = connections.connections
assert isinstance(connection_list, list), "Expected 'connection' to be a list"
assert len(connection_list) > 0, "Expected at least one connection in the response"
assert isinstance(connection_list[0], ConnectionDetails), (
"Expected the first connection to be a ConnectionDetails object"
)
@pytest.mark.asyncio
async def test_get_vehicle():
"""Test the get_vehicle endpoint.
Verifies that:
- The response is not None
- The response is a dictionary
- The response contains a 'vehicle' key
- The vehicle list is non-empty
"""
async with iRail() as api:
vehicle = await api.get_vehicle("IC538")
assert vehicle is not None, "The response should not be None"
assert isinstance(vehicle, VehicleApiResponse), "Expected response to be a VehicleApiResponse object"
assert isinstance(vehicle.vehicle_info, VehicleInfo), "Expected vehicle_info to be a VehicleInfo object"
assert isinstance(vehicle.stops, list), "Expected 'stop' to be a list"
assert len(vehicle.stops) > 0, "Expected at least one stop"
if len(vehicle.stops) > 0:
stop = vehicle.stops[0]
assert isinstance(stop.platform_info, PlatformInfo), "Expected platform_info to be a PlatformInfo object"
assert isinstance(stop.occupancy, Occupancy), "Expected occupancy to be an Occupancy object"
@pytest.mark.asyncio
async def test_get_composition():
"""Test the get_composition endpoint.
Verifies that:
- The response is not None
- The response is a CompositionApiResponse object
- The response contains a 'composition' key
- The composition segments list is non-empty
- The segment has valid attributes
- The composition units list is non-empty
- The unit has valid attributes
"""
async with iRail() as api:
composition = await api.get_composition("IC538")
assert composition is not None, "The response should not be None"
assert isinstance(composition, CompositionApiResponse), (
"Expected response to be a CompositionApiResponse object"
)
# Test segments structure
segments = composition.composition
assert isinstance(segments, list), "Expected 'segments' to be a list"
assert len(segments) > 0, "Expected 'number' to be a non-negative integer"
if len(segments) > 0:
segment = segments[0]
assert isinstance(segment.origin, StationDetails), "Expected origin to be a StationDetails object"
assert isinstance(segment.destination, StationDetails), "Expected destination to be a StationDetails object"
# Test units in composition
units = segment.composition.units
assert len(units) > 0, "Expected 'number' to be a non-negative integer"
if len(units) > 0:
unit = units[0]
assert isinstance(unit.has_toilets, bool), "Expected 'has_toilets' to be a boolean"
assert isinstance(unit.seats_first_class, int), "Expected 'seats_first_class' to be an integer"
assert isinstance(unit.length_in_meter, int), "Expected 'length_in_meter' to be an integer"
@pytest.mark.asyncio
async def test_get_disturbances():
"""Test the get_disturbances endpoint.
Verifies that:
- The response is not None
- The response is a DisturbancesApiResponse object
- The response contains a 'disturbances' key
- The disturbances list is non-empty
- The disturbance has valid attributes
"""
async with iRail() as api:
disturbances = await api.get_disturbances()
assert disturbances is not None, "The response should not be None"
assert isinstance(disturbances, DisturbancesApiResponse), (
"Expected response to be a DisturbancesApiResponse object"
)
assert isinstance(disturbances.disturbances, list), "Expected 'disturbances' to be a list"
# Test disturbance attributes
if len(disturbances.disturbances) > 0:
disturbance = disturbances.disturbances[0]
assert isinstance(disturbance.title, str), "Expected 'title' to be a string"
assert isinstance(disturbance.description, str), "Expected 'description' to be a string"
assert disturbance.type in DisturbanceType, "Expected 'type' to be 'disturbance' or 'planned'"
@pytest.mark.asyncio
async def test_date_time_validation():
"""Test date and time format validation."""
async with iRail() as api:
# Valid date examples
assert api._validate_date("150923") # September 15, 2023
assert api._validate_date("010124") # January 1, 2024
assert api._validate_date(None) # None is valid (uses current date)
assert api._validate_date("290224") # Leap year 2024
# Invalid date examples
assert not api._validate_date("320923") # Invalid day
assert not api._validate_date("151323") # Invalid month
assert not api._validate_date("abcdef") # Not numeric
assert not api._validate_date("15092023") # Too long
assert not api._validate_date("0") # Too short
assert not api._validate_date("290223") # Invalid leap year 2023
# Valid time examples
assert api._validate_time("1430") # 2:30 PM
assert api._validate_time("0000") # Midnight
assert api._validate_time("2359") # 11:59 PM
assert api._validate_time(None) # None is valid (uses current time)
assert api._validate_time("0001") # 12:01 AM
# Invalid time examples
assert not api._validate_time("2460") # Invalid hour
assert not api._validate_time("2361") # Invalid minute
assert not api._validate_time("abcd") # Not numeric
assert not api._validate_time("143000") # Too long
assert not api._validate_time("1") # Too short
@pytest.mark.asyncio
async def test_liveboard_with_date_time():
"""Test liveboard request with date and time parameters."""
async with iRail() as api:
# Valid date/time
result = await api.get_liveboard(
station="Brussels-Central",
date=datetime.now().strftime("%d%m%y"),
time="1430", # 2:30 PM
)
assert result is not None
# Test with future date
result = await api.get_liveboard(
station="Brussels-Central",
date=(datetime.now() + timedelta(days=1)).strftime("%d%m%y"),
)
assert result is not None
# Invalid date
result = await api.get_liveboard(
station="Brussels-Central",
date="320923", # Invalid day 32
)
assert result is None
# Invalid time
result = await api.get_liveboard(
station="Brussels-Central",
time="2460", # Invalid hour 24
)
assert result is None
@pytest.mark.asyncio
async def test_error_handling():
"""Test error handling for various API endpoints with invalid data.
Verifies that:
- The response is None for invalid data
"""
async with iRail() as api:
# Test with invalid vehicle ID
vehicle = await api.get_vehicle("INVALID_ID")
assert vehicle is None, "Expected None for invalid vehicle ID"
# Test with invalid station for liveboard
liveboard = await api.get_liveboard("INVALID_STATION")
assert liveboard is None, "Expected None for invalid station"
# Test with invalid train ID for composition
composition = await api.get_composition("INVALID_TRAIN")
assert composition is None, "Expected None for invalid train ID"
# Test with invalid station for connections
connections = await api.get_connections("InvalidStation1", "InvalidStation2")
assert connections is None, "Expected None for invalid stations"
@pytest.mark.asyncio
@pytest.mark.skip(reason="Timezone is different on different systems")
async def test_timestamp_to_datetime():
"""Test the timestamp_to_datetime function."""
# Test valid timestamps
assert _timestamp_to_datetime("1705593600") == datetime(2024, 1, 18, 17, 0) # 2024-01-18 16:00:00
assert _timestamp_to_datetime("0") == datetime(1970, 1, 1, 1, 0) # Unix epoch
@pytest.mark.asyncio
@pytest.mark.skip(reason="Timezone is different on different systems")
async def test_timestamp_field_deserialization():
"""Test timestamp field deserialization in various models."""
# Test ApiResponse timestamp
api_response = ApiResponse.from_dict({"version": "1.0", "timestamp": "1705593600"})
assert api_response.timestamp == datetime(2024, 1, 18, 17, 0)
# Test LiveboardDeparture time
departure = LiveboardDeparture.from_dict(
{
"id": "0",
"station": "Brussels-South/Brussels-Midi",
"stationinfo": {
"@id": "http://irail.be/stations/NMBS/008814001",
"id": "BE.NMBS.008814001",
"name": "Brussels-South/Brussels-Midi",
"locationX": "4.336531",
"locationY": "50.835707",
"standardname": "Brussel-Zuid/Bruxelles-Midi",
},
"time": "1705593600",
"delay": "0",
"canceled": "0",
"left": "0",
"isExtra": "0",
"vehicle": "BE.NMBS.EC9272",
"vehicleinfo": {
"name": "BE.NMBS.EC9272",
"shortname": "EC 9272",
"number": "9272",
"type": "EC",
"locationX": "0",
"locationY": "0",
"@id": "http://irail.be/vehicle/EC9272",
},
"platform": "23",
"platforminfo": {"name": "23", "normal": "1"},
"occupancy": {"@id": "http://api.irail.be/terms/low", "name": "low"},
"departureConnection": "http://irail.be/connections/8821006/20250106/EC9272",
}
)
assert departure.time == datetime(2024, 1, 18, 17, 0)
# Test Alert start_time and end_time
alert = Alert.from_dict(
{
"id": "0",
"header": "Anvers-Central / Antwerpen-Centraal - Anvers-Berchem / Antwerpen-Berchem",
"description": "During the weekends, from 4 to 19/01 Infrabel is working on the track. The departure times of this train change. The travel planner takes these changes into account.",
"lead": "During the weekends, from 4 to 19/01 Infrabel is working on the track",
"startTime": "1705593600",
"endTime": "1705597200",
}
)
assert alert.start_time == datetime(2024, 1, 18, 17, 0)
assert alert.end_time == datetime(2024, 1, 18, 18, 0)
# Test Disturbance timestamp
disturbance = Disturbance.from_dict(
{
"id": "1",
"title": "Mouscron / Moeskroen - Lille Flandres (FR)",
"description": "On weekdays from 6 to 17/01 works will take place on the French rail network.An SNCB bus replaces some IC trains Courtrai / Kortrijk - Mouscron / Moeskroen - Lille Flandres (FR) between Mouscron / Moeskroen and Lille Flandres (FR).The travel planner takes these changes into account.Meer info over de NMBS-bussen (FAQ)En savoir plus sur les bus SNCB (FAQ)Où prendre mon bus ?Waar is mijn bushalte?",
"type": "planned",
"link": "https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus",
"timestamp": "1705593600",
"richtext": "On weekdays from 6 to 17/01 works will take place on the French rail network.An SNCB bus replaces some IC trains Courtrai / Kortrijk - Mouscron / Moeskroen - Lille Flandres (FR) between Mouscron / Moeskroen and Lille Flandres (FR).The travel planner takes these changes into account.<br><a href='https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus'>Meer info over de NMBS-bussen (FAQ)</a><br><a href='https://www.belgiantrain.be/fr/support/faq/faq-routes-schedules/faq-bus'>En savoir plus sur les bus SNCB (FAQ)</a><br><a href='https://www.belgianrail.be/jp/download/brail_him/1736172333792_FR_2501250_S.pdf'>Où prendre mon bus ?</a><br><a href='https://www.belgianrail.be/jp/download/brail_him/1736172333804_NL_2501250_S.pdf'>Waar is mijn bushalte?</a>",
"descriptionLinks": {
"number": "4",
"descriptionLink": [
{
"id": "0",
"link": "https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus",
"text": "Meer info over de NMBS-bussen (FAQ)",
},
{
"id": "1",
"link": "https://www.belgiantrain.be/fr/support/faq/faq-routes-schedules/faq-bus",
"text": "En savoir plus sur les bus SNCB (FAQ)",
},
{
"id": "2",
"link": "https://www.belgianrail.be/jp/download/brail_him/1736172333792_FR_2501250_S.pdf",
"text": "Où prendre mon bus ?",
},
{
"id": "3",
"link": "https://www.belgianrail.be/jp/download/brail_him/1736172333804_NL_2501250_S.pdf",
"text": "Waar is mijn bushalte?",
},
],
},
}
)
assert disturbance.timestamp == datetime(2024, 1, 18, 17, 0)
@pytest.mark.asyncio
async def test_str_to_bool():
"""Test the str_to_bool function that converts string values to boolean."""
# Test valid inputs
assert _str_to_bool("1") is True, "String '1' should convert to True"
assert _str_to_bool("0") is False, "String '0' should convert to False"
@pytest.mark.asyncio
async def test_boolean_field_deserialization():
"""Test the deserialization of boolean fields in models."""
# Test PlatformInfo boolean field
platform = PlatformInfo.from_dict({"name": "1", "normal": "1"})
assert platform.normal is True, "Platform normal field should be True when '1'"
platform = PlatformInfo.from_dict({"name": "1", "normal": "0"})
assert platform.normal is False, "Platform normal field should be False when '0'"
# Test LiveboardDeparture multiple boolean fields
departure = LiveboardDeparture.from_dict(
{
"id": "1",
"station": "Brussels",
"stationinfo": {
"@id": "1",
"id": "1",
"name": "Brussels",
"locationX": 4.3517,
"locationY": 50.8503,
"standardname": "Brussels-Central",
},
"time": "1705593600", # Example timestamp
"delay": 0,
"canceled": "1",
"left": "0",
"isExtra": "1",
"vehicle": "BE.NMBS.IC1234",
"vehicleinfo": {
"name": "IC1234",
"shortname": "IC1234",
"number": "1234",
"type": "IC",
"locationX": 4.3517,
"locationY": 50.8503,
"@id": "1",
},
"platform": "1",
"platforminfo": {"name": "1", "normal": "1"},
"occupancy": {"@id": "http://api.irail.be/terms/low", "name": "low"},
"departureConnection": "1",
}
)
# Verify boolean fields are correctly deserialized
assert departure.canceled is True, "Departure canceled field should be True when '1'"
assert departure.left is False, "Departure left field should be False when '0'"
assert departure.is_extra is True, "Departure is_extra field should be True when '1'"
assert departure.platform_info.normal is True, "Platform normal field should be True when '1'"