Skip to content

Commit 97c5613

Browse files
authored
Merge pull request #107 from mapswipe/feature/unit-testing-additional
2 parents 4768ed2 + 08b28e0 commit 97c5613

File tree

5 files changed

+362
-0
lines changed

5 files changed

+362
-0
lines changed

project_types/validate/api_calls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def retry_get(url: str, retries: int | None = 3, timeout: int | None = 4, to_osm
3737
return session.get(url, timeout=timeout)
3838

3939

40+
# FIXME(rup): Not used anywhere
4041
def geojsonToFeatureCollection(geojson: dict) -> dict:
4142
"""Take a GeoJson and wrap it in a FeatureCollection."""
4243
if geojson["type"] != "FeatureCollection":

project_types/validate/tests/__init__.py

Whitespace-only changes.
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import typing
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from main.config import Config
7+
from main.tests import TestCase
8+
from project_types.validate.api_calls import (
9+
ValidateApiCallError,
10+
ohsome,
11+
query_osm,
12+
query_osmcha,
13+
remove_noise_and_add_user_info,
14+
remove_troublesome_chars,
15+
)
16+
17+
18+
class TestValidateProject(TestCase):
19+
@typing.override
20+
@classmethod
21+
def setUpClass(cls):
22+
super().setUpClass()
23+
24+
def test_remove_troublesome_chars(self):
25+
data = [
26+
("Hello\nWorld", "Hello World"),
27+
('She said "Hello"', "She said Hello"),
28+
("It's fine", "Its fine"),
29+
("Hello 'World'", "Hello World"),
30+
(None, None),
31+
(123, 123),
32+
]
33+
for input_str, expected in data:
34+
result = remove_troublesome_chars(input_str)
35+
assert result == expected
36+
37+
@patch("project_types.validate.api_calls.retry_get")
38+
def test_query_osmcha(self, mock_retry_get):
39+
changeset_ids = [12345, 67890]
40+
changeset_results = {}
41+
42+
# Fake API JSON response
43+
mock_response_data = {
44+
"features": [
45+
{
46+
"id": "12345",
47+
"properties": {
48+
"user": "Sita Devi",
49+
"uid": 1001,
50+
"comment": "Looks good!",
51+
"editor": "Ram",
52+
},
53+
},
54+
{
55+
"id": "67890",
56+
"properties": {
57+
"user": "Hari",
58+
"uid": 1002,
59+
"comment": "It's not a bridge",
60+
"editor": "Shyam 'Bahadur'",
61+
},
62+
},
63+
],
64+
}
65+
66+
mock_response = MagicMock()
67+
mock_response.status_code = 200
68+
mock_response.json.return_value = mock_response_data
69+
mock_retry_get.return_value = mock_response
70+
71+
query_osmcha(changeset_ids, changeset_results)
72+
73+
assert changeset_results == {
74+
12345: {
75+
"username": "Sita Devi",
76+
"userid": 1001,
77+
"comment": "Looks good!",
78+
"editor": "Ram",
79+
},
80+
67890: {
81+
"username": "Hari",
82+
"userid": 1002,
83+
"comment": "Its not a bridge",
84+
"editor": "Shyam Bahadur",
85+
},
86+
}
87+
88+
# Check request is made for osmcha
89+
mock_retry_get.assert_called_once()
90+
called_url = mock_retry_get.call_args[0][0]
91+
assert called_url.startswith(Config.OSMCHA_API_LINK)
92+
assert "changesets/?ids=12345,67890" in called_url
93+
94+
# check for other status code to raise error
95+
mock_response.status_code = 403
96+
with pytest.raises(ValidateApiCallError):
97+
query_osmcha(changeset_ids, changeset_results)
98+
99+
@patch("project_types.validate.api_calls.retry_get")
100+
def test_query_osm(self, mock_retry_get):
101+
changeset_ids = [12345, 67890]
102+
changeset_results = {}
103+
104+
xml_response = """
105+
<osm>
106+
<changeset id="12345" user='Sita "Devi"' uid="1001">
107+
<tag k="comment" v="Looks good!"/>
108+
<tag k="created_by" v="Ram"/>
109+
</changeset>
110+
<changeset id="67890" user="Hari" uid="1002">
111+
<tag k="comment" v="It's not a bridge"/>
112+
<tag k="created_by" v="Shyam Bahadur"/>
113+
</changeset>
114+
</osm>
115+
"""
116+
117+
mock_response = MagicMock()
118+
mock_response.status_code = 200
119+
mock_response.content = xml_response.encode("utf-8")
120+
mock_retry_get.return_value = mock_response
121+
122+
result = query_osm(changeset_ids, changeset_results)
123+
124+
assert result == {
125+
12345: {
126+
"username": "Sita Devi",
127+
"userid": "1001",
128+
"comment": "Looks good!",
129+
"editor": "Ram",
130+
},
131+
67890: {
132+
"username": "Hari",
133+
"userid": "1002",
134+
"comment": "Its not a bridge",
135+
"editor": "Shyam Bahadur",
136+
},
137+
}
138+
139+
# check for other status code to raise error
140+
mock_response.status_code = 500
141+
with pytest.raises(ValidateApiCallError):
142+
query_osm([12345], {})
143+
144+
@patch("requests.post")
145+
@patch("project_types.validate.api_calls.remove_noise_and_add_user_info")
146+
def test_ohsome(self, mock_remove_noise, mock_post):
147+
sample_request = {
148+
"endpoint": "elements/geometry",
149+
"filter": "highway=primary",
150+
}
151+
sample_area = "POLYGON((8.67 49.39,8.68 49.39,8.68 49.40,8.67 49.40,8.67 49.39))"
152+
sample_properties = "tags,timestamp"
153+
154+
mock_response_data = {
155+
"attribution": {
156+
"url": "https://ohsome.org/copyrights",
157+
"text": "© OpenStreetMap contributors",
158+
},
159+
"apiVersion": "1.10.4",
160+
"type": "FeatureCollection",
161+
"features": [
162+
{
163+
"type": "Feature",
164+
"geometry": {
165+
"type": "Point",
166+
"coordinates": [
167+
8.6861,
168+
49.4051089,
169+
],
170+
},
171+
"properties": {
172+
"@osmId": "node/385941986",
173+
"@snapshotTimestamp": "2019-09-01T00: 00: 00Z",
174+
},
175+
},
176+
{
177+
"type": "Feature",
178+
"geometry": {
179+
"type": "Point",
180+
"coordinates": [
181+
8.6819524,
182+
49.3825748,
183+
],
184+
},
185+
"properties": {
186+
"@osmId": "node/699583613",
187+
"@snapshotTimestamp": "2019-09-01T00: 00: 00Z",
188+
},
189+
},
190+
],
191+
}
192+
193+
mock_response = MagicMock()
194+
mock_response.status_code = 200
195+
mock_response.json.return_value = mock_response_data
196+
mock_post.return_value = mock_response
197+
198+
processed_data = {"processed": True, "data": mock_response_data}
199+
mock_remove_noise.return_value = processed_data
200+
201+
result = ohsome(sample_request, sample_area, sample_properties)
202+
203+
mock_post.assert_called_once()
204+
mock_remove_noise.assert_called_once_with(mock_response_data)
205+
assert result == processed_data
206+
207+
# test for error
208+
status_codes = [400, 401, 403, 404, 500]
209+
for status_code in status_codes:
210+
mock_response.status_code = status_code
211+
with pytest.raises(ValidateApiCallError):
212+
result = ohsome(sample_request, sample_area, sample_properties)
213+
214+
@patch("project_types.validate.api_calls.query_osmcha")
215+
@patch("project_types.validate.api_calls.query_osm")
216+
def test_remove_noise_and_add_user_info(self, mock_query_osm, mock_query_osmcha):
217+
input_data = {
218+
"type": "FeatureCollection",
219+
"features": [
220+
{
221+
"type": "Feature",
222+
"geometry": {"type": "Point", "coordinates": [0, 0]},
223+
"properties": {
224+
"@changesetId": 12345,
225+
"@lastEdit": 1234567890,
226+
"@osmId": 111,
227+
"@version": 1,
228+
"unwanted_field": "should_be_removed",
229+
"another_noise": 999,
230+
},
231+
},
232+
{
233+
"type": "Feature",
234+
"geometry": {"type": "Point", "coordinates": [1, 1]},
235+
"properties": {
236+
"@changesetId": 12346,
237+
"@lastEdit": 1234567891,
238+
"@osmId": 222,
239+
"@version": 2,
240+
"extra_data": "also_removed",
241+
},
242+
},
243+
],
244+
}
245+
mock_osmcha_response = {
246+
12345: {
247+
"username": "Sita",
248+
"comment": "This is a test comment",
249+
"editor": "iD",
250+
"userid": 1001,
251+
},
252+
12346: None,
253+
}
254+
255+
mock_query_osmcha.return_value = mock_osmcha_response
256+
257+
mock_query_osm.return_value = {
258+
12345: {
259+
"username": "Sita",
260+
"userid": "1001",
261+
"comment": "This is an updated test comment",
262+
"editor": "Ram",
263+
},
264+
12346: {
265+
"username": "Kiran",
266+
"userid": "1002",
267+
"comment": "This is a test comment",
268+
"editor": "Hari",
269+
},
270+
}
271+
272+
remove_noise_and_add_user_info(input_data)
273+
mock_query_osmcha.assert_called()
274+
called_ids = mock_query_osmcha.call_args[0][0]
275+
assert set(called_ids) == {12345, 12346}
276+
mock_query_osm.assert_called()

utils/tests/common_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.core.files.base import ContentFile
88
from pydantic_core import ValidationError as PydanticValidationError
99

10+
from apps.tutorial.models import generate_tutorial_firebase_id
1011
from main.tests import TestCase
1112
from utils.common import (
1213
compress_tasks,
@@ -262,3 +263,15 @@ def test_parse_b64gzjson_to_dict(self):
262263
compressed_task = compress_tasks(original_json)
263264
result = parse_b64gzjson_to_dict(compressed_task)
264265
assert result == original_json
266+
267+
def test_generate_tutorial_firebase_id(self):
268+
tutorial_firebase_id = generate_tutorial_firebase_id()
269+
assert tutorial_firebase_id.startswith("tutorial_")
270+
271+
# check is_valid
272+
split_id = tutorial_firebase_id.split("_")
273+
validate_ulid(split_id[1])
274+
275+
# check unique tutorial id
276+
tutorial_firebase_id_2 = generate_tutorial_firebase_id()
277+
assert tutorial_firebase_id != tutorial_firebase_id_2

utils/tests/geo_test.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import typing
2+
3+
from main.tests import TestCase
4+
from utils.geo.tile_functions import (
5+
lat_long_zoom_to_pixel_coords,
6+
pixel_coords_zoom_to_lat_lon,
7+
tile_coords_and_zoom_to_quad_key,
8+
)
9+
10+
11+
class TestGeoUtils(TestCase):
12+
@typing.override
13+
@classmethod
14+
def setUpClass(cls):
15+
super().setUpClass()
16+
17+
def test_lat_long_zoom_to_pixel_coords(self):
18+
# Lat=0, Lon=0 at zoom=0 (whole world 256x256)
19+
p = lat_long_zoom_to_pixel_coords(0, 0, 0)
20+
assert p.x == 128
21+
assert p.y == 128
22+
23+
# test with zoom level 1
24+
p = lat_long_zoom_to_pixel_coords(0, 0, 1)
25+
# At zoom 1, map is 512x512 pixels, middle is 256,256
26+
assert p.x == 256
27+
assert p.y == 256
28+
29+
# test with zoom level 2
30+
p = lat_long_zoom_to_pixel_coords(0, 0, 2)
31+
assert p.x == 512
32+
assert p.y == 512
33+
34+
# test with zoom level 5
35+
p = lat_long_zoom_to_pixel_coords(0, 0, 5)
36+
assert p.x == 4096
37+
assert p.y == 4096
38+
39+
def test_pixel_coords_zoom_to_lat_lon(self):
40+
# Center of the map at zoom 0
41+
lon, lat = pixel_coords_zoom_to_lat_lon(128, 128, 0)
42+
assert lon == 0.0
43+
assert lat == 0.0
44+
45+
# Known point (London: 51.5074° N, 0.1278° W)
46+
lon, lat = pixel_coords_zoom_to_lat_lon(512, 341.3333, 2)
47+
assert lon == 0
48+
assert lat == 51.32604237282497
49+
50+
def test_tile_coords_and_zoom_to_quadKey(self):
51+
# At zoom 0, only one tile: quadkey is empty
52+
assert not tile_coords_and_zoom_to_quad_key(0, 0, 0)
53+
54+
# Zoom 1: top-left, top-right, bottom-left, bottom-right
55+
assert tile_coords_and_zoom_to_quad_key(0, 0, 1) == "0"
56+
assert tile_coords_and_zoom_to_quad_key(1, 0, 1) == "1"
57+
assert tile_coords_and_zoom_to_quad_key(0, 1, 1) == "2"
58+
assert tile_coords_and_zoom_to_quad_key(1, 1, 1) == "3"
59+
60+
# Zoom 3: arbitrary
61+
assert tile_coords_and_zoom_to_quad_key(3, 5, 3) == "213"
62+
63+
# Zoom 5: arbitrary
64+
assert tile_coords_and_zoom_to_quad_key(10, 12, 5) == "03210"
65+
66+
# Zoom 22: top-left, bottom-right
67+
assert tile_coords_and_zoom_to_quad_key(0, 0, 22) == "0000000000000000000000"
68+
assert tile_coords_and_zoom_to_quad_key(4194303, 4194303, 22) == "3333333333333333333333" # max tile = (2**22) - 1
69+
70+
# Zoom 23: top-left, bottom-right
71+
assert tile_coords_and_zoom_to_quad_key(0, 0, 23) == "00000000000000000000000"
72+
assert tile_coords_and_zoom_to_quad_key(8388607, 8388607, 23) == "33333333333333333333333" # maxtile = (2*23) - 1

0 commit comments

Comments
 (0)