66Run with: pytest tests/ -m integration -v
77"""
88
9+ from __future__ import annotations
10+
11+ import tempfile
12+ from pathlib import Path
13+
914import pytest
1015
1116import abovepy
17+ from abovepy ._exceptions import CountyError , ProductError
1218
1319pytestmark = pytest .mark .integration
1420
1521
22+ # ---------------------------------------------------------------------------
23+ # STAC Connection & Search
24+ # ---------------------------------------------------------------------------
25+
26+
1627class TestLiveSTACConnection :
1728 def test_client_connects (self ):
1829 """Client can connect and list collections."""
@@ -56,6 +67,109 @@ def test_info_all_products(self):
5667 """info() returns metadata for all 9 products."""
5768 df = abovepy .info ()
5869 assert len (df ) == 9
70+ assert "product" in df .columns
71+ assert "resolution" in df .columns
72+ assert "format" in df .columns
73+
74+
75+ # ---------------------------------------------------------------------------
76+ # Product Coverage — search each of the 9 products
77+ # ---------------------------------------------------------------------------
78+
79+
80+ class TestProductCoverage :
81+ @pytest .mark .parametrize ("product" , [
82+ "dem_phase1" , "dem_phase2" , "dem_phase3" ,
83+ ])
84+ def test_dem_products (self , frankfort_bbox , product ):
85+ """DEM products return tiles for Frankfort area."""
86+ tiles = abovepy .search (bbox = frankfort_bbox , product = product , max_items = 3 )
87+ assert len (tiles ) > 0
88+ assert tiles .iloc [0 ]["product" ] == product
89+
90+ @pytest .mark .parametrize ("product" , [
91+ "ortho_phase1" , "ortho_phase2" , "ortho_phase3" ,
92+ ])
93+ def test_ortho_products (self , frankfort_bbox , product ):
94+ """Ortho products return tiles for Frankfort area."""
95+ tiles = abovepy .search (bbox = frankfort_bbox , product = product , max_items = 3 )
96+ assert len (tiles ) > 0
97+
98+ @pytest .mark .parametrize ("product" , [
99+ "laz_phase1" , "laz_phase2" , "laz_phase3" ,
100+ ])
101+ def test_laz_products (self , frankfort_bbox , product ):
102+ """LiDAR products return tiles for Frankfort area."""
103+ tiles = abovepy .search (bbox = frankfort_bbox , product = product , max_items = 3 )
104+ assert len (tiles ) > 0
105+
106+
107+ # ---------------------------------------------------------------------------
108+ # County Search
109+ # ---------------------------------------------------------------------------
110+
111+
112+ class TestCountySearch :
113+ @pytest .mark .parametrize ("county" , [
114+ "Franklin" , "Fayette" , "Pike" , "Jefferson" ,
115+ ])
116+ def test_search_counties (self , county ):
117+ """Multiple counties return DEM results."""
118+ tiles = abovepy .search (county = county , product = "dem_phase3" , max_items = 5 )
119+ assert len (tiles ) > 0
120+
121+ def test_county_case_insensitive (self ):
122+ """County search is case-insensitive."""
123+ tiles = abovepy .search (county = "franklin" , product = "dem_phase3" , max_items = 3 )
124+ assert len (tiles ) > 0
125+
126+ def test_invalid_county_raises (self ):
127+ """Unknown county raises CountyError."""
128+ with pytest .raises (CountyError ):
129+ abovepy .search (county = "Atlantis" , product = "dem_phase3" )
130+
131+
132+ # ---------------------------------------------------------------------------
133+ # Bbox Edge Cases
134+ # ---------------------------------------------------------------------------
135+
136+
137+ class TestBboxEdgeCases :
138+ def test_very_small_bbox (self ):
139+ """Very small bbox (single point area) still returns tiles."""
140+ tiny = (- 84.85 , 38.20 , - 84.849 , 38.201 )
141+ tiles = abovepy .search (bbox = tiny , product = "dem_phase3" , max_items = 5 )
142+ assert len (tiles ) > 0
143+
144+ def test_bbox_on_ky_border (self ):
145+ """Bbox on KY southern border returns results."""
146+ border = (- 84.5 , 36.50 , - 84.3 , 36.60 )
147+ tiles = abovepy .search (bbox = border , product = "dem_phase3" , max_items = 5 )
148+ # May or may not have tiles right at the border, but shouldn't error
149+ assert isinstance (tiles , type (tiles ))
150+
151+ def test_bbox_straddling_multiple_tiles (self ):
152+ """Large bbox should return many tiles."""
153+ large = (- 85.0 , 38.0 , - 84.5 , 38.5 )
154+ tiles = abovepy .search (bbox = large , product = "dem_phase3" , max_items = 100 )
155+ assert len (tiles ) > 5 # Should span multiple tile grid cells
156+
157+
158+ # ---------------------------------------------------------------------------
159+ # Error Cases
160+ # ---------------------------------------------------------------------------
161+
162+
163+ class TestErrorCases :
164+ def test_invalid_product_raises (self ):
165+ """Invalid product key raises ProductError."""
166+ with pytest .raises (ProductError ):
167+ abovepy .search (bbox = (- 84.9 , 38.15 , - 84.8 , 38.25 ), product = "invalid_product" )
168+
169+
170+ # ---------------------------------------------------------------------------
171+ # Read
172+ # ---------------------------------------------------------------------------
59173
60174
61175class TestLiveRead :
@@ -76,3 +190,95 @@ def test_read_full_tile(self, frankfort_bbox):
76190 data , profile = abovepy .read (url )
77191 assert data .shape [1 ] > 0
78192 assert data .shape [2 ] > 0
193+
194+ @pytest .mark .slow
195+ def test_read_returns_epsg3089 (self , frankfort_bbox ):
196+ """Read tile CRS should be EPSG:3089."""
197+ tiles = abovepy .search (bbox = frankfort_bbox , product = "dem_phase3" , max_items = 1 )
198+ url = tiles .iloc [0 ]["asset_url" ]
199+ _ , profile = abovepy .read (url )
200+ crs_str = str (profile ["crs" ])
201+ assert "3089" in crs_str
202+
203+
204+ # ---------------------------------------------------------------------------
205+ # Download & Mosaic
206+ # ---------------------------------------------------------------------------
207+
208+
209+ class TestLiveDownloadAndMosaic :
210+ @pytest .mark .slow
211+ def test_download_single_tile (self , frankfort_bbox ):
212+ """Download a single DEM tile and verify it exists."""
213+ tiles = abovepy .search (bbox = frankfort_bbox , product = "dem_phase3" , max_items = 1 )
214+ with tempfile .TemporaryDirectory () as tmpdir :
215+ paths = abovepy .download (tiles , output_dir = tmpdir )
216+ assert len (paths ) == 1
217+ assert paths [0 ].exists ()
218+ assert paths [0 ].stat ().st_size > 0
219+ assert paths [0 ].suffix == ".tif"
220+
221+ @pytest .mark .slow
222+ def test_download_skip_existing (self , frankfort_bbox ):
223+ """Second download should skip already-downloaded files."""
224+ tiles = abovepy .search (bbox = frankfort_bbox , product = "dem_phase3" , max_items = 1 )
225+ with tempfile .TemporaryDirectory () as tmpdir :
226+ paths1 = abovepy .download (tiles , output_dir = tmpdir )
227+ mtime1 = paths1 [0 ].stat ().st_mtime
228+ paths2 = abovepy .download (tiles , output_dir = tmpdir )
229+ mtime2 = paths2 [0 ].stat ().st_mtime
230+ assert mtime1 == mtime2 # File was not re-downloaded
231+
232+ @pytest .mark .slow
233+ def test_mosaic_vrt (self , frankfort_bbox ):
234+ """Download 2 tiles, mosaic to VRT, verify it's readable."""
235+ tiles = abovepy .search (bbox = frankfort_bbox , product = "dem_phase3" , max_items = 2 )
236+ if len (tiles ) < 2 :
237+ pytest .skip ("Need at least 2 tiles for mosaic test" )
238+ with tempfile .TemporaryDirectory () as tmpdir :
239+ paths = abovepy .download (tiles , output_dir = tmpdir )
240+ vrt_path = Path (tmpdir ) / "mosaic.vrt"
241+ result = abovepy .mosaic (paths , output = vrt_path )
242+ assert Path (result ).exists ()
243+ assert str (result ).endswith (".vrt" )
244+
245+
246+ # ---------------------------------------------------------------------------
247+ # Info
248+ # ---------------------------------------------------------------------------
249+
250+
251+ class TestLiveInfo :
252+ def test_info_columns (self ):
253+ """info() DataFrame has expected columns."""
254+ df = abovepy .info ()
255+ expected = {"product" , "display_name" , "format" , "resolution" , "phase" }
256+ assert expected .issubset (set (df .columns ))
257+
258+ def test_info_products_complete (self ):
259+ """info() includes all 9 known products."""
260+ df = abovepy .info ()
261+ products = set (df ["product" ])
262+ for p in ["dem_phase1" , "dem_phase2" , "dem_phase3" ,
263+ "ortho_phase1" , "ortho_phase2" , "ortho_phase3" ,
264+ "laz_phase1" , "laz_phase2" , "laz_phase3" ]:
265+ assert p in products , f"Missing product: { p } "
266+
267+
268+ # ---------------------------------------------------------------------------
269+ # LiDAR (optional — only runs if laspy is installed)
270+ # ---------------------------------------------------------------------------
271+
272+
273+ class TestLiDAROptional :
274+ @pytest .mark .slow
275+ def test_laz_tile_url_accessible (self , frankfort_bbox ):
276+ """LAZ tile URLs from search should be HTTP accessible."""
277+ import httpx
278+
279+ tiles = abovepy .search (bbox = frankfort_bbox , product = "laz_phase2" , max_items = 1 )
280+ if tiles .empty :
281+ pytest .skip ("No COPC tiles found in Frankfort area" )
282+ url = tiles .iloc [0 ]["asset_url" ]
283+ resp = httpx .head (url , follow_redirects = True , timeout = 30 )
284+ assert resp .status_code == 200
0 commit comments