resolve-backend / tests /test_api.py
RAONE777's picture
Map area super-resolution and global AlphaEarth index
ed24bc8 verified
Raw History Blame Contribute Delete
25.8 kB
import io
import re
import numpy as np
import pytest
import rasterio
from fastapi.testclient import TestClient
from PIL import Image
from rasterio.io import MemoryFile
from rasterio.transform import from_origin
from app.main import SAMPLES_DIR, app, load_samples
client = TestClient(app)
SAMPLE_IDS = [s["id"] for s in load_samples()]
def tif_bytes(arr: np.ndarray, **tags) -> bytes:
with MemoryFile() as mem:
with mem.open(driver="GTiff", count=arr.shape[0], height=arr.shape[1], width=arr.shape[2],
dtype=arr.dtype, crs="EPSG:32643", transform=from_origin(500000, 3000000, 10, 10)) as dst:
dst.write(arr)
dst.update_tags(**tags)
return mem.read()
def read_output(result_id: str):
r = client.get(f"/api/results/{result_id}/output.tif")
assert r.status_code == 200
with MemoryFile(r.content) as mem, mem.open() as src:
return src.read(), src.transform, src.crs, src.dtypes
def check_result(body: dict, w: int, h: int, input_refl: np.ndarray):
assert body["input"]["width"] == body["output"]["width"] == w * 4
assert body["input"]["height"] == body["output"]["height"] == h * 4
for key in ("input", "output"):
png = client.get(body[key]["png"])
assert png.status_code == 200 and png.headers["content-type"] == "image/png"
out, transform, crs, dtypes = read_output(body["id"])
assert out.shape == (4, h * 4, w * 4) and set(dtypes) == {"float32"}
assert np.isfinite(out).all()
pooled = out.reshape(4, h, 4, w, 4).mean(axis=(2, 4))
assert np.abs(pooled - input_refl).mean() < 0.02
return transform, crs
def test_health():
r = client.get("/api/health")
assert r.json() == {"status": "ok", "model": "SEN2SR-Lite RGBN x4", "device": "cpu"}
def test_samples_list():
r = client.get("/api/samples")
assert r.status_code == 200
items = r.json()
assert len(items) >= 3
assert set(items[0]) == {"id", "name", "location", "date", "width", "height"}
@pytest.mark.parametrize("sample_id", SAMPLE_IDS)
def test_sample_superres(sample_id):
r = client.post(f"/api/samples/{sample_id}/superres")
assert r.status_code == 200
body = r.json()
assert body["runtime_ms"] > 0 and body["crs"].startswith("EPSG:") and body["notes"]
with rasterio.open(SAMPLES_DIR / f"{sample_id}.tif") as src:
refl, w, h, t = src.read(), src.width, src.height, src.transform
transform, crs = check_result(body, w, h, refl)
assert transform.a == pytest.approx(t.a / 4) and transform.e == pytest.approx(t.e / 4)
assert (transform.c, transform.f) == (t.c, t.f)
def test_upload_matches_sample_endpoint():
sample = SAMPLE_IDS[0]
data = (SAMPLES_DIR / f"{sample}.tif").read_bytes()
r = client.post("/api/superres", files={"file": ("a.tif", data, "image/tiff")})
assert r.status_code == 200
with rasterio.open(SAMPLES_DIR / f"{sample}.tif") as src:
check_result(r.json(), src.width, src.height, src.read())
def test_non_square_upload_and_band_order():
yy, xx = np.mgrid[0:100, 0:150]
base = 0.15 + 0.1 * np.sin(xx / 9.0) * np.cos(yy / 7.0)
refl = np.stack([base * k for k in (1.0, 0.9, 0.7, 1.5)]).astype("float32")
shuffled = refl[[3, 2, 1, 0]] # file order B8,B2,B3,B4
r = client.post("/api/superres", files={"file": ("a.tif", tif_bytes(shuffled), "image/tiff")},
data={"band_order": "B8,B2,B3,B4"})
assert r.status_code == 200
check_result(r.json(), 150, 100, refl)
def test_integer_dn_scaling_depends_on_baseline():
dn = (np.full((4, 64, 64), 3000)).astype("uint16")
old = client.post("/api/superres", files={"file": ("a.tif", tif_bytes(dn), "image/tiff")}).json()
new = client.post("/api/superres", files={"file": ("a.tif", tif_bytes(dn, processing_baseline="05.11"), "image/tiff")}).json()
assert read_output(old["id"])[0].mean() == pytest.approx(0.3, abs=0.02)
assert read_output(new["id"])[0].mean() == pytest.approx(0.2, abs=0.02)
assert any("offset" in n for n in new["notes"])
def test_error_three_bands():
r = client.post("/api/superres", files={"file": ("a.tif", tif_bytes(np.zeros((3, 64, 64), "float32")), "image/tiff")})
assert r.status_code == 400 and "4 bands" in r.json()["detail"]
def test_error_not_a_tif():
r = client.post("/api/superres", files={"file": ("a.txt", b"hello", "text/plain")})
assert r.status_code == 400 and "detail" in r.json()
def test_error_oversize():
big = tif_bytes(np.zeros((4, 513, 100), "float32"))
r = client.post("/api/superres", files={"file": ("a.tif", big, "image/tiff")})
assert r.status_code == 413
def test_error_bad_band_order():
data = (SAMPLES_DIR / f"{SAMPLE_IDS[0]}.tif").read_bytes()
r = client.post("/api/superres", files={"file": ("a.tif", data, "image/tiff")}, data={"band_order": "B4,B3"})
assert r.status_code == 400
def test_unknown_sample_and_result():
assert client.post("/api/samples/nope/superres").status_code == 404
assert client.get("/api/results/" + "0" * 32 + "/output.tif").status_code == 404
assert client.get("/api/results/../output.tif").status_code in (404, 422)
@pytest.mark.parametrize("sample_id", SAMPLE_IDS)
def test_sample_thumbnail(sample_id):
r = client.get(f"/api/samples/{sample_id}/input.png")
assert r.status_code == 200 and r.headers["content-type"] == "image/png"
img = Image.open(io.BytesIO(r.content))
assert img.format == "PNG" and img.size == (256, 256) and img.mode == "RGB"
def test_thumbnail_unknown_sample():
assert client.get("/api/samples/nope/input.png").status_code == 404
def check_scene_patches(body: dict, w: int, h: int):
sc = body["scene"]
west, south, east, north = sc["bounds"]
assert west < east and south < north
assert 5 < sc["center"]["lat"] < 38 and 65 < sc["center"]["lon"] < 100
assert west < sc["center"]["lon"] < east and south < sc["center"]["lat"] < north
assert sc["crs"].startswith("EPSG:") and sc["pixel_size_m"] == pytest.approx(10)
assert (sc["width"], sc["height"]) == (w, h)
p = body["patches"]
assert p["tile"] == 128 and p["overlap"] == 32
assert p["count"] == p["cols"] * p["rows"] >= 1
@pytest.mark.parametrize("sample_id", SAMPLE_IDS)
def test_sample_scene_and_patches(sample_id):
body = client.post(f"/api/samples/{sample_id}/superres").json()
check_scene_patches(body, 256, 256)
sc = body["scene"]
assert re.fullmatch(r"\d{4}-\d{2}-\d{2}", sc["date"]) and re.fullmatch(r"T\d{2}[A-Z]{3}", sc["tile_id"])
assert sc["satellite"].startswith("Sentinel-2")
assert body["patches"] == {"tile": 128, "overlap": 32, "cols": 3, "rows": 3, "count": 9}
def test_scene_from_source_item_tag_and_single_pass():
item = "S2A_MSIL2A_20250101T054231_R005_T42QZG_20250101T085751"
data = tif_bytes(np.full((4, 64, 64), 0.2, "float32"), source_item=item)
body = client.post("/api/superres", files={"file": ("a.tif", data, "image/tiff")}).json()
sc = body["scene"]
assert (sc["date"], sc["satellite"], sc["tile_id"], sc["source_item"]) == ("2025-01-01", "Sentinel-2A", "T42QZG", item)
assert body["patches"]["cols"] == body["patches"]["rows"] == 1
# ---- analysis layers, stages, lock, AlphaEarth ------------------------------------------------
BASE_LAYERS = ["rgb", "nir_false", "ndvi", "ndwi", "water", "landcover", "builtup", "fields", "wavelet",
"features", "uncertainty", "inferred", "confidence"]
STAGES = ["ingest", "normalise", "alphaearth", "patching", "backbone", "wavelet", "lock", "uncertainty", "products"]
def layer_pixels(body: dict, layer_id: str) -> np.ndarray:
r = client.get(f"/api/results/{body['id']}/layers/{layer_id}.png")
assert r.status_code == 200
return np.array(Image.open(io.BytesIO(r.content)).convert("RGBA"))
def check_layers(body: dict, expect_ae: bool = False):
ids = [l["id"] for l in body["layers"]]
assert ids[:len(BASE_LAYERS)] == BASE_LAYERS
assert set(ids) - set(BASE_LAYERS) == ({"alphaearth", "gate"} if expect_ae else set())
size = (body["output"]["width"], body["output"]["height"])
for l in body["layers"]:
assert l["url"] == f"/api/results/{body['id']}/layers/{l['id']}.png" and l["name"] and l["group"]
lg = l["legend"]
if lg["type"] == "ramp":
assert lg["min"] < lg["max"] or l["id"] == "rgb"
assert lg["min_label"] and lg["max_label"] and lg["colormap"]
else:
assert lg["type"] == "classes" and lg["classes"]
assert all(re.fullmatch(r"#[0-9A-Fa-f]{6}", c["color"]) and c["label"] for c in lg["classes"])
r = client.get(l["url"])
expected = "image/png" if lg["type"] == "classes" or l["id"] == "rgb" else "image/webp"
assert r.status_code == 200 and r.headers["content-type"] == expected
assert Image.open(io.BytesIO(r.content)).size == size
assert client.get(l["url"]).content == r.content # cached render is stable
@pytest.mark.parametrize("sample_id", SAMPLE_IDS)
def test_sample_layers_stages_confidence_lock(sample_id):
body = client.post(f"/api/samples/{sample_id}/superres").json()
check_layers(body)
assert [s["id"] for s in body["stages"]] == STAGES
assert all(isinstance(s["ms"], int) and s["ms"] >= 0 for s in body["stages"])
assert body["stages"][STAGES.index("backbone")]["ms"] > 0
c = body["confidence"]
assert 0 <= c["mean"] <= 1 and 0 <= c["high_fraction"] <= 1
lock = body["lock"]
assert 0 < lock["consistency_after"] < lock["consistency_before"] < 1 and lock["applied"] is True
assert body["alphaearth"] == {"available": False, "year": None, "source": None,
"note": "AlphaEarth lookup is switched off on this server."}
def test_layer_semantics_on_vegetation_and_water():
# left half vegetation (high NIR), right half water (low NIR, green above NIR)
refl = np.zeros((4, 64, 64), "float32")
refl[:, :, :32] = np.array([0.05, 0.08, 0.04, 0.45], "float32")[:, None, None]
refl[:, :, 32:] = np.array([0.03, 0.06, 0.05, 0.02], "float32")[:, None, None]
body = client.post("/api/superres", files={"file": ("a.tif", tif_bytes(refl), "image/tiff")}).json()
out = read_output(body["id"])[0]
assert out[3, :, :100].mean() > 0.3 and out[3, :, 160:].mean() < 0.1
water = layer_pixels(body, "water")
assert water[:, 200:, 3].mean() > 250 and water[:, :60, 3].mean() < 5 # water opaque, other transparent
lc = layer_pixels(body, "landcover")
assert tuple(lc[128, 20, :3]) == (0x15, 0x80, 0x3D) # dense vegetation
assert tuple(lc[128, 230, :3]) == (0x25, 0x63, 0xEB) # water
def test_uncertainty_uses_eight_passes_when_asked(monkeypatch):
monkeypatch.setenv("RESOLVE_TTA", "8")
data = tif_bytes(np.random.default_rng(0).uniform(0.05, 0.3, (4, 40, 40)).astype("float32"))
body = client.post("/api/superres", files={"file": ("a.tif", data, "image/tiff")}).json()
unc = next(l for l in body["layers"] if l["id"] == "uncertainty")
assert "8 test-time" in unc["legend"]["note"] and unc["legend"]["max"] > 0
def test_layer_endpoint_errors():
body = client.post(f"/api/samples/{SAMPLE_IDS[0]}/superres").json()
assert client.get(f"/api/results/{body['id']}/layers/nope.png").status_code == 404
assert client.get(f"/api/results/{body['id']}/layers/gate.png").status_code == 404 # no AlphaEarth, no gate
assert client.get("/api/results/" + "0" * 32 + "/layers/ndvi.png").status_code == 404
assert client.get(f"/api/results/{body['id']}/layers/NDVI.png").status_code == 404
def test_lock_reduces_inconsistency():
from app.lock import lock
rng = np.random.default_rng(1)
z = rng.uniform(0.05, 0.3, (4, 128, 128)).astype("float32")
y = z.reshape(4, 32, 4, 32, 4).mean((2, 4)) * 1.1
x, before, after = lock(z, y)
assert after < before and np.isfinite(x).all() and x.min() >= 0
def test_haar_energy_and_dihedral():
from app.layers import haar_energy
from app.model import DIHEDRAL, d_apply, d_invert
flat = np.full((4, 32, 32), 0.2, "float32")
assert haar_energy(flat).max() == pytest.approx(0, abs=1e-6)
stripes = flat.copy()
stripes[:, :, ::2] += 0.1
assert haar_energy(stripes).mean() > 0.05
x = np.random.default_rng(2).random((4, 6, 9))
for k, f in DIHEDRAL:
assert np.array_equal(d_invert(d_apply(x, k, f), k, f), x)
def test_alphaearth_unit_helpers():
from app import alphaearth as ae
assert ae.target_year("2025-03-22") == 2024 and ae.target_year("2017-01-01") == 2017
assert ae.target_year("2030-01-01") == 2025 and ae.target_year(None) == 2024
out = ae._dequantise(np.array([[-128, 127, -127, 0]], "int8"))
assert np.isnan(out[0, 0]) and out[0, 1] == pytest.approx((127 / 127.5) ** 2) and out[0, 2] < 0 and out[0, 3] == 0
idx = ae._index()
assert idx["blocks"]["2024/43N"] and idx["grid"]["west0"] == 8480 and idx["grid"]["size"] == 81920
assert len(ae.cells(2024, "43N")) > 100 and ae.cells(2024, "99N") is None
def test_alphaearth_network_failure_does_not_break_request(monkeypatch, tmp_path):
from app import alphaearth as ae
monkeypatch.setenv("RESOLVE_ALPHAEARTH", "1")
monkeypatch.setattr(ae, "CACHE_DIR", tmp_path)
monkeypatch.setattr(ae, "_failures", {})
def boom(*a, **k):
raise OSError("network down")
monkeypatch.setattr(ae, "_read_cell_band", boom)
body = client.post(f"/api/samples/{SAMPLE_IDS[0]}/superres").json()
a = body["alphaearth"]
assert a["available"] is False and a["year"] is None and a["source"] is None and a["note"]
check_layers(body, expect_ae=False)
def test_alphaearth_present_adds_layers(monkeypatch):
from app import alphaearth as ae
emb = np.random.default_rng(3).normal(size=(6, 256, 256)).astype("float32")
def fake(crs, transform, w, h, date, budget=None):
return ae._result(True, 2024, "Read 6 of 64 dimensions.", emb, ae.SOURCE)
monkeypatch.setattr(ae, "fetch", fake)
body = client.post(f"/api/samples/{SAMPLE_IDS[0]}/superres").json()
assert body["alphaearth"]["available"] and body["alphaearth"]["year"] == 2024 and body["alphaearth"]["source"]
check_layers(body, expect_ae=True)
def _online() -> bool:
import socket
try:
socket.create_connection(("s3.us-west-2.amazonaws.com", 443), timeout=3).close()
return True
except OSError:
return False
@pytest.mark.skipif(not _online(), reason="needs network access to the AlphaEarth bucket")
def test_alphaearth_live_window_read(monkeypatch, tmp_path):
from app import alphaearth as ae
monkeypatch.setenv("RESOLVE_ALPHAEARTH", "1")
monkeypatch.setenv("AEF_BANDS", "3")
monkeypatch.setenv("AEF_TIMEOUT_S", "30")
monkeypatch.setattr(ae, "CACHE_DIR", tmp_path)
monkeypatch.setattr(ae, "_failures", {})
with rasterio.open(SAMPLES_DIR / "delhi-city.tif") as src:
res = ae.fetch(src.crs, src.transform, src.width, src.height, "2025-03-24")
if not res["available"]:
pytest.skip(f"AlphaEarth not reachable right now: {res['note']}")
emb = res["emb"]
assert res["year"] == 2024 and emb.shape == (3, 256, 256)
assert np.nanmax(np.nansum(emb ** 2, 0)) <= 1.01 # 3 dims of a unit-length 64-d vector
assert np.isnan(emb).mean() < 0.01 and emb.std() > 0.01
GLOBAL_POINTS = { # name: (lat, lon, expected UTM zone), from the bundled global index (offline)
"Delhi": (28.61, 77.21, "43N"), "Paris": (48.8566, 2.3522, "31N"), "Nairobi": (-1.2921, 36.8219, "37S"),
"Sao Paulo": (-23.55, -46.63, "23S"), "Sydney": (-33.87, 151.21, "56S"), "Anchorage": (61.2, -149.9, "6N"),
"Quito": (-0.18, -78.47, "17S"), "Cape Town": (-33.92, 18.42, "34S"),
}
@pytest.mark.parametrize("name", GLOBAL_POINTS)
def test_alphaearth_global_index_lookup(name):
from app import alphaearth as ae
lat, lon, zone = GLOBAL_POINTS[name]
for year in (2017, 2024, 2025):
url = ae.locate(lat, lon, year)
assert url and f"/{year}/{zone}/" in url and url.endswith(".tiff")
assert ae.locate(0.0, -160.0, 2024) is None # open Pacific, no file
def test_alphaearth_index_is_global_and_small():
from app import alphaearth as ae
assert ae.INDEX_PATH.stat().st_size < 3_000_000
zones = {k.split("/")[1] for k in ae._index()["blocks"]}
assert len(zones) == 120
assert sum(len(ae.cells(2024, z) or {}) for z in zones) > 30000
# ---- area super-resolution -------------------------------------------------------------------------------
AREA_BAD = [
({"lat": 90, "lon": 0}, "lat"), ({"lat": 0, "lon": 181}, "lon"), ({"lat": 10, "lon": 10, "size_px": 300}, "size_px"),
({"lat": 10, "lon": 10, "date_from": "2025-05-01", "date_to": "2025-04-01"}, "date_from"),
({"lat": 10, "lon": 10, "date_to": "2999-01-01"}, "future"), ({"lat": 10, "lon": 10, "date_from": "yesterday"}, "date_from"),
({"lat": 10, "lon": 10, "max_cloud": 150}, "max_cloud"), ({"lon": 10}, "lat"),
]
@pytest.mark.parametrize("body,word", AREA_BAD)
def test_area_validation_errors(body, word):
r = client.post("/api/area/superres", json=body)
assert r.status_code == 422 and isinstance(r.json()["detail"], str) and word in r.json()["detail"]
class FakeAsset:
def __init__(self, href, shape, transform):
self.href, self.extra_fields = href, {"proj:shape": shape, "proj:transform": transform}
class FakeItem:
def __init__(self, id_, day, cloud, mgrs="43RGM"):
from datetime import datetime, timezone
self.id = id_
self.datetime = datetime(2025, 3, day, 5, 30, tzinfo=timezone.utc)
self.properties = {"eo:cloud_cover": cloud, "s2:processing_baseline": "05.12", "proj:code": "EPSG:32643",
"platform": "Sentinel-2B", "s2:mgrs_tile": mgrs}
t10, t20 = [10.0, 0, 660000.0, 0, -10.0, 3260040.0], [20.0, 0, 660000.0, 0, -20.0, 3260040.0]
self.assets = {b: FakeAsset(f"fake/{id_}/{b}", [10980, 10980], t10) for b in ("B04", "B03", "B02", "B08")}
self.assets["SCL"] = FakeAsset(f"fake/{id_}/SCL", [5490, 5490], t20)
def install_fake_stac(monkeypatch, items, cloudy=()):
"""Replace the catalogue search and the window reader. Items named in `cloudy` return SCL cloud everywhere."""
from app import area
calls = []
monkeypatch.setattr(area, "_search", lambda *a, **k: (calls.append(a), items)[1])
def fake_read(href, col0, row0, size, scl):
item_id = href.split("/")[-2]
if scl:
return np.full((size, size), 9 if item_id in cloudy else 4, "uint8")
yy, xx = np.mgrid[:size, :size]
base = 1000 + 300 * (np.sin(xx / 7.0) + np.cos(yy / 9.0)) + 200 * ((xx // 16 + yy // 16) % 2)
return (base + (2000 if href.endswith("B08") else 0)).astype("uint16")
monkeypatch.setattr(area, "_read_band", fake_read)
return calls
def check_area_body(body, size):
assert body["input"]["width"] == size * 4 and body["stages"][0]["id"] == "search" and body["stages"][0]["ms"] >= 0
assert [s["id"] for s in body["stages"][1:3]] == ["ingest", "normalise"]
ss = body["source_scene"]
assert set(ss) >= {"item_id", "date", "satellite", "tile_id", "cloud_cover", "window_cloud_fraction",
"processing_baseline", "relaxed_cloud"}
assert body["scene"]["source_item"] == ss["item_id"] and body["scene"]["date"] == ss["date"]
assert body["scene"]["satellite"] == ss["satellite"] and body["scene"]["tile_id"] == ss["tile_id"]
assert {l["id"] for l in body["layers"]} >= {"rgb", "ndvi", "uncertainty", "confidence"}
r = client.get(f"/api/results/{body['id']}/input.tif")
assert r.status_code == 200
with MemoryFile(r.content) as mem, mem.open() as src:
assert src.count == 4 and src.width == src.height == size and src.crs.to_epsg() in (32631, 32643)
return src.read()
def test_area_superres_mocked_stac(monkeypatch):
items = [FakeItem("S2B_MSIL2A_20250310T053000_R062_T43RGM_20250310T080000", 10, 3.0),
FakeItem("S2A_MSIL2A_20250320T053000_R062_T43RGM_20250320T080000", 20, 8.0)]
calls = install_fake_stac(monkeypatch, items)
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2, "size_px": 256, "name": "Test"})
assert r.status_code == 200, r.text
body = r.json()
inp = check_area_body(body, 256)
assert body["name"] == "Test" and abs(body["scene"]["center"]["lat"] - 28.9) < 0.01
ss = body["source_scene"]
assert ss["item_id"].startswith("S2A_MSIL2A_20250320") and ss["date"] == "2025-03-20" # latest first
assert ss["satellite"] == "Sentinel-2B" and ss["tile_id"] == "T43RGM" and ss["processing_baseline"] == "05.12"
assert ss["cloud_cover"] == 8.0 and ss["window_cloud_fraction"] == 0 and ss["relaxed_cloud"] is False
assert inp.max() < 1.0 and 0.0 < inp[0].mean() < 0.3 # offset of 1000 DN removed, then / 10 000
assert calls[0][4] == 20 and calls[0][5] is True # max_cloud, latest mode
def test_area_range_prefers_lowest_cloud_and_skips_cloudy_windows(monkeypatch):
items = [FakeItem("S2B_MSIL2A_20250305T053000_R062_T43RGM_x", 5, 2.0),
FakeItem("S2B_MSIL2A_20250312T053000_R062_T43RGM_x", 12, 6.0),
FakeItem("S2B_MSIL2A_20250322T053000_R062_T43RGM_x", 22, 1.0)]
install_fake_stac(monkeypatch, items, cloudy=("S2B_MSIL2A_20250322T053000_R062_T43RGM_x",))
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2, "date_from": "2025-03-01", "date_to": "2025-03-31"})
assert r.status_code == 200, r.text
assert r.json()["source_scene"]["date"] == "2025-03-05" # lowest cloud (22nd) was cloudy in the window, next lowest wins
def test_area_relaxed_cloud_is_flagged(monkeypatch):
install_fake_stac(monkeypatch, [FakeItem("S2B_MSIL2A_20250310T053000_R062_T43RGM_x", 10, 45.0)])
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2, "max_cloud": 10})
assert r.status_code == 200 and r.json()["source_scene"]["relaxed_cloud"] is True and r.json()["name"] == "28.9000, 77.2000"
assert any("45" in n for n in r.json()["notes"])
NO_SCENE = "No clear Sentinel-2 scene found for this area and date range. Try a wider date range or a higher cloud limit."
def test_area_no_scene_is_404(monkeypatch):
install_fake_stac(monkeypatch, [])
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2})
assert r.status_code == 404 and r.json() == {"detail": NO_SCENE}
def test_area_all_windows_cloudy_is_404(monkeypatch):
item = FakeItem("S2B_MSIL2A_20250310T053000_R062_T43RGM_x", 10, 3.0)
install_fake_stac(monkeypatch, [item], cloudy=(item.id,))
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2})
assert r.status_code == 404 and r.json() == {"detail": NO_SCENE}
def test_area_tile_not_containing_square_is_skipped(monkeypatch):
install_fake_stac(monkeypatch, [FakeItem("S2B_MSIL2A_20250310T053000_R062_T43RGM_x", 10, 3.0)])
r = client.post("/api/area/superres", json={"lat": 10.0, "lon": 77.2}) # far south of the fake tile
assert r.status_code == 404
def test_area_upstream_failure_is_503(monkeypatch):
from app import area
def boom(*a, **k):
raise ConnectionError("down")
monkeypatch.setattr(area, "_search", boom)
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2})
assert r.status_code == 503 and r.json()["detail"]
install_fake_stac(monkeypatch, [FakeItem("S2B_MSIL2A_20250310T053000_R062_T43RGM_x", 10, 3.0)])
monkeypatch.setattr(area, "_read_band", boom)
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2})
assert r.status_code == 503 and r.json()["detail"]
def test_area_hard_timeout_is_503(monkeypatch):
import time as _t
from app import area
monkeypatch.setattr(area, "_search", lambda *a, **k: _t.sleep(3))
monkeypatch.setenv("RESOLVE_AREA_TIMEOUT_S", "0.5")
t = _t.time()
r = client.post("/api/area/superres", json={"lat": 28.9, "lon": 77.2, "size_px": 256})
assert r.status_code == 503 and _t.time() - t < 2.5
def _pc_online() -> bool:
import socket
try:
socket.create_connection(("planetarycomputer.microsoft.com", 443), timeout=3).close()
return True
except OSError:
return False
@pytest.mark.skipif(not _pc_online(), reason="needs network access to the Planetary Computer")
def test_area_live_paris_256(monkeypatch):
monkeypatch.setenv("RESOLVE_AREA_TIMEOUT_S", "60")
r = client.post("/api/area/superres", json={"lat": 48.8566, "lon": 2.3522, "size_px": 256, "max_cloud": 40,
"date_from": "2025-04-01", "date_to": "2025-09-30"})
if r.status_code == 503:
pytest.skip(f"Planetary Computer not reachable right now: {r.json()['detail']}")
assert r.status_code == 200, r.text
body = r.json()
inp = check_area_body(body, 256)
ss = body["source_scene"]
assert ss["tile_id"].startswith("T31") and "2025-04-01" <= ss["date"] <= "2025-09-30" and ss["window_cloud_fraction"] <= 0.1
assert 0.0 < inp.mean() < 1.0 and body["scene"]["center"]["lat"] == pytest.approx(48.8566, abs=0.01)