Coverage for tests/test_places.py: 100%

78 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-07-22 06:43 -0400

1"""Test the places.py module.""" 

2 

3from copy import deepcopy 

4from typing import Any 

5 

6import pydantic 

7import pytest 

8 

9from src.overturetoosm.objects import ConfidenceError, UnmatchedError 

10from src.overturetoosm.places import process_place 

11from src.overturetoosm.utils import process_geojson 

12 

13 

14@pytest.fixture(name="clean_dict") 

15def clean_fix() -> dict[str, Any]: 

16 """Fixture with the clean place properties.""" 

17 return { 

18 "name": "Primary Name", 

19 "brand": "Brand Name", 

20 "brand:wikidata": "Q123", 

21 "addr:street_address": "123 Main St", 

22 "addr:city": "City", 

23 "addr:postcode": "12345", 

24 "addr:state": "CA", 

25 "addr:country": "US", 

26 "phone": "+1234567890", 

27 "website": "https://example.com/", 

28 "source": "dataset1 via overturetoosm", 

29 "office": "lawyer", 

30 "lawyer": "notary", 

31 "contact:facebook": "https://www.facebook.com/example/", 

32 } 

33 

34 

35@pytest.fixture(name="geojson_dict") 

36def geojson_fix() -> dict[str, Any]: 

37 """Fixture with a mock place geojson.""" 

38 return { 

39 "type": "FeatureCollection", 

40 "features": [ 

41 { 

42 "type": "Feature", 

43 "geometry": {"type": "Point", "coordinates": [-1, 1]}, 

44 "properties": { 

45 "id": "123", 

46 "version": 1, 

47 "update_time": "2022-01-01T00:00:00Z", 

48 "sources": [ 

49 { 

50 "property": "property1", 

51 "dataset": "dataset1", 

52 "record_id": "record1", 

53 "confidence": 0.8, 

54 } 

55 ], 

56 "names": { 

57 "primary": "Primary Name", 

58 "common": {"ex": "Example common name"}, 

59 "rules": [{"variant": "short", "value": "EX"}], 

60 }, 

61 "brand": { 

62 "wikidata": "Q123", 

63 "names": { 

64 "primary": "Brand Name", 

65 "common": {"ex": "Example common name"}, 

66 "rules": [{"variant": "short", "value": "EX"}], 

67 }, 

68 }, 

69 "categories": { 

70 "main": "notary_public", 

71 "alternate": ["alternate_category1", "alternate_category2"], 

72 }, 

73 "confidence": 0.8, 

74 "websites": ["https://example.com/"], 

75 "socials": ["https://www.facebook.com/example/"], 

76 "phones": ["+1234567890"], 

77 "addresses": [ 

78 { 

79 "freeform": "123 Main St", 

80 "locality": "City", 

81 "postcode": "12345", 

82 "region": "CA", 

83 "country": "US", 

84 } 

85 ], 

86 }, 

87 } 

88 ], 

89 } 

90 

91 

92@pytest.fixture(name="props_dict") 

93def props_fix() -> dict[str, Any]: 

94 """Fixture with the raw place properties.""" 

95 return { 

96 "id": "123", 

97 "version": 1, 

98 "update_time": "2022-01-01T00:00:00Z", 

99 "sources": [ 

100 { 

101 "property": "property1", 

102 "dataset": "dataset1", 

103 "record_id": "record1", 

104 "confidence": 0.8, 

105 } 

106 ], 

107 "names": { 

108 "primary": "Primary Name", 

109 "common": {"ex": "Example common name"}, 

110 "rules": [{"variant": "short", "value": "EX"}], 

111 }, 

112 "brand": { 

113 "wikidata": "Q123", 

114 "names": { 

115 "primary": "Brand Name", 

116 "common": {"ex": "Example common name"}, 

117 "rules": [{"variant": "short", "value": "EX"}], 

118 }, 

119 }, 

120 "categories": { 

121 "main": "notary_public", 

122 "alternate": ["alternate_category1", "alternate_category2"], 

123 }, 

124 "confidence": 0.8, 

125 "websites": ["https://example.com/"], 

126 "socials": ["https://www.facebook.com/example/"], 

127 "phones": ["+1234567890"], 

128 "addresses": [ 

129 { 

130 "freeform": "123 Main St", 

131 "locality": "City", 

132 "postcode": "12345", 

133 "region": "CA", 

134 "country": "US", 

135 } 

136 ], 

137 } 

138 

139 

140def test_place_props(props_dict: dict, clean_dict: dict) -> None: 

141 """Test that all properties are processed correctly.""" 

142 new_props = process_place(props_dict) 

143 assert new_props == clean_dict 

144 

145 

146def test_place_props_no_brand(props_dict: dict, clean_dict: dict) -> None: 

147 """Test that all properties are processed correctly.""" 

148 props_dict.pop("brand", None) 

149 new_props = process_place(props_dict) 

150 for i in ["brand", "brand:wikidata"]: 

151 clean_dict.pop(i, None) 

152 assert new_props == clean_dict 

153 

154 

155def test_place_props_no_category(props_dict: dict, clean_dict: dict) -> None: 

156 """Test that all properties are processed correctly.""" 

157 props_dict.pop("categories", None) 

158 new_props = process_place(props_dict) 

159 for i in ["office", "lawyer"]: 

160 clean_dict.pop(i, None) 

161 assert new_props == clean_dict 

162 

163 

164def test_place_props_twitter(props_dict: dict, clean_dict: dict) -> None: 

165 """Test that all properties are processed correctly.""" 

166 props_dict["socials"].append("https://twitter.com/example/") 

167 new_props = process_place(props_dict) 

168 clean_dict["contact:twitter"] = "https://twitter.com/example/" 

169 assert new_props == clean_dict 

170 

171 

172def test_low_confidence(props_dict) -> None: 

173 """Test that properties with low confidence are not processed.""" 

174 with pytest.raises(ConfidenceError): 

175 process_place(props_dict, confidence=0.9) 

176 

177 

178def test_confidence(props_dict) -> None: 

179 """Test that invalid properties are not processed.""" 

180 props_dict["confidence"] = -0.1 

181 with pytest.raises(pydantic.ValidationError): 

182 process_place(props_dict) 

183 

184 

185def test_unmatched_error(props_dict) -> None: 

186 """Test that invalid properties are not processed.""" 

187 props_dict["categories"]["main"] = "invalid_category" 

188 with pytest.raises(UnmatchedError): 

189 process_place(props_dict, unmatched="error") 

190 

191 

192def test_unmatched_ignore(props_dict, clean_dict: dict) -> None: 

193 """Test that invalid properties are not processed.""" 

194 props_dict["categories"]["main"] = "invalid_category" 

195 for i in ["office", "lawyer"]: 

196 clean_dict.pop(i, None) 

197 assert process_place(props_dict, unmatched="ignore") == clean_dict 

198 

199 

200def test_unmatched_force(props_dict, clean_dict: dict) -> None: 

201 """Test that invalid properties are not processed.""" 

202 cat = "invalid_category" 

203 props_dict["categories"]["main"] = cat 

204 for i in ["office", "lawyer"]: 

205 clean_dict.pop(i, None) 

206 clean_dict["type"] = cat 

207 assert process_place(props_dict, unmatched="force") == clean_dict 

208 

209 

210def test_place_geojson(geojson_dict, clean_dict: dict) -> None: 

211 """Test that all properties are processed correctly.""" 

212 assert process_geojson(geojson=geojson_dict, fx=process_place) == { 

213 "type": "FeatureCollection", 

214 "features": [ 

215 { 

216 "type": "Feature", 

217 "geometry": {"type": "Point", "coordinates": [-1, 1]}, 

218 "properties": clean_dict, 

219 } 

220 ], 

221 } 

222 

223 

224def test_place_geojson_error(geojson_dict) -> None: 

225 """Test that all properties are processed correctly when error flag is set.""" 

226 copy = deepcopy(geojson_dict) 

227 copy["features"][0]["properties"]["categories"]["main"] = "invalid_category" 

228 assert process_geojson( 

229 geojson=copy, fx=process_place, options={"unmatched": "error"} 

230 ) == {"type": "FeatureCollection", "features": []} 

231 

232 

233def test_place_geojson_force(geojson_dict, clean_dict: dict) -> None: 

234 """Test that all properties are processed correctly when force flag is set.""" 

235 copy = deepcopy(geojson_dict) 

236 copy["features"][0]["properties"]["categories"]["main"] = "invalid_category" 

237 clean_dict["type"] = "invalid_category" 

238 for i in ["office", "lawyer"]: 

239 clean_dict.pop(i, None) 

240 assert process_geojson( 

241 geojson=copy, fx=process_place, options={"unmatched": "force"} 

242 ) == { 

243 "type": "FeatureCollection", 

244 "features": [ 

245 { 

246 "type": "Feature", 

247 "geometry": {"type": "Point", "coordinates": [-1, 1]}, 

248 "properties": clean_dict, 

249 } 

250 ], 

251 } 

252 

253 

254def test_place_geojson_ignore(geojson_dict, clean_dict: dict) -> None: 

255 """Test that all properties are processed correctly when ignore flag is set.""" 

256 copy = deepcopy(geojson_dict) 

257 copy["features"][0]["properties"]["categories"]["main"] = "invalid_category" 

258 for i in ["office", "lawyer"]: 

259 clean_dict.pop(i, None) 

260 assert process_geojson( 

261 geojson=copy, fx=process_place, options={"unmatched": "ignore"} 

262 ) == { 

263 "type": "FeatureCollection", 

264 "features": [ 

265 { 

266 "type": "Feature", 

267 "geometry": {"type": "Point", "coordinates": [-1, 1]}, 

268 "properties": clean_dict, 

269 } 

270 ], 

271 }