1+ import pytest
2+ from unittest .mock import AsyncMock , patch
3+ from src .models .history_data import CachedLocation
4+
5+ MOCK_LAT = 40.7128
6+ MOCK_LON = - 74.0060
7+
8+ class TestLocationRoutes :
9+ """
10+ Integration tests for /api/v1/locations/ routes.
11+ Uses the in-memory MongoDB (AsyncMongoMockClient) via the app fixture.
12+ Data is inserted directly into the mock DB before each test that needs it.
13+ """
14+
15+ # Runs after every test to keep the DB clean.
16+ @pytest .fixture (autouse = True )
17+ async def clean_db (self ):
18+ yield
19+ await CachedLocation .find_all ().delete ()
20+
21+ # Inserts a real CachedLocation document into the mock DB.
22+ # Returns the inserted document so tests can reference its id.
23+ async def _insert_location (self , location :dict , name : str = "Test Location" ):
24+ doc = CachedLocation (name = name , location = location )
25+ await doc .insert ()
26+ return doc
27+
28+ @pytest .mark .anyio
29+ async def test_list_locations_returns_200_with_empty_db (
30+ self , async_client , auth_headers
31+ ):
32+ response = await async_client .get (
33+ "/api/v1/locations/locations/" , headers = auth_headers
34+ )
35+ assert response .status_code == 200
36+ assert response .json () == []
37+
38+ @pytest .mark .anyio
39+ async def test_list_locations_returns_inserted_documents (
40+ self , async_client , auth_headers , mock_location
41+ ):
42+ await self ._insert_location (mock_location , "Location A" )
43+ await self ._insert_location (mock_location , "Location B" )
44+
45+ response = await async_client .get (
46+ "/api/v1/locations/locations/" , headers = auth_headers
47+ )
48+
49+ assert response .status_code == 200
50+ assert len (response .json ()) == 2
51+ names = [loc ["name" ] for loc in response .json ()]
52+ assert "Location A" in names
53+ assert "Location B" in names
54+
55+ @pytest .mark .anyio
56+ async def test_list_locations_returns_403_without_auth (self , async_client ):
57+ response = await async_client .get ("/api/v1/locations/locations/" )
58+ assert response .status_code == 403
59+
60+ @pytest .mark .anyio
61+ async def test_get_location_by_coordinates_returns_200 (
62+ self , async_client , auth_headers , mock_location
63+ ):
64+ await self ._insert_location (mock_location )
65+
66+ response = await async_client .get (
67+ "/api/v1/locations/locations/by-coordinates/" ,
68+ params = {"lat" : MOCK_LAT , "lon" : MOCK_LON },
69+ headers = auth_headers ,
70+ )
71+
72+ assert response .status_code == 200
73+ assert response .json ()["lat" ] == MOCK_LAT
74+ assert response .json ()["lon" ] == MOCK_LON
75+
76+ @pytest .mark .anyio
77+ async def test_get_location_by_coordinates_returns_404_when_not_found (
78+ self , async_client , auth_headers
79+ ):
80+ # Nothing inserted — DB is empty
81+ response = await async_client .get (
82+ "/api/v1/locations/locations/by-coordinates/" ,
83+ params = {"lat" : MOCK_LAT , "lon" : MOCK_LON },
84+ headers = auth_headers ,
85+ )
86+
87+ assert response .status_code == 404
88+
89+ @pytest .mark .anyio
90+ async def test_get_location_by_coordinates_returns_403_without_auth (
91+ self , async_client
92+ ):
93+ response = await async_client .get (
94+ "/api/v1/locations/locations/by-coordinates/" ,
95+ params = {"lat" : MOCK_LAT , "lon" : MOCK_LON },
96+ )
97+
98+ assert response .status_code == 403
99+
100+
101+ @pytest .mark .skip (
102+ reason = "Uses MongoDB $near geospatial query — not supported by mongomock. "
103+ "Requires a real MongoDB instance to test meaningfully."
104+ )
105+ async def test_check_location_exists_in_radius (self ):
106+ pass
107+
108+ @pytest .mark .anyio
109+ async def test_add_locations_returns_200_and_inserts (
110+ self , async_client , auth_headers
111+ ):
112+ payload = {
113+ "locations" : [{"name" : "Farm A" , "lat" : MOCK_LAT , "lon" : MOCK_LON }]
114+ }
115+
116+ with patch (
117+ "src.api.api_v1.endpoints.locations.fetch_and_cache_last_month" ,
118+ new_callable = AsyncMock
119+ ):
120+ response = await async_client .post (
121+ "/api/v1/locations/locations/" ,
122+ json = payload ,
123+ headers = auth_headers ,
124+ )
125+
126+ assert response .status_code == 200
127+ # Verify it actually landed in the DB
128+ docs = await CachedLocation .find_all ().to_list ()
129+ assert len (docs ) == 1
130+ assert docs [0 ].name == "Farm A"
131+
132+ @pytest .mark .anyio
133+ async def test_add_locations_skips_existing_location (
134+ self , async_client , auth_headers , mock_location
135+ ):
136+ await self ._insert_location (mock_location , "Farm A" )
137+
138+ payload = {
139+ "locations" : [{"name" : "Farm A" , "lat" : MOCK_LAT , "lon" : MOCK_LON }]
140+ }
141+
142+ with patch (
143+ "src.api.api_v1.endpoints.locations.fetch_and_cache_last_month" ,
144+ new_callable = AsyncMock
145+ ):
146+ response = await async_client .post (
147+ "/api/v1/locations/locations/" ,
148+ json = payload ,
149+ headers = auth_headers ,
150+ )
151+
152+ assert response .status_code == 200
153+ # Still only one document — duplicate was skipped
154+ docs = await CachedLocation .find_all ().to_list ()
155+ assert len (docs ) == 1
156+
157+ @pytest .mark .anyio
158+ async def test_add_locations_returns_403_without_auth (self , async_client ):
159+ payload = {
160+ "locations" : [{"name" : "Farm A" , "lat" : MOCK_LAT , "lon" : MOCK_LON }]
161+ }
162+ response = await async_client .post ("/api/v1/locations/locations/" , json = payload )
163+ assert response .status_code == 403
164+
165+ @pytest .mark .skip (
166+ reason = "Uses dao.find_location_nearby which relies on MongoDB $near "
167+ "geospatial query — not supported by mongomock."
168+ )
169+ async def test_add_unique_locations (self ):
170+ pass
171+
172+ @pytest .mark .anyio
173+ async def test_delete_location_returns_200 (
174+ self , async_client , auth_headers , mock_location
175+ ):
176+ doc = await self ._insert_location (mock_location )
177+
178+ with patch (
179+ "src.api.api_v1.endpoints.locations.scheduler"
180+ ) as mock_scheduler :
181+ mock_scheduler .get_job .return_value = None
182+ response = await async_client .delete (
183+ f"/api/v1/locations/locations/{ doc .id } /" ,
184+ headers = auth_headers ,
185+ )
186+
187+ assert response .status_code == 200
188+ # Verify it was actually removed from the DB
189+ remaining = await CachedLocation .find_all ().to_list ()
190+ assert len (remaining ) == 0
191+
192+
193+ @pytest .mark .anyio
194+ async def test_delete_location_returns_403_without_auth (self , async_client ):
195+ response = await async_client .delete ("/api/v1/locations/locations/some-id/" )
196+ assert response .status_code == 403
0 commit comments