Initial import of ADIF API reverse-engineering toolkit

This commit is contained in:
2025-12-16 08:37:56 +01:00
commit 60388529c1
11486 changed files with 1086536 additions and 0 deletions

158
tests/README.md Normal file
View File

@@ -0,0 +1,158 @@
# Tests - ADIF API
Test scripts to validate the functionality of the ADIF API.
## Active Tests
### test_endpoints_detailed.py
Exhaustive test of all endpoints with complete debug information.
**Features**:
- Shows status codes, headers and JSON response
- Tests multiple payload variations
- Identifies 400, 401 errors and their causes
- Useful for debugging new endpoints
**Usage**:
```bash
python3 tests/test_endpoints_detailed.py
```
**Expected output**:
- Detailed information for each request
- Error analysis with server messages
- Differentiation between payload vs permission errors
---
### test_onepaths_with_real_trains.py
Functional test that gets real trains and tests the `onepaths` endpoint.
**Features**:
- Queries `departures` to get running trains
- Extracts `commercialNumber`, `launchingDate`, station codes
- Tests `onepaths` with real data
- Validates that the endpoint works correctly
**Usage**:
```bash
python3 tests/test_onepaths_with_real_trains.py
```
**Requirements**:
- Run during the day (when trains are running)
- If run at night/early morning may not find trains
**Expected output**:
```
======================================================================
STEP 1: Getting real trains from Madrid Atocha
======================================================================
Got 25 trains
======================================================================
STEP 2: Testing onePaths with real trains
======================================================================
SUCCESS! onePaths works with real data
```
---
## Archived Tests
The `archived/` folder contains old tests that were useful during development but are no longer necessary:
- `test_all_endpoints.py` - Simple version without debug
- `test_complete_bodies.py` - Complete payload tests
- `test_corrected_api.py` / `test_corrected_api_v2.py` - Previous versions
- `test_real_auth.py` - Basic authentication tests
- `test_simple.py` - Minimalist test
- `test_with_auth_headers.py` - Header validation
- `test_without_auth.py` - Test without authentication
- `debug_auth.py` - HMAC algorithm debug
These tests are kept in case they're useful as reference, but the active tests are more complete.
---
## Test Structure
### Basic Template
```python
from adif_auth import AdifAuthenticator
import requests
import uuid
ACCESS_KEY = "and20210615"
SECRET_KEY = "Jthjtr946RTt"
def test_endpoint():
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
url = "https://circulacion.api.adif.es/portroyalmanager/secure/..."
payload = {
# Your payload here
}
user_id = str(uuid.uuid4())
headers = auth.get_auth_headers("POST", url, payload, user_id=user_id)
headers["User-key"] = auth.USER_KEY_CIRCULATION
response = requests.post(url, json=payload, headers=headers, timeout=10)
assert response.status_code == 200
print(f"Test passed: {response.json()}")
if __name__ == "__main__":
test_endpoint()
```
### Status Code Analysis
```python
if response.status_code == 200:
print("SUCCESS - Endpoint functional")
data = response.json()
elif response.status_code == 204:
print("NO CONTENT - Correct authentication but no data")
elif response.status_code == 400:
print("BAD REQUEST - Incorrect payload")
print(f"Error: {response.json()}")
elif response.status_code == 401:
print("UNAUTHORIZED - No permissions")
print(f"Error: {response.json()}")
```
---
## Expected Results
### Functional Endpoints (200)
- `/departures/traffictype/`
- `/arrivals/traffictype/`
- `/onepaths/` (with real commercialNumber)
- `/stationsobservations/`
- `/betweenstations/traffictype/`
- `/onestation/`
- `/allstations/reducedinfo/{token}/`
### Endpoints returning 204 (Success without data)
- `/severalpaths/`
- `/compositions/path/`
---
## Tips for Creating New Tests
1. **Use `test_endpoints_detailed.py` as base** - Has good error handling
2. **Validate timestamps** - Use milliseconds, not seconds
3. **Test with real data** - Like `test_onepaths_with_real_trains.py` does
4. **Differentiate errors**:
- 400 = Incorrect payload -> Check fields
- 401 = No permissions -> Keys don't have access
- 204 = No data -> Authentication OK, but empty response

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Tests for ADIF endpoints using HMAC-SHA256 authentication.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any, Dict
import requests
from requests import Response
# Add project root to sys.path to import adif_auth
sys.path.insert(0, str(Path(__file__).parent.parent))
from adif_auth import AdifAuthenticator # noqa: E402
# Keys extracted with Frida
ACCESS_KEY = "and20210615"
SECRET_KEY = "Jthjtr946RTt"
USER_ID = "0c8c32dce47f8512"
auth = AdifAuthenticator(ACCESS_KEY, SECRET_KEY)
def build_headers(url: str, payload: Dict[str, Any]) -> Dict[str, str]:
"""Create authenticated headers including the correct User-key for the host."""
headers = auth.get_auth_headers("POST", url, payload, user_id=USER_ID)
headers["User-key"] = auth.get_user_key_for_url(url)
return headers
def test_departures() -> None:
"""Test departures endpoint."""
print("\n" + "=" * 70)
print("TEST: Departures from Madrid Atocha (Commuter)")
print("=" * 70)
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/"
payload: Dict[str, Any] = {
"stationCode": "10200",
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {"pageNumber": 0},
"trafficType": "CERCANIAS",
}
headers = build_headers(url, payload)
print(f"\nURL: {url}")
print(f"Payload: {json.dumps(payload)}")
print("\nHeaders:")
for key, value in headers.items():
preview = f"{value[:50]}..." if key == "Authorization" else value
print(f" {key}: {preview}")
try:
response: Response = requests.post(
url, headers=headers, json=payload, timeout=10
)
print(f"\nStatus Code: {response.status_code}")
if response.status_code == 200:
print("✅ SUCCESS!")
data = response.json()
print("\nResponse (preview):")
print(json.dumps(data, indent=2, ensure_ascii=False)[:1000])
else:
print("❌ ERROR")
print(f"Response: {response.text[:500]}")
except Exception as exc: # pragma: no cover - interactive script
print(f"❌ EXCEPTION: {exc}")
def test_station_details() -> None:
"""Test station details endpoint."""
print("\n" + "=" * 70)
print("TEST: Station details Madrid Atocha")
print("=" * 70)
url = "https://estaciones.api.adif.es/portroyalmanager/secure/stations/onestation/"
payload: Dict[str, Any] = {"stationCode": "10200"}
headers = build_headers(url, payload)
print(f"\nURL: {url}")
print(f"Payload: {json.dumps(payload)}")
try:
response: Response = requests.post(
url, headers=headers, json=payload, timeout=10
)
print(f"\nStatus Code: {response.status_code}")
if response.status_code == 200:
print("✅ SUCCESS!")
data = response.json()
print("\nResponse (preview):")
print(json.dumps(data, indent=2, ensure_ascii=False)[:1000])
else:
print("❌ ERROR")
print(f"Response: {response.text[:500]}")
except Exception as exc: # pragma: no cover
print(f"❌ EXCEPTION: {exc}")
def test_arrivals() -> None:
"""Test arrivals endpoint."""
print("\n" + "=" * 70)
print("TEST: Arrivals to Madrid Atocha (All traffic types)")
print("=" * 70)
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/arrivals/traffictype/"
payload: Dict[str, Any] = {
"stationCode": "10200",
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {"pageNumber": 0},
"trafficType": "ALL",
}
headers = build_headers(url, payload)
print(f"\nURL: {url}")
try:
response: Response = requests.post(
url, headers=headers, json=payload, timeout=10
)
print(f"\nStatus Code: {response.status_code}")
if response.status_code == 200:
print("✅ SUCCESS!")
data = response.json()
print("\nResponse (preview):")
print(json.dumps(data, indent=2, ensure_ascii=False)[:800])
else:
print("❌ ERROR")
print(f"Response: {response.text[:500]}")
except Exception as exc: # pragma: no cover
print(f"❌ EXCEPTION: {exc}")
if __name__ == "__main__":
print("\n" + "=" * 70)
print("ADIF API TESTS WITH HMAC-SHA256 AUTHENTICATION")
print("=" * 70)
print("\nUsing keys:")
print(f" ACCESS_KEY: {ACCESS_KEY}")
print(f" SECRET_KEY: {SECRET_KEY}")
print(f" USER_ID: {USER_ID}")
test_departures()
test_station_details()
test_arrivals()
print("\n" + "=" * 70)
print("TESTS COMPLETED")
print("=" * 70)

185
tests/test_endpoints.py Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Script to test different ADIF API endpoints with signed requests.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any, Dict
import requests
from requests import Response
# Make adif_auth importable when running this script directly
sys.path.insert(0, str(Path(__file__).parent.parent))
from adif_auth import AdifAuthenticator # noqa: E402
ACCESS_KEY: str = "and20210615"
SECRET_KEY: str = "Jthjtr946RTt"
BASE_CIRCULATION = "https://circulacion.api.adif.es"
BASE_STATIONS = "https://estaciones.api.adif.es"
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
def _headers_for(
url: str, payload: Dict[str, Any], use_stations_key: bool = False
) -> Dict[str, str]:
"""Build authentication headers for a given request body and URL."""
headers = auth.get_auth_headers("POST", url, payload)
headers["User-key"] = (
auth.USER_KEY_STATIONS if use_stations_key else auth.USER_KEY_CIRCULATION
)
return headers
def test_endpoint(
name: str, url: str, payload: Dict[str, Any], use_stations_key: bool = False
) -> None:
"""Test an endpoint and show a compact preview of the response."""
print(f"\n{'='*60}")
print(f"TEST: {name}")
print(f"{'='*60}")
headers = _headers_for(url, payload, use_stations_key=use_stations_key)
try:
response: Response = requests.post(
url, headers=headers, json=payload, timeout=10
)
print(f"\nStatus: {response.status_code}")
if response.status_code == 200:
print("✅ SUCCESS")
result = response.json()
print("\nResponse Preview:")
print(json.dumps(result, indent=2, ensure_ascii=False)[:1000] + "...")
elif response.status_code == 204:
print("⚠️ NO CONTENT")
else:
print("❌ ERROR")
print(f"Response: {response.text[:500]}")
except Exception as exc: # pragma: no cover - simple console diagnostic
print(f"❌ EXCEPTION: {exc}")
def main() -> None:
print("=" * 60)
print("ADIF API ENDPOINT TESTS")
print("=" * 60)
test_endpoint(
"Departures - Simple Format",
f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/",
{
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {"pageNumber": 0},
"stationCode": "10200",
"trafficType": "ALL",
},
)
test_endpoint(
"Departures - State YES",
f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/",
{
"commercialService": "YES",
"commercialStopType": "YES",
"page": {"pageNumber": 0},
"stationCode": "10200",
"trafficType": "CERCANIAS",
},
)
test_endpoint(
"Route Details",
f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpathdetails/onepaths/",
{
"commercialNumber": "03194",
"destinationStationCode": "71801",
"launchingDate": 1713984000000, # Example timestamp
"originStationCode": "10200",
},
)
test_endpoint(
"Between Stations",
f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/",
{
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"destinationStationCode": "71801",
"originStationCode": "10200",
"page": {"pageNumber": 0},
"trafficType": "ALL",
},
)
test_endpoint(
"Station Details",
f"{BASE_STATIONS}/portroyalmanager/secure/stations/onestation/",
{"stationCode": "10200"},
use_stations_key=True,
)
test_endpoint(
"Station Observations",
f"{BASE_STATIONS}/portroyalmanager/secure/stationsobservations/",
{"stationCodes": ["10200", "71801"]},
use_stations_key=True,
)
test_endpoint(
"Train Composition",
f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/compositions/path/",
{
"commercialNumber": "03194",
"destinationStationCode": "71801",
"launchingDate": 1713984000000,
"originStationCode": "10200",
},
)
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(
"""
Tests completed. Review results above.
NOTES:
- Some endpoints may require valid codes/numbers
- Station codes are numeric (e.g.: 10200 for Madrid Atocha)
- Commercial numbers vary by train type
- Some data may not be available in real-time
COMMON STATION CODES:
- 10200: Madrid Puerta de Atocha
- 10302: Madrid Chamartin
- 71801: Barcelona Sants
- 50000: Valencia Nord
- 11401: Sevilla Santa Justa
TRAFFIC TYPES:
- CERCANIAS: Commuter trains
- MEDIA_DISTANCIA: Medium distance
- LARGA_DISTANCIA: Long distance
- ALL: All types
STATES:
- YES: Only commercial services/stops
- NOT: Without commercial services/stops
- BOTH: Both types
"""
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Detailed endpoint testing with full error messages and typed helpers.
"""
from __future__ import annotations
import json
import sys
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, Sequence
import requests
from requests import Response
# Add project root to sys.path to import adif_auth
sys.path.insert(0, str(Path(__file__).parent.parent))
from adif_auth import AdifAuthenticator # noqa: E402
ACCESS_KEY = "and20210615"
SECRET_KEY = "Jthjtr946RTt"
def test_endpoint_detailed(
name: str, url: str, payload: Dict[str, Any], use_stations_key: bool = False
) -> bool:
"""
Test an endpoint and display detailed information about request and response.
Returns:
True when the endpoint responds with 200, False otherwise.
"""
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
user_id = str(uuid.uuid4())
headers = auth.get_auth_headers("POST", url, payload, user_id=user_id)
headers["User-key"] = (
auth.USER_KEY_STATIONS if use_stations_key else auth.USER_KEY_CIRCULATION
)
print(f"\n{'='*70}")
print(f"Testing: {name}")
print(f"{'='*70}")
print(f"URL: {url}")
print(f"Payload: {json.dumps(payload, indent=2)}")
try:
response: Response = requests.post(
url, json=payload, headers=headers, timeout=10
)
print(f"\nStatus Code: {response.status_code}")
print(f"Headers: {dict(response.headers)}")
try:
response_json = response.json()
print(
f"Response Body: {json.dumps(response_json, indent=2, ensure_ascii=False)[:1000]}"
)
except ValueError:
print(f"Response Body (text): {response.text[:500]}")
if response.status_code == 200:
print("✅ SUCCESS")
return True
print(f"❌ FAILED - Status {response.status_code}")
return False
except Exception as exc: # pragma: no cover - console-only diagnostics
print(f"❌ ERROR: {exc}")
return False
def run_onepaths_variations(today_start: int) -> None:
"""Exercise the onepaths endpoint with a variety of payloads."""
print("\n\n" + "=" * 70)
print("TESTING ONEPATHS WITH DIFFERENT VARIATIONS")
print("=" * 70)
variations: Sequence[Dict[str, Any]] = [
{
"allControlPoints": True,
"commercialNumber": "03194",
"destinationStationCode": "71801",
"launchingDate": today_start,
"originStationCode": "10200",
},
{
"allControlPoints": True,
"commercialNumber": None,
"destinationStationCode": "71801",
"launchingDate": today_start,
"originStationCode": "10200",
},
{
"allControlPoints": True,
"destinationStationCode": "71801",
"launchingDate": today_start,
"originStationCode": "10200",
},
{
"allControlPoints": True,
"launchingDate": today_start,
"originStationCode": "10200",
},
{"commercialNumber": "03194", "launchingDate": today_start},
]
for index, payload in enumerate(variations, start=1):
test_endpoint_detailed(
f"OnePaths Variation {index}",
"https://circulacion.api.adif.es/portroyalmanager/secure/circulationpathdetails/onepaths/",
payload,
)
def main() -> None:
now = datetime.now()
today_start = int(datetime(now.year, now.month, now.day).timestamp() * 1000)
tomorrow_start = int(
(datetime(now.year, now.month, now.day) + timedelta(days=1)).timestamp() * 1000
)
print("Testing with dates:")
print(f"Today (start): {today_start} = {datetime.fromtimestamp(today_start/1000)}")
print(
f"Tomorrow (start): {tomorrow_start} = {datetime.fromtimestamp(tomorrow_start/1000)}"
)
test_endpoint_detailed(
"BetweenStations",
"https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/",
{
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"originStationCode": "10200",
"destinationStationCode": "71801",
"page": {"pageNumber": 0},
"trafficType": "ALL",
},
)
run_onepaths_variations(today_start)
test_endpoint_detailed(
"OneStation",
"https://estaciones.api.adif.es/portroyalmanager/secure/stations/onestation/",
{
"stationCode": "10200",
"detailedInfo": {
"extendedStationInfo": True,
"stationActivities": True,
"stationBanner": True,
"stationCommercialServices": True,
"stationInfo": True,
"stationServices": True,
"stationTransportServices": True,
},
},
use_stations_key=True,
)
test_endpoint_detailed(
"OneStation - Simple",
"https://estaciones.api.adif.es/portroyalmanager/secure/stations/onestation/",
{"stationCode": "10200"},
use_stations_key=True,
)
print("\n" + "=" * 70)
print("TEST COMPLETED")
print("=" * 70)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Fetch real trains from departures and then test onePaths with those numbers.
"""
from __future__ import annotations
import json
import sys
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Sequence
import requests
from requests import Response
# Add project root to sys.path to import adif_auth
sys.path.insert(0, str(Path(__file__).parent.parent))
from adif_auth import AdifAuthenticator # noqa: E402
ACCESS_KEY = "and20210615"
SECRET_KEY = "Jthjtr946RTt"
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
DEPARTURES_URL = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/"
ONEPATHS_URL = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpathdetails/onepaths/"
def fetch_departures(station_code: str, traffic_type: str) -> List[Dict[str, Any]]:
"""Fetch current departures from a station."""
payload = {
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {"pageNumber": 0},
"stationCode": station_code,
"trafficType": traffic_type,
}
user_id = str(uuid.uuid4())
headers = auth.get_auth_headers("POST", DEPARTURES_URL, payload, user_id=user_id)
headers["User-key"] = auth.USER_KEY_CIRCULATION
response: Response = requests.post(
DEPARTURES_URL, json=payload, headers=headers, timeout=10
)
if response.status_code != 200:
raise RuntimeError(
f"Error getting departures: {response.status_code} - {response.text}"
)
data = response.json()
return data.get("circulations", [])
def test_onepaths_with_trains(
trains: Sequence[Dict[str, Any]], launching_date: int
) -> None:
"""Run onepaths queries against a subset of the provided trains."""
for index, train in enumerate(trains[:3], start=1):
commercial_number = train.get("commercialNumber")
destination = train.get("destination", {})
dest_code = destination.get("stationCode")
origin = train.get("origin", {})
origin_code = origin.get("stationCode")
payload_onepaths: Dict[str, Any] = {
"allControlPoints": True,
"commercialNumber": commercial_number,
"destinationStationCode": dest_code,
"launchingDate": launching_date,
"originStationCode": origin_code,
}
print(f"\n{'='*70}")
print(f"Test {index}: Train {commercial_number}")
print(f"{'='*70}")
print(f"Payload: {json.dumps(payload_onepaths, indent=2)}")
user_id = str(uuid.uuid4())
headers = auth.get_auth_headers(
"POST", ONEPATHS_URL, payload_onepaths, user_id=user_id
)
headers["User-key"] = auth.USER_KEY_CIRCULATION
response: Response = requests.post(
ONEPATHS_URL, json=payload_onepaths, headers=headers, timeout=10
)
print(f"\nStatus: {response.status_code}")
if response.status_code == 200:
print("✅ SUCCESS!")
try:
data = response.json()
print(
f"Response: {json.dumps(data, indent=2, ensure_ascii=False)[:2000]}"
)
except ValueError:
print(f"Response text: {response.text[:500]}")
elif response.status_code == 204:
print("⚠️ 204 No Content - Correct authentication but no data")
else:
print(f"❌ FAILED - Status {response.status_code}")
try:
print(f"Error: {response.json()}")
except ValueError:
print(f"Response text: {response.text}")
def main() -> None:
print("=" * 70)
print("STEP 1: Getting real trains from Madrid Atocha")
print("=" * 70)
trains = fetch_departures(station_code="10200", traffic_type="AVLDMD")
print(f"✅ Retrieved {len(trains)} trains\n")
print("First 5 trains:")
for i, train in enumerate(trains[:5], start=1):
commercial_number = train.get("commercialNumber")
destination = train.get("destination", {})
dest_name = destination.get("longName", "Unknown")
origin = train.get("origin", {})
origin_name = origin.get("longName", "Unknown")
planned_time = train.get("plannedTime", "Unknown")
print(f"\n{i}. Train {commercial_number}")
print(f" Origin: {origin_name}")
print(f" Destination: {dest_name}")
print(f" Departure time: {planned_time}")
print("\n" + "=" * 70)
print("STEP 2: Testing onePaths with real trains")
print("=" * 70)
now = datetime.now()
launching_date = int(datetime(now.year, now.month, now.day).timestamp() * 1000)
test_onepaths_with_trains(trains, launching_date)
print("\n" + "=" * 70)
print("TEST COMPLETE")
print("=" * 70)
if __name__ == "__main__":
try:
main()
except Exception as exc: # pragma: no cover - console script
print(f"❌ Error running test: {exc}")