1+ # tests/test_client.py
2+
3+ import pytest
4+ from unittest import mock # For mocking requests
5+ import requests # Need this to mock its methods and exceptions
6+ import json # For JSONDecodeError simulation if needed (though requests.exceptions handles it)
7+
8+ # --- Corrected Imports ---
9+ # Import directly from the client module where they are defined
10+ from freerouting .client import (
11+ FreeroutingClient ,
12+ FreeroutingError ,
13+ FreeroutingAPIError ,
14+ FreeroutingAuthError
15+ )
16+
17+
18+ # --- Fixtures ---
19+
20+ @pytest .fixture
21+ def api_key ():
22+ """Provides a dummy API key for tests."""
23+ return "test_api_key_123"
24+
25+ @pytest .fixture
26+ def client (api_key ):
27+ """Provides a FreeroutingClient instance for tests."""
28+ # Using a non-standard base_url ensures we definitely don't hit the real API
29+ return FreeroutingClient (api_key = api_key , base_url = "http://test.invalid" )
30+
31+ # --- Test Cases ---
32+
33+ def test_client_initialization (client , api_key ):
34+ """Test if the client initializes correctly with mandatory args."""
35+ assert client .api_key == api_key
36+ assert client .base_url == "http://test.invalid/v1" # Default version is v1
37+ assert client .session_id is None
38+ assert isinstance (client .profile_id , str )
39+
40+ def test_client_initialization_custom_params (api_key ):
41+ """Test client initialization with custom parameters."""
42+ client = FreeroutingClient (
43+ api_key = api_key ,
44+ base_url = "http://localhost:8080" ,
45+ version = "dev" ,
46+ profile_id = "custom-profile-id" ,
47+ host_name = "pytest-runner/1.0"
48+ )
49+ assert client .base_url == "http://localhost:8080/dev"
50+ assert client .profile_id == "custom-profile-id"
51+ assert client .host_name == "pytest-runner/1.0"
52+
53+ def test_client_initialization_no_api_key ():
54+ """Test that ValueError is raised if no API key is provided."""
55+ with pytest .raises (ValueError , match = "API key must be provided" ):
56+ FreeroutingClient (api_key = "" )
57+
58+
59+ # --- Mocking API Calls ---
60+
61+ @mock .patch ('requests.get' ) # Patch where it's used by the client
62+ def test_get_system_status_success (mock_get , client ):
63+ """Test a successful GET request (e.g., get_system_status)."""
64+ mock_response = mock .Mock ()
65+ mock_response .status_code = 200
66+ mock_response .json .return_value = {"status" : "OK" , "message" : "Service is running" }
67+ mock_get .return_value = mock_response
68+
69+ status = client .get_system_status ()
70+
71+ assert status == {"status" : "OK" , "message" : "Service is running" }
72+ mock_get .assert_called_once ()
73+ args , kwargs = mock_get .call_args
74+ assert args [0 ] == "http://test.invalid/v1/system/status"
75+ assert "Authorization" in kwargs ['headers' ]
76+ assert kwargs ['headers' ]['Authorization' ] == f"Bearer { client .api_key } "
77+
78+ @mock .patch ('requests.post' )
79+ def test_create_session_success (mock_post , client ):
80+ """Test a successful POST request (e.g., create_session)."""
81+ session_id = "session-xyz-789"
82+ mock_response = mock .Mock ()
83+ mock_response .status_code = 201
84+ mock_response .json .return_value = {"id" : session_id , "status" : "created" }
85+ mock_post .return_value = mock_response
86+
87+ session_details = client .create_session ()
88+
89+ assert session_details == {"id" : session_id , "status" : "created" }
90+ assert client .session_id == session_id
91+ mock_post .assert_called_once_with (
92+ "http://test.invalid/v1/sessions/create" ,
93+ headers = client ._get_headers (),
94+ data = None # No data payload expected for this call
95+ )
96+
97+ @mock .patch ('requests.put' )
98+ def test_start_job_success (mock_put , client ):
99+ """Test a successful PUT request returning 202 Accepted (e.g., start_job)."""
100+ job_id = "job-abc-123"
101+ mock_response = mock .Mock ()
102+ mock_response .status_code = 202
103+ mock_response .content = b'' # Simulate empty response body for 202
104+ # Make .json() raise an error if called, as per client._make_request handling
105+ mock_response .json .side_effect = requests .exceptions .JSONDecodeError ("Expecting value" , "doc" , 0 )
106+ mock_put .return_value = mock_response
107+
108+ result = client .start_job (job_id )
109+
110+ assert result == {} # Expect empty dict for 202 with no body
111+ mock_put .assert_called_once_with (
112+ f"http://test.invalid/v1/jobs/{ job_id } /start" ,
113+ headers = client ._get_headers (),
114+ data = None
115+ )
116+
117+ @mock .patch ('requests.get' )
118+ def test_make_request_api_error (mock_get , client ):
119+ """Test handling of a non-2xx API error response (e.g., 404)."""
120+ mock_response = mock .Mock ()
121+ mock_response .status_code = 404
122+ mock_response .text = "Resource Not Found"
123+ mock_response .json .side_effect = requests .exceptions .JSONDecodeError ("Expecting value" , "doc" , 0 )
124+ mock_get .return_value = mock_response
125+
126+ with pytest .raises (FreeroutingAPIError ) as excinfo :
127+ client .get_system_status () # Any method using _make_request with GET
128+
129+ assert excinfo .value .status_code == 404
130+ assert "Resource Not Found" in str (excinfo .value )
131+ assert "API request failed: 404" in str (excinfo .value )
132+
133+ @mock .patch ('requests.get' )
134+ def test_make_request_auth_error (mock_get , client ):
135+ """Test handling of a 401 Unauthorized error."""
136+ mock_response = mock .Mock ()
137+ mock_response .status_code = 401
138+ mock_response .text = "Invalid credentials provided"
139+ mock_response .json .side_effect = requests .exceptions .JSONDecodeError ("Expecting value" , "doc" , 0 )
140+ mock_get .return_value = mock_response
141+
142+ with pytest .raises (FreeroutingAuthError ) as excinfo :
143+ client .get_system_status ()
144+
145+ assert "Authentication failed: 401" in str (excinfo .value )
146+ assert "Invalid credentials provided" in str (excinfo .value )
147+
148+
149+ def test_get_session_no_id_error (client ):
150+ """Test that get_session raises ValueError if no session ID is available."""
151+ client .session_id = None
152+ with pytest .raises (ValueError , match = "No session ID provided or stored internally" ):
153+ client .get_session ()
154+
155+ def test_enqueue_job_no_id_error (client ):
156+ """Test that enqueue_job raises ValueError if no session ID is available."""
157+ client .session_id = None
158+ with pytest .raises (ValueError , match = "No session ID provided or stored internally" ):
159+ client .enqueue_job (name = "test_job" )
160+
161+ # --- Value Error tests for missing IDs ---
162+ def test_get_job_no_id (client ):
163+ """Test ValueError if job_id is empty string for get_job."""
164+ with pytest .raises (ValueError , match = "Job ID must be provided" ):
165+ client .get_job (job_id = "" )
166+
167+ def test_start_job_no_id (client ):
168+ """Test ValueError if job_id is empty string for start_job."""
169+ with pytest .raises (ValueError , match = "Job ID must be provided" ):
170+ client .start_job (job_id = "" )
171+
172+ def test_upload_input_no_params (client ):
173+ """Test ValueErrors for missing parameters in upload_input."""
174+ with pytest .raises (ValueError , match = "Job ID must be provided" ):
175+ client .upload_input (job_id = "" , filename = "f" , file_path = "p" )
176+ with pytest .raises (ValueError , match = "Filename must be provided" ):
177+ client .upload_input (job_id = "j" , filename = "" , file_path = "p" )
178+ with pytest .raises (ValueError , match = "File path must be provided" ):
179+ client .upload_input (job_id = "j" , filename = "f" , file_path = "" )
180+
181+
182+ # --- TODO: Add more tests! ---
183+ # - Test methods like upload_input with file mocking (e.g., using mock_open)
184+ # - Test download_output (mocking GET and checking file write if path provided)
185+ # - Test run_routing_job workflow (requires more complex mocking of multiple steps)
186+ # - Test network errors (patch requests.get/post/put to raise requests.exceptions.RequestException)
0 commit comments