relo-calculator / tests /test_integration.py
sabilmakbar's picture
add integration, regression and security test layers
903a3b6
Raw
History Blame Contribute Delete
4.34 kB
"""Integration tests — exercise the full request → model → render pipeline.
External I/O (Numbeo scrape, Yahoo/Frankfurter FX) is monkeypatched at the
import site so these run offline, but everything else (validation, currency
inference, the savings model, template rendering) runs for real.
"""
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
HIGHER_COL = {"valuePct": 134.5, "direction": "higher"}
HIGHER_RENT = {"valuePct": 409.4, "direction": "higher"}
async def fake_diffs(country1, city1, country2, city2):
return {"city_from": city1, "city_to": city2,
"col_excl_rent": HIGHER_COL, "rent": HIGHER_RENT}
def fake_fx(rate=0.30, spot_only=False):
async def _fx(from_currency, to_currency):
return {
"pair": f"{from_currency}/{to_currency}", "source": "Yahoo Finance",
"latest": rate, "ema_30": rate, "ema_90": rate, "ema_180": rate,
"forecast": rate, "trend": "flat", "spot_only": spot_only,
"scale": 1, "band_low": rate, "band_high": rate, "days": 200,
}
return _fx
@pytest.fixture
def patched(monkeypatch):
monkeypatch.setattr("app.main.get_percentage_diff", fake_diffs)
monkeypatch.setattr("app.main.fetch_fx_ema", fake_fx())
def test_cross_country_end_to_end_numbers(patched):
# Explicit weights so the arithmetic is deterministic:
# savings 20% → rent 25% of the remaining 80% = 20% of income, other = 60%.
r = client.post("/compare", data={
"country1": "Malaysia", "city1": "Kuala Lumpur",
"country2": "Singapore", "city2": "Singapore",
"net_home": "16000", "net_new": "24000",
"savings_ratio": "20", "rent_share": "25",
})
assert r.status_code == 200
t = r.text
# Home buckets (MYR): rent 0.2*16000=3,200 · other 0.6*16000=9,600 · savings 3,200
assert "9,600" in t
# Destination rent (SGD): 0.2 * (16000*0.30) * (1+4.094) = 4,890
assert "4,890" in t
# Destination savings (SGD): 24000 - 4890.24 - 6753.6 = 12,356
assert "12,356" in t
assert "breakdown-table" in t
def test_same_country_skips_fx_and_currency_section(patched):
r = client.post("/compare", data={
"country1": "Malaysia", "city1": "Kuala Lumpur",
"country2": "Malaysia", "city2": "Penang",
"net_home": "10000", "net_new": "12000",
})
assert r.status_code == 200
assert "FX forecast" not in r.text # no FX card same-country
assert "Monthly savings estimate" in r.text
def test_savings_target_endpoint_roundtrip(patched):
# Ask for +25% savings; the derived salary should reproduce ~+25%.
r = client.post("/compare", data={
"country1": "Malaysia", "city1": "Kuala Lumpur",
"country2": "Singapore", "city2": "Singapore",
"net_home": "16000", "increment_pct": "25",
"savings_ratio": "20", "rent_share": "25",
})
assert r.status_code == 200
assert "To save" in r.text and "destination salary" in r.text
assert "+25.0% savings" in r.text
def test_spot_only_pair_still_computes(monkeypatch):
monkeypatch.setattr("app.main.get_percentage_diff", fake_diffs)
monkeypatch.setattr("app.main.fetch_fx_ema", fake_fx(rate=0.000252, spot_only=True))
r = client.post("/compare", data={
"country1": "Indonesia", "city1": "Jakarta",
"country2": "Saudi arabia", "city2": "Riyadh",
"net_home": "25000000", "net_new": "7500",
})
assert r.status_code == 200
assert "FX rate · spot" in r.text # spot card, no forecast
assert "Monthly savings estimate" in r.text # savings still computed
def test_fx_failure_keeps_cost_indices(monkeypatch):
from app.fx import FxUnavailable
async def boom(*a, **k):
raise FxUnavailable("rate limited")
monkeypatch.setattr("app.main.get_percentage_diff", fake_diffs)
monkeypatch.setattr("app.main.fetch_fx_ema", boom)
r = client.post("/compare", data={
"country1": "Malaysia", "city1": "Kuala Lumpur",
"country2": "Singapore", "city2": "Singapore",
"net_home": "16000", "net_new": "24000",
})
assert r.status_code == 200
assert "Cost of living indices" in r.text
assert "could not be retrieved" in r.text
assert "Monthly savings estimate" not in r.text