From d057bdb4f48ff70055b9b5f0a76c8b32f3087b12 Mon Sep 17 00:00:00 2001 From: anurag-mapup Date: Fri, 5 Jun 2026 14:38:02 +0530 Subject: [PATCH] feat: add test suite and development dependencies for gps-tracks-csv-upload module --- .../gps-tracks-csv-upload.cpython-313.pyc | Bin 0 -> 2348 bytes pytest.ini | 6 + requirements-dev.txt | 3 + tests/__init__.py | 0 tests/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 170 bytes ...ks_csv_upload.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 53534 bytes tests/test_gps_tracks_csv_upload.py | 532 ++++++++++++++++++ 7 files changed, 541 insertions(+) create mode 100644 __pycache__/gps-tracks-csv-upload.cpython-313.pyc create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/__pycache__/test_gps_tracks_csv_upload.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/test_gps_tracks_csv_upload.py diff --git a/__pycache__/gps-tracks-csv-upload.cpython-313.pyc b/__pycache__/gps-tracks-csv-upload.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e58a0c234bd602c4294d6057fde2d07d31f0fb1d GIT binary patch literal 2348 zcmZuxO>7fK6rTO{Pi)7DorK@uBuK?k9EXxnVG6}fLI{B*^4KD+BUy{HNgSNrWp)iD zC#xq{Z6&CLpi-qqJ(XJzJ#gruNIh_Lnp8Cdl~7A7(NiLdkb3HywKx2TvHa%EdvE9G zz3+R^{C+Qj@yQq6OMkc!`jai3;ch6;{sHAC!U%H{h_IQT;Lh@dkJN1f5p17GM3{vM z>8woT2$~gB0cWe1bE@T~?r2yX;_5o_V2OCKOq#HZ_^=y$uopLBU%xQG5kGFj{@K7F zj{|$qlDNc?=1zp$ar0~t2WNwWB5rBeTV`9k(QI=!8fqNj><_nMo|d{7x8l$-4u_9% zFZ~N#wBsrJYBI`E$t0g9(YimH~^=TsU@Pn|nAmPuuD$?5Uj#OOz>U5l1wn2C5? zF$!kCr5B3}B~t3o>&tPrh+lbkwQa#LV-`{Jm&{n+yb>!JMO~R&?RdjHr(e~WgSxJ$ zi-mkqE!=HF=qW1^Z5m2mE2?Iil=7FN0+p|-g@r|n%8T_`v{nq2x(jAbF;}#FzL8M- z{T`44{Ga^|0t(4#T-J6l5rA@Ld1nbLAuBnn0hE;;9gEC7%3&#k2Ds6klm*ZcN#nB) zSP+)6tDo0|EZn?4G8}dfHBR7>CT4w(=WPJ#M%XjclM%7^n5anrDEK*FM^HvOh`LZ8 zI@H)=4&FHrKs!-Ahqk477sCqA# zho9{0ySua=7>AEN{>s92YtyrDje8W@`&+2zmr&2z>FV*q*h-@tXGg+GMp^jviaIedo9B@&Is1{kF(wtgUR`QG$&Vcq;XNeCmbrWScO#r4q zOVgx`fceCa2q{+)0%UTD8PG2y^cstVS6OsK(Bv#Dsy(oXCE$eXC7V(^d~sqB0_4jx z3mJr64Eb5RF*9#!hcBF*h%^yy?}>GQCS-S-Z-#dwfm6=w;7&WQ z1HPVz7c#y@y^~ZLO{Jz%gt3^guD%*n45L`cD^@|*;!CEk5w}V{h~I!=LruZeM$EV|VYPmab~Bd#!8p;IV2?q8d0}5uQW_svoA;TQe2c z69>OFU+eYZYCN?q@L_Mo{jZF|yKW`EOx#}lX8Eh-jZk03U2EBW`_OtzPerb|{nuMQ z^;V=uZhuAn+_%+%nu5-mzwYkc+GB(N8Qc%0qnzCvV2Z&q%AJn#PwfOBX3G%ig$c4+ zSXS+<+IgqEZc-ogkYeFtzo8K56b!r6rD|6S1d1Un0Bh`BSRKhOCZbF*rc?2vt`}|M zQ=tF}XBnfCK|sHQ$N-D8Q|x|!S)VHv)sy5n*cdR(Z$WGc9LGIGO~0e|hp71>^8AIm zABWm*C2k~coxE|f+C8`t8mgjTEp!y*=7.4.0 +pytest-cov>=4.1.0 +requests>=2.31.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-313.pyc b/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e5a8d3e5a29f35246133e95b81fddcc7cded4f5 GIT binary patch literal 170 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 literal 0 HcmV?d00001 diff --git a/tests/test_gps_tracks_csv_upload.py b/tests/test_gps_tracks_csv_upload.py new file mode 100644 index 0000000..e82f1c3 --- /dev/null +++ b/tests/test_gps_tracks_csv_upload.py @@ -0,0 +1,532 @@ +""" +Tests for gps-tracks-csv-upload.py + +Patterns borrowed from mapup-products-frontend: +- One test class per public function / concern +- Heavy use of unittest.mock – no real network calls or file I/O +- Explicit assertions on URL shape, headers, retry semantics, and edge cases +- Each test is self-contained: all patches are scoped to the test itself +""" + +import importlib.util +import json +import sys +from unittest.mock import MagicMock, call, mock_open, patch + +import pytest + + +# --------------------------------------------------------------------------- +# Module loader +# +# The script executes two top-level print() statements on import, which call +# both functions and hit the network. We suppress all I/O before the module +# is executed so that the test suite can import cleanly without env vars or +# a live API key. +# --------------------------------------------------------------------------- + +def _load_module(): + """ + Load gps-tracks-csv-upload.py with all side-effects neutralised. + + Strategy (mirrors how mapup-products-frontend mocks next/navigation at + the Jest setup level): + - patch requests.request → returns a canned SUCCESS response + - patch builtins.open → returns fake CSV bytes + - patch builtins.print → silences the two top-level print() calls + - patch time.sleep → skips the 5-second download poll delay + """ + stub_http = MagicMock() + stub_http.text = json.dumps({"status": "SUCCESS", "requestId": "stub-id"}) + + m = mock_open(read_data=b"latitude,longitude,timestamp\n47.0,0.69,2023-01-01T00:00:00Z\n") + + with ( + patch("requests.request", return_value=stub_http), + patch("builtins.open", m), + patch("builtins.print"), + patch("time.sleep"), + ): + spec = importlib.util.spec_from_file_location( + "_gps_tracks_module", "gps-tracks-csv-upload.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + # Register so patch("_gps_tracks_module.sleep") works if ever needed + sys.modules["_gps_tracks_module"] = mod + return mod + + +# Load once for the entire test session +_mod = _load_module() + + +# --------------------------------------------------------------------------- +# Shared test data +# --------------------------------------------------------------------------- + +SAMPLE_CSV = b"latitude,longitude,timestamp\n47.24157,0.69426,2023-01-12T13:40:35Z\n" + +_SYNC_SUCCESS = {"status": "SUCCESS", "toll": {"amount": 12.5, "currency": "EUR"}} +_ASYNC_INIT = {"status": "SUCCESS", "requestId": "req-abc-123"} +_ERROR_BODY = {"status": "ERROR", "message": "not ready yet"} + + +def _http_mock(body: dict) -> MagicMock: + """Return a mock requests.Response whose .text is the JSON-encoded body.""" + m = MagicMock() + m.text = json.dumps(body) + return m + + +# --------------------------------------------------------------------------- +# gps_tracks_csv_upload() +# --------------------------------------------------------------------------- + +class TestGpsTracksCsvUpload: + """Unit tests for the upload function – mirrors utils.test.ts patterns.""" + + # -- Return value -------------------------------------------------------- + + def test_sync_returns_raw_response_object(self): + """Function returns the Response directly; caller decides how to parse.""" + mock_resp = _http_mock(_SYNC_SUCCESS) + with ( + patch("requests.request", return_value=mock_resp), + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + result = _mod.gps_tracks_csv_upload(is_async=False) + assert result is mock_resp + + def test_async_returns_raw_response_object(self): + """Async mode also returns the unparsed Response object.""" + mock_resp = _http_mock(_ASYNC_INIT) + with ( + patch("requests.request", return_value=mock_resp), + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + result = _mod.gps_tracks_csv_upload(is_async=True) + assert result is mock_resp + + # -- HTTP method --------------------------------------------------------- + + def test_uses_http_post(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + assert mock_req.call_args[0][0] == "POST" + + # -- URL construction ---------------------------------------------------- + + def test_url_starts_with_tollguru_base(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + url = mock_req.call_args[0][1] + assert url.startswith("https://apis.tollguru.com/toll/v2") + + def test_url_contains_upload_endpoint(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + url = mock_req.call_args[0][1] + assert "gps-tracks-csv-upload" in url + + def test_url_includes_vehicle_type_param(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + url = mock_req.call_args[0][1] + assert "5AxlesTruck" in url + + def test_url_includes_weight_param(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + url = mock_req.call_args[0][1] + assert "weight=3000" in url + + def test_url_includes_height_param(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + url = mock_req.call_args[0][1] + assert "height=10" in url + + def test_sync_url_has_is_async_false(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload(is_async=False) + url = mock_req.call_args[0][1] + assert "isAsync=False" in url + + def test_async_url_has_is_async_true(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload(is_async=True) + url = mock_req.call_args[0][1] + assert "isAsync=True" in url + + # -- Request headers ----------------------------------------------------- + + def test_sends_api_key_header(self): + """x-api-key header is set from the module-level TOLLGURU_API_KEY.""" + mock_resp = MagicMock() + with ( + patch.object(_mod, "TOLLGURU_API_KEY", "test-key-xyz"), + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + headers = mock_req.call_args[1]["headers"] + assert headers["x-api-key"] == "test-key-xyz" + + def test_sends_csv_content_type(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + headers = mock_req.call_args[1]["headers"] + assert headers["Content-Type"] == "text/csv" + + # -- File handling ------------------------------------------------------- + + def test_opens_france_csv_in_binary_mode(self): + """The hardcoded CSV path is opened as binary ('rb').""" + mock_resp = MagicMock() + m = mock_open(read_data=SAMPLE_CSV) + with ( + patch("requests.request", return_value=mock_resp), + patch("builtins.open", m) as mock_file, + ): + _mod.gps_tracks_csv_upload() + mock_file.assert_called_once_with( + "Sample-GPS-tracks/france-GPS-Track-sample.csv", "rb" + ) + + def test_passes_file_handle_as_request_data(self): + """The file handle (not its contents) is forwarded to requests.request.""" + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + assert "data" in mock_req.call_args[1] + + def test_makes_exactly_one_http_request(self): + mock_resp = MagicMock() + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + _mod.gps_tracks_csv_upload() + assert mock_req.call_count == 1 + + +# --------------------------------------------------------------------------- +# gps_tracks_csv_download() +# --------------------------------------------------------------------------- + +class TestGpsTracksCsvDownload: + """ + Unit tests for the async-polling download function. + + Key behaviours under test: + - Exits the retry loop as soon as status != "ERROR" + - Exhausts all retries when every response is ERROR + - Passes the upload response body verbatim as the download request body + - Sleeps between every poll (including the final one) + """ + + # -- Happy path ---------------------------------------------------------- + + def test_returns_parsed_dict_on_first_success(self): + mock_resp = _http_mock({"status": "SUCCESS", "toll": {"amount": 15.0}}) + with ( + patch("requests.request", return_value=mock_resp), + patch.object(_mod, "sleep"), + ): + result = _mod.gps_tracks_csv_download(b'{"requestId":"r1"}', retry=3, delay=1) + assert result == {"status": "SUCCESS", "toll": {"amount": 15.0}} + + def test_one_http_call_when_first_response_is_success(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=5, delay=0) + assert mock_req.call_count == 1 + + # -- Retry logic --------------------------------------------------------- + + def test_retries_while_status_is_error(self): + """retry=3 → 4 total attempts (initial + 3 retries).""" + mock_resp = _http_mock(_ERROR_BODY) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=3, delay=0) + assert mock_req.call_count == 4 + + def test_stops_on_first_non_error_response(self): + """After two ERRORs followed by SUCCESS, exactly three calls are made.""" + responses = [ + _http_mock(_ERROR_BODY), + _http_mock(_ERROR_BODY), + _http_mock({"status": "SUCCESS", "toll": {"amount": 7.5}}), + ] + with ( + patch("requests.request", side_effect=responses) as mock_req, + patch.object(_mod, "sleep"), + ): + result = _mod.gps_tracks_csv_download(b'{}', retry=5, delay=0) + assert mock_req.call_count == 3 + assert result["status"] == "SUCCESS" + + def test_zero_retries_makes_exactly_one_attempt(self): + """retry=0 means the loop runs once (count starts at 0, 0 >= 0 is True).""" + mock_resp = _http_mock(_ERROR_BODY) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=0, delay=0) + assert mock_req.call_count == 1 + + def test_returns_last_error_response_after_retry_exhaustion(self): + """When all retries are ERROR the last parsed response is returned.""" + mock_resp = _http_mock({"status": "ERROR", "message": "timeout"}) + with ( + patch("requests.request", return_value=mock_resp), + patch.object(_mod, "sleep"), + ): + result = _mod.gps_tracks_csv_download(b'{}', retry=2, delay=0) + assert result["status"] == "ERROR" + + # -- Sleep / delay ------------------------------------------------------- + + def test_sleep_called_after_each_request(self): + """sleep() is invoked once per loop iteration, even on success.""" + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp), + patch.object(_mod, "sleep") as mock_sleep, + ): + _mod.gps_tracks_csv_download(b'{}', retry=2, delay=9) + mock_sleep.assert_called_once_with(9) + + def test_sleep_delay_forwarded_correctly(self): + """Every sleep call receives the exact delay value passed in.""" + error_resp = _http_mock(_ERROR_BODY) + with ( + patch("requests.request", return_value=error_resp), + patch.object(_mod, "sleep") as mock_sleep, + ): + _mod.gps_tracks_csv_download(b'{}', retry=2, delay=42) + for c in mock_sleep.call_args_list: + assert c == call(42) + + # -- HTTP method & URL --------------------------------------------------- + + def test_uses_http_post(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=1, delay=0) + assert mock_req.call_args[0][0] == "POST" + + def test_url_starts_with_tollguru_base(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=1, delay=0) + url = mock_req.call_args[0][1] + assert url.startswith("https://apis.tollguru.com/toll/v2") + + def test_url_contains_download_endpoint(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=1, delay=0) + url = mock_req.call_args[0][1] + assert "gps-tracks-csv-download" in url + + # -- Request headers ----------------------------------------------------- + + def test_sends_api_key_header(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch.object(_mod, "TOLLGURU_API_KEY", "dl-test-key"), + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=1, delay=0) + headers = mock_req.call_args[1]["headers"] + assert headers["x-api-key"] == "dl-test-key" + + def test_sends_json_content_type(self): + mock_resp = _http_mock({"status": "SUCCESS"}) + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(b'{}', retry=1, delay=0) + headers = mock_req.call_args[1]["headers"] + assert headers["Content-Type"] == "application/json" + + # -- Request body -------------------------------------------------------- + + def test_upload_response_forwarded_as_request_body(self): + """The raw upload response is passed through as the download POST body.""" + mock_resp = _http_mock({"status": "SUCCESS"}) + upload_payload = b'{"requestId":"some-id"}' + with ( + patch("requests.request", return_value=mock_resp) as mock_req, + patch.object(_mod, "sleep"), + ): + _mod.gps_tracks_csv_download(upload_payload, retry=1, delay=0) + assert mock_req.call_args[1]["data"] == upload_payload + + +# --------------------------------------------------------------------------- +# Module-level constants +# --------------------------------------------------------------------------- + +class TestModuleConstants: + """ + Smoke-test the global config values so any accidental edits are caught. + Mirrors the config/__tests__/*.test.ts pattern in mapup-products-frontend. + """ + + def test_api_url(self): + assert _mod.TOLLGURU_API_URL == "https://apis.tollguru.com/toll/v2" + + def test_upload_endpoint(self): + assert _mod.GPS_UPLOAD_ENDPOINT == "gps-tracks-csv-upload" + + def test_download_endpoint(self): + assert _mod.GPS_DOWNLOAD_ENDPOINT == "gps-tracks-csv-download" + + def test_default_vehicle_type(self): + assert _mod.PARAMETERS["vehicle"]["type"] == "5AxlesTruck" + + def test_default_vehicle_weight(self): + assert _mod.PARAMETERS["vehicle"]["weight"] == 3000 + + def test_default_vehicle_height(self): + assert _mod.PARAMETERS["vehicle"]["height"] == 10 + + def test_api_key_sourced_from_env(self, monkeypatch): + """Verifies the env-var lookup path; the constant is set at import time.""" + import os + monkeypatch.setenv("TOLLGURU_API_KEY", "env-check-value") + assert os.environ.get("TOLLGURU_API_KEY") == "env-check-value" + + +# --------------------------------------------------------------------------- +# Integration-style wiring tests +# --------------------------------------------------------------------------- + +class TestIntegrationWorkflow: + """ + End-to-end wiring without real I/O – analogous to hooks.test.tsx which + wires context providers and hooks together in mapup-products-frontend. + """ + + def test_sync_workflow_returns_a_response(self): + mock_resp = MagicMock() + mock_resp.text = json.dumps(_SYNC_SUCCESS) + with ( + patch("requests.request", return_value=mock_resp), + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + ): + result = _mod.gps_tracks_csv_upload(is_async=False) + assert result is not None + + def test_async_workflow_upload_then_download(self): + """ + Full async flow: upload returns a raw response, download receives it + as its body and eventually returns parsed JSON. + Two separate HTTP calls must be made. + """ + upload_resp = MagicMock() + upload_resp.text = json.dumps({"requestId": "r42", "status": "SUCCESS"}) + + download_resp = _http_mock({"status": "SUCCESS", "toll": {"amount": 9.99}}) + + call_n = {"v": 0} + + def _side_effect(*args, **kwargs): + call_n["v"] += 1 + return upload_resp if call_n["v"] == 1 else download_resp + + with ( + patch("requests.request", side_effect=_side_effect) as mock_req, + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + patch.object(_mod, "sleep"), + ): + upload_result = _mod.gps_tracks_csv_upload(is_async=True) + download_result = _mod.gps_tracks_csv_download( + upload_result, retry=1, delay=0 + ) + + assert mock_req.call_count == 2 + assert download_result["status"] == "SUCCESS" + assert download_result["toll"]["amount"] == 9.99 + + def test_upload_endpoint_and_download_endpoint_are_different(self): + """Upload and download must target different URL paths.""" + urls = [] + + def _capture(*args, **kwargs): + urls.append(args[1]) + return _http_mock({"status": "SUCCESS"}) + + with ( + patch("requests.request", side_effect=_capture), + patch("builtins.open", mock_open(read_data=SAMPLE_CSV)), + patch.object(_mod, "sleep"), + ): + resp = _mod.gps_tracks_csv_upload(is_async=True) + _mod.gps_tracks_csv_download(resp, retry=1, delay=0) + + assert len(urls) == 2 + assert urls[0] != urls[1] + assert "csv-upload" in urls[0] + assert "csv-download" in urls[1]