This commit is contained in:
Ole
2026-05-16 06:54:17 +00:00
commit 1399f61c1a
44 changed files with 6746 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Test fixtures and utilities."""
+236
View File
@@ -0,0 +1,236 @@
"""Fixture data for testing without hitting live APIs."""
# noqa: E501
SAMPLE_FINN_SEARCH_HTML = """
<!DOCTYPE html>
<html lang="no">
<head><title>FINN.no - Leiligheter til salgs</title></head>
<body>
<div class="listings">
<article class="listing-card" data-id="462400360">
<a href="https://www.finn.no/realestate/homes/ad.html?finnkode=462400360" class="listing-link">
<div class="title">Flott 3-roms i Ferner</div>
<div class="meta">
<span class="area">77 m²</span>
<span class="price">7 200 991 kr</span>
<span class="price-per-sqm">93 500 kr/m²</span>
</div>
</a>
<div class="details">
<span class="bedrooms">3</span>
<span class="location">Grünerløkka, Oslo</span>
<span class="common-costs">3 500 kr/mnd</span>
</div>
</article>
<article class="listing-card" data-id="460784945">
<a href="https://www.finn.no/realestate/homes/ad.html?finnkode=460784945" class="listing-link">
<div class="title">Leilighet med potensial - må renoveres</div>
<div class="meta">
<span class="area">65 m²</span>
<span class="price">6 500 000 kr</span>
<span class="price-per-sqm">100 000 kr/m²</span>
</div>
</a>
<div class="details">
<span class="bedrooms">2</span>
<span class="location">Sagene, Oslo</span>
<span class="common-costs">2 800 kr/mnd</span>
</div>
</article>
</div>
</body>
</html>
"""
# noqa: E501
SAMPLE_FINN_SEARCH_HTML_NEW = """
<!DOCTYPE html>
<html lang="no">
<head><title>FINN.no - Leiligheter til salgs</title></head>
<body>
<div class="listings">
<article class="relative isolate sf-search-ad card card--cardShadow">
<div class="col-span-2 p-16 grid sm:grid-cols-2">
<h2 class="h4 mb-0 col-span-2 mt-12 sm:mt-24 sf-realestate-heading">IDYLLISKE ILADALEN - Lekker 3-roms loftsleilighet fra 2016 | Privat, solrik takterrasse | Peis | Gulvareal på 77kvm | Sentralt, men rolig</h2>
<a href="https://www.finn.no/realestate/homes/ad.html?finnkode=462880791" class="sf-search-ad-link s-text!">IDYLLISKE ILADALEN - Lekker 3-roms loftsleilighet fra 2016 | Privat, solrik takterrasse | Peis | Gulvareal på 77kvm | Sentralt, men rolig</a>
<div class="mt-4 sf-line-clamp-2 sm:order-first sm:text-right sm:mt-0 sm:ml-16 sf-realestate-location">Lofotgata 4B, Oslo</div>
<div class="col-span-2 mt-16 flex justify-between sm:mt-4 sm:block space-x-12 font-bold">62 m² 6 750 000 kr</div>
<div class="col-span-2 sm:flex sm:items-baseline sm:justify-between">Totalpris: 7 253 377 kr ∙ Fellesutg.: 7 067 kr ∙ Andel ∙ Leilighet ∙ 2 soverom</div>
</div>
</article>
</div>
</body>
</html>
"""
SAMPLE_FINN_LISTING_HTML = """
<!DOCTYPE html>
<html lang="no">
<head><title>Flott 3-roms i Ferner - FINN.no</title></head>
<body>
<div class="listing-details">
<div class="heading">
<h1>Flott 3-roms i Ferner</h1>
<div class="price">Totalpris: 7 200 991 kr</div>
</div>
<div class="properties">
<dl>
<dt>Adresse</dt>
<dd>Fernerveien 42, 0554 Oslo</dd>
<dt>Område</dt>
<dd>Grünerløkka</dd>
<dt>Postnummer</dt>
<dd>0554</dd>
<dt>Eierform</dt>
<dd>Eierbolig</dd>
<dt>Eiendomstype</dt>
<dd>Leilighet</dd>
<dt>Prisantydning</dt>
<dd>7 200 000 kr</dd>
<dt>Totalpris</dt>
<dd>7 200 991 kr</dd>
<dt>Fellesgjeld</dt>
<dd>0 kr</dd>
<dt>Felles utgifter</dt>
<dd>3 500 kr/mnd</dd>
<dt>Boligareal</dt>
<dd>77 m²</dd>
<dt>Rom</dt>
<dd>4</dd>
<dt>Soverom</dt>
<dd>3</dd>
<dt>Etasje</dt>
<dd>4. etasje</dd>
<dt>Byggeår</dt>
<dd>2005</dd>
<dt>Energimerking</dt>
<dd>C</dd>
<dt>Oppvarming</dt>
<dd>Fjernvarme</dd>
<dt>Balkonger/terrasser</dt>
<dd>Ja, balkonger</dd>
<dt>Heis</dt>
<dd>Ja</dd>
<dt>Parkering/garasje</dt>
<dd>Privat parkering</dd>
</dl>
</div>
<div class="description">
<h2>Beskrivelse</h2>
<p>Flott beliggenhet med fin utsikt over Oslo. Moderne kjøkken og bad.</p>
<p>Klar til visning!</p>
</div>
<div class="broker">
<div class="broker-info">
<span class="broker-name">Meglerhuset AS</span>
<span class="broker-contact">Telefon: 21 00 00 00</span>
</div>
</div>
</div>
</body>
</html>
"""
SAMPLE_FINN_LISTING_HTML_NEW = """
<!DOCTYPE html>
<html lang="no">
<head><title>Romslig 5-roms i 5.etasje med heisadkomst</title></head>
<body>
<div data-testid="object-details">
<h1>Romslig 5-roms i 5.etasje med heisadkomst | 2 hybler | 4 balkonger | Ingen dokavgift!</h1>
<span data-testid="object-address">Hegdehaugsveien 3, 0352 Oslo</span>
<span data-testid="local-area-name">Homansbyen</span>
<section data-testid="pricing-details">
<div data-testid="pricing-incicative-price">Prisantydning10 900 000 kr</div>
<div data-testid="pricing-total-price"><dt>Totalpris</dt><dd>10 986 901 kr</dd></div>
<div data-testid="pricing-joint-debt"><dt>Fellesgjeld</dt><dd>76 911 kr</dd></div>
<div data-testid="pricing-common-monthly-cost"><dt>Felleskost/mnd.</dt><dd>12 011 kr</dd></div>
</section>
<section data-testid="key-info">
<div data-testid="info-property-type">BoligtypeLeilighet</div>
<div data-testid="info-ownership-type">EieformAndel</div>
<div data-testid="info-bedrooms">Soverom2</div>
<div data-testid="info-rooms">Rom5</div>
<div data-testid="info-construction-year">Byggeår1938</div>
<div data-testid="info-usable-i-area">Internt bruksareal124 m² (BRA-i)</div>
</section>
<section data-testid="object-facilities">FasiliteterBalkong/TerrasseParkettHeis</section>
<section data-testid="om boligen">
<h2>Om boligen</h2>
<p>Her bor du med kort vei til daglige behov og offentlig transport.</p>
</section>
</div>
</body>
</html>
"""
SAMPLE_EIENDOM_UNIT_JSON = {
"units": [
{
"unitCode": "c-gxw-xmyum-s2a",
"address": "Fernerveien 42, 0554 Oslo",
"municipality": "Oslo",
"lat": 59.9287,
"lon": 10.7803,
"propertyType": "APARTMENT",
"floor": 4,
"rooms": 4,
"constructionYear": 2005,
"usableArea": 77,
"estimatedSellingPrice": 7650000,
"estimatedSellingPriceLower": 6900000,
"estimatedSellingPriceUpper": 8400000,
"listingPrice": 7200000,
"listingSquareMeterPrice": 93500,
"commonCosts": 3500,
"daysOnMarket": 12,
"saleStatus": "FOR_SALE",
"marketPlacementScore": "ABOVE_AVERAGE",
"similarUnitCount": 12,
"averageSquareMeterPrice": 98000,
}
]
}
SAMPLE_EIENDOM_SIMILAR_UNITS_JSON = {
"units": [
{
"unitCode": "c-recent-1",
"address": "Birketveien 10, 0554 Oslo",
"lat": 59.9290,
"lon": 10.7810,
"propertyType": "APARTMENT",
"floor": 3,
"rooms": 3,
"constructionYear": 2004,
"usableArea": 75,
"listingPrice": 7100000,
"sellingPrice": 7050000,
"sharedDebt": 0,
"commonCosts": 3400,
"squareMeterPrice": 94000,
"daysOnMarket": 18,
"saleStatus": "SOLD",
"finalizedAt": "2024-05-01",
},
{
"unitCode": "c-recent-2",
"address": "Sommers gate 5, 0554 Oslo",
"lat": 59.9280,
"lon": 10.7820,
"propertyType": "APARTMENT",
"floor": 2,
"rooms": 4,
"constructionYear": 2006,
"usableArea": 80,
"listingPrice": 7400000,
"sellingPrice": 7350000,
"sharedDebt": 0,
"commonCosts": 3600,
"squareMeterPrice": 91875,
"daysOnMarket": 22,
"saleStatus": "SOLD",
"finalizedAt": "2024-04-28",
},
]
}
+45
View File
@@ -0,0 +1,45 @@
from finn_eiendom.ad import scrape_ad
from tests.fixtures import SAMPLE_FINN_LISTING_HTML, SAMPLE_FINN_LISTING_HTML_NEW
def test_scrape_ad():
ad = scrape_ad(
SAMPLE_FINN_LISTING_HTML,
url="https://www.finn.no/realestate/homes/ad.html?finnkode=462400360",
)
assert ad.finnkode == "462400360"
assert ad.title == "Flott 3-roms i Ferner"
assert ad.address == "Fernerveien 42, 0554 Oslo"
assert ad.area_m2 == 77
assert ad.asking_price == 7200000
assert ad.total_price == 7200991
assert ad.common_costs == 3500
assert ad.rooms == 4
assert ad.bedrooms == 3
assert ad.floor == "4. etasje"
assert ad.construction_year == 2005
assert ad.energy_rating == "C"
assert ad.heating == "Fjernvarme"
assert "Moderne kjøkken" in ad.listing_description
assert ad.broker_company == "Meglerhuset AS"
def test_scrape_ad_new_structure():
ad = scrape_ad(
SAMPLE_FINN_LISTING_HTML_NEW,
url="https://www.finn.no/realestate/homes/ad.html?finnkode=455978973",
)
assert ad.finnkode == "455978973"
assert ad.title.startswith("Romslig 5-roms i 5.etasje")
assert ad.address == "Hegdehaugsveien 3, 0352 Oslo"
assert ad.property_type == "Leilighet"
assert ad.ownership_type == "Andel"
assert ad.asking_price == 10900000
assert ad.total_price == 10986901
assert ad.common_costs == 12011
assert ad.area_m2 == 124
assert ad.rooms == 5
assert ad.bedrooms == 2
assert ad.construction_year == 1938
assert ad.floor == "5. etasje"
assert "kort vei" in ad.listing_description.lower()
+71
View File
@@ -0,0 +1,71 @@
import tempfile
from datetime import UTC, datetime, timedelta
from pathlib import Path
from finn_eiendom.cache import (
get_eiendom_unit,
get_finn_ad,
get_search_page,
get_similar_units,
init_db,
save_eiendom_unit,
save_finn_ad,
save_search_page,
save_similar_units,
)
from finn_eiendom.models import EiendomUnit, FinnAd, SimilarUnit
def test_cache_roundtrip():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "cache.sqlite"
conn = init_db(str(db_path))
ad = FinnAd(finnkode="1234", url="https://example.com", title="Test")
save_finn_ad(conn, ad)
loaded_ad = get_finn_ad(conn, "1234")
assert loaded_ad is not None
assert loaded_ad.finnkode == "1234"
assert loaded_ad.url == "https://example.com"
unit = EiendomUnit(unit_code="abc", address="Oslo")
save_eiendom_unit(conn, unit)
loaded_unit = get_eiendom_unit(conn, "abc")
assert loaded_unit is not None
assert loaded_unit.address == "Oslo"
comps = [
SimilarUnit(unit_code="x1"),
SimilarUnit(unit_code="x2"),
]
save_similar_units(conn, "abc", "RECENTLY_SOLD", comps)
loaded_comps = get_similar_units(conn, "abc", "RECENTLY_SOLD")
assert len(loaded_comps) == 2
assert loaded_comps[0].unit_code == "x1"
def test_search_page_cache_roundtrip():
with tempfile.TemporaryDirectory() as tmpdir:
conn = init_db(str(Path(tmpdir) / "cache.sqlite"))
html = "<html><body>search page</body></html>"
url = "https://www.finn.no/realestate/homes/search.html"
save_search_page(conn, url, html, ttl_minutes=5)
loaded_html = get_search_page(conn, url)
assert loaded_html == html
def test_finn_ad_cache_ttl_expiration():
with tempfile.TemporaryDirectory() as tmpdir:
conn = init_db(str(Path(tmpdir) / "cache.sqlite"))
ad = FinnAd(
finnkode="1234",
url="https://example.com",
title="Test",
detail_fetched_at=datetime.now(UTC) - timedelta(hours=2),
)
save_finn_ad(conn, ad)
expired_ad = get_finn_ad(conn, "1234", ttl_hours=1)
assert expired_ad is None
+44
View File
@@ -0,0 +1,44 @@
from finn_eiendom.eiendom_no import (
build_unit_vector,
decode_unit_vector,
parse_eiendom_unit_json,
parse_similar_units_json,
resolve_unit_from_finn_url,
)
from tests.fixtures import (
SAMPLE_EIENDOM_SIMILAR_UNITS_JSON,
SAMPLE_EIENDOM_UNIT_JSON,
)
def test_parse_eiendom_unit_json():
unit = parse_eiendom_unit_json(SAMPLE_EIENDOM_UNIT_JSON["units"][0])
assert unit.unit_code == "c-gxw-xmyum-s2a"
assert unit.address == "Fernerveien 42, 0554 Oslo"
assert unit.estimated_selling_price == 7650000
assert unit.listing_sqm_price == 93500
def test_unit_vector_roundtrip():
unit = parse_eiendom_unit_json(SAMPLE_EIENDOM_UNIT_JSON["units"][0])
vector = build_unit_vector(unit)
decoded = decode_unit_vector(vector)
assert decoded["ptype"] == "APARTMENT"
assert decoded["area"] == 77
assert decoded["price"] == 7200000
assert isinstance(decoded, dict)
assert decoded["lon"] == unit.lng
def test_parse_similar_units_json():
comps = parse_similar_units_json(SAMPLE_EIENDOM_SIMILAR_UNITS_JSON)
assert len(comps) == 2
assert comps[0].unit_code == "c-recent-1"
assert comps[1].selling_price == 7350000
def test_resolve_unit_from_finn_url():
unit_code = resolve_unit_from_finn_url(
"https://www.finn.no/realestate/homes/ad.html?finnkode=462400360"
)
assert unit_code == "462400360"
+83
View File
@@ -0,0 +1,83 @@
"""Tests for HTTP client retry logic."""
import httpx
import pytest
import respx
from finn_eiendom.http import HTTPClient
@pytest.mark.asyncio
async def test_get_retries_on_500():
"""Test that HTTPClient retries on 500 errors and succeeds on second attempt."""
client = HTTPClient(request_delay_seconds=0.0, retries=2)
with respx.mock:
route = respx.get("https://example.com/api")
route.side_effect = [
httpx.Response(500, text="Server Error"),
httpx.Response(200, text="Success"),
]
response = await client.get("https://example.com/api")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_raises_on_404():
"""Test that HTTPClient raises on 4xx errors immediately."""
client = HTTPClient(request_delay_seconds=0.0, retries=2)
with respx.mock:
respx.get("https://example.com/api").mock(return_value=httpx.Response(404))
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await client.get("https://example.com/api")
assert exc_info.value.response.status_code == 404
@pytest.mark.asyncio
async def test_get_retries_on_502_bad_gateway():
"""Test that HTTPClient retries on 502 Bad Gateway."""
client = HTTPClient(request_delay_seconds=0.0, retries=2)
with respx.mock:
route = respx.get("https://example.com/api")
route.side_effect = [
httpx.Response(502, text="Bad Gateway"),
httpx.Response(200, text="Success"),
]
response = await client.get("https://example.com/api")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_post_retries_on_503():
"""Test that HTTPClient retries POST on 503 Service Unavailable."""
client = HTTPClient(request_delay_seconds=0.0, retries=2)
with respx.mock:
route = respx.post("https://example.com/api")
route.side_effect = [
httpx.Response(503, text="Service Unavailable"),
httpx.Response(201, json={"success": True}),
]
response = await client.post("https://example.com/api", json={"test": "data"})
assert response.status_code == 201
@pytest.mark.asyncio
async def test_get_eventually_fails_on_persistent_500():
"""Test that HTTPClient gives up after max retries."""
client = HTTPClient(request_delay_seconds=0.0, retries=1)
with respx.mock:
respx.get("https://example.com/api").mock(return_value=httpx.Response(500))
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await client.get("https://example.com/api")
assert exc_info.value.response.status_code == 500
+69
View File
@@ -0,0 +1,69 @@
"""Tests for the MCP server tools."""
import json
from finn_eiendom.mcp_server import (
finn_decode_unit_vector,
mcp,
)
def test_mcp_server_has_correct_tools():
"""Assert that the MCP server has all expected tools."""
import asyncio
async def check_tools():
tools = await mcp.list_tools()
tool_names = {tool.name for tool in tools}
expected_tools = {
"finn_analyze_search",
"finn_get_ad",
"finn_resolve_eiendom_unit",
"finn_get_eiendom_unit",
"finn_get_similar_units",
"finn_build_unit_vector",
"finn_decode_unit_vector",
}
assert expected_tools.issubset(tool_names), f"Missing tools: {expected_tools - tool_names}"
asyncio.run(check_tools())
def test_finn_decode_unit_vector_returns_json():
"""Test that finn_decode_unit_vector returns valid JSON with expected keys."""
from unittest.mock import patch
test_vector = {
"lon": 10.7,
"lat": 59.9,
"ptype": "APARTMENT",
"floor": 3,
"rooms": 3,
"built": 2000,
"area": 80,
"price": 5000000,
}
with patch("finn_eiendom.mcp_server.decode_unit_vector", return_value=test_vector):
result = finn_decode_unit_vector("dGVzdA==")
data = json.loads(result)
assert "lon" in data
assert "lat" in data
assert "ptype" in data
assert data["lat"] == 59.9
assert data["lon"] == 10.7
def test_finn_decode_unit_vector_error_handling():
"""Test that finn_decode_unit_vector handles errors gracefully."""
from unittest.mock import patch
with patch(
"finn_eiendom.mcp_server.decode_unit_vector", side_effect=Exception("decode failed")
):
result = finn_decode_unit_vector("invalid")
data = json.loads(result)
assert data.get("error") is True
assert "message" in data
+45
View File
@@ -0,0 +1,45 @@
from finn_eiendom.parser import (
clean_text,
extract_finnkode_from_url,
normalize_area,
normalize_finnkode,
normalize_number,
normalize_price,
)
def test_normalize_price():
assert normalize_price("7 200 991 kr") == 7200991
assert normalize_price("1 234") == 1234
assert normalize_price(None) is None
def test_normalize_area():
assert normalize_area("77 m²") == 77
assert normalize_area("100,5 m²") == 100
assert normalize_area("") is None
def test_normalize_number():
assert normalize_number("3 500 kr/mnd") == 3500
assert normalize_number("7,2") == 7
assert normalize_number("1.234") == 1234
assert normalize_number(None) is None
def test_normalize_finnkode():
assert normalize_finnkode(" 462400360 ") == "462400360"
assert normalize_finnkode(None) is None
def test_extract_finnkode_from_url():
assert (
extract_finnkode_from_url("https://www.finn.no/realestate/homes/ad.html?finnkode=462400360")
== "462400360"
)
assert extract_finnkode_from_url("https://www.finn.no/realestate/homes/ad.html") is None
def test_clean_text():
assert clean_text(" Hello world \n") == "Hello world"
assert clean_text(None) is None
+22
View File
@@ -0,0 +1,22 @@
from finn_eiendom.models import EiendomUnit, FinnAd
from finn_eiendom.scoring import classify_ad, score_ad
def test_score_ad_and_classify():
ad = FinnAd(
finnkode="462400360",
url="https://www.finn.no/realestate/homes/ad.html?finnkode=462400360",
title="Flott 3-roms i Ferner",
)
unit = EiendomUnit(
unit_code="c-gxw-xmyum-s2a",
estimated_selling_price=7650000,
listing_price=7200000,
property_type="APARTMENT",
usable_area=77,
rooms=4,
)
scores = score_ad(ad, unit, [])
assert scores["market_position"] >= 0
categories = classify_ad(scores)
assert isinstance(categories, list)
+38
View File
@@ -0,0 +1,38 @@
from finn_eiendom.search import extract_ad_links, extract_search_cards
from tests.fixtures import SAMPLE_FINN_SEARCH_HTML, SAMPLE_FINN_SEARCH_HTML_NEW
def test_extract_search_cards():
cards = extract_search_cards(SAMPLE_FINN_SEARCH_HTML)
assert len(cards) == 2
assert cards[0].finnkode == "462400360"
assert cards[0].url.endswith("finnkode=462400360")
assert cards[0].area_m2 == 77
assert cards[0].total_price == 7200991
assert cards[0].common_costs == 3500
assert cards[1].bedrooms == 2
def test_extract_search_cards_new_format():
cards = extract_search_cards(SAMPLE_FINN_SEARCH_HTML_NEW)
assert len(cards) == 1
assert cards[0].finnkode == "462880791"
assert cards[0].url.endswith("finnkode=462880791")
assert cards[0].address == "Lofotgata 4B, Oslo"
assert cards[0].area_m2 == 62
assert cards[0].total_price == 7253377
assert cards[0].common_costs == 7067
assert cards[0].bedrooms == 2
def test_extract_ad_links():
links = extract_ad_links(SAMPLE_FINN_SEARCH_HTML)
assert len(links) == 2
assert "finnkode=462400360" in links[0]
assert "finnkode=460784945" in links[1]
def test_extract_ad_links_new_format():
links = extract_ad_links(SAMPLE_FINN_SEARCH_HTML_NEW)
assert len(links) == 1
assert "finnkode=462880791" in links[0]
+97
View File
@@ -0,0 +1,97 @@
"""Tests for the service layer (cache-aware fetching)."""
from unittest.mock import patch
import pytest
from finn_eiendom.models import EiendomUnit, FinnAd
from finn_eiendom.service import get_or_fetch_ad, get_or_fetch_eiendom_unit
@pytest.mark.asyncio
async def test_get_or_fetch_ad_uses_cache():
"""Test that get_or_fetch_ad returns cached ad without fetching."""
mock_ad = FinnAd(finnkode="123", url="http://example.com")
with (
patch("finn_eiendom.service.init_db"),
patch("finn_eiendom.service.get_finn_ad", return_value=mock_ad) as mock_get,
patch("finn_eiendom.service.fetch_ad_details") as mock_fetch,
):
result = await get_or_fetch_ad("123")
assert result.finnkode == "123"
mock_get.assert_called_once()
mock_fetch.assert_not_called()
@pytest.mark.asyncio
async def test_get_or_fetch_ad_fetches_when_cache_miss():
"""Test that get_or_fetch_ad fetches when cache is empty."""
mock_ad = FinnAd(finnkode="123", url="http://example.com")
with (
patch("finn_eiendom.service.init_db"),
patch("finn_eiendom.service.get_finn_ad", return_value=None),
patch("finn_eiendom.service.fetch_ad_details", return_value=mock_ad) as mock_fetch,
patch("finn_eiendom.service.save_finn_ad") as mock_save,
):
result = await get_or_fetch_ad("123")
assert result.finnkode == "123"
mock_fetch.assert_called_once_with("123")
mock_save.assert_called_once()
@pytest.mark.asyncio
async def test_get_or_fetch_ad_force_refresh():
"""Test that force_refresh=True bypasses cache."""
mock_ad = FinnAd(finnkode="123", url="http://example.com")
with (
patch("finn_eiendom.service.init_db"),
patch("finn_eiendom.service.get_finn_ad", return_value=mock_ad) as mock_get,
patch("finn_eiendom.service.fetch_ad_details", return_value=mock_ad) as mock_fetch,
patch("finn_eiendom.service.save_finn_ad") as mock_save,
):
result = await get_or_fetch_ad("123", force_refresh=True)
assert result.finnkode == "123"
mock_get.assert_not_called()
mock_fetch.assert_called_once_with("123")
mock_save.assert_called_once()
@pytest.mark.asyncio
async def test_get_or_fetch_eiendom_unit_uses_cache():
"""Test that get_or_fetch_eiendom_unit returns cached unit without fetching."""
mock_unit = EiendomUnit(unit_code="test-code")
with (
patch("finn_eiendom.service.init_db"),
patch("finn_eiendom.service.get_cached_eiendom_unit", return_value=mock_unit) as mock_get,
patch("finn_eiendom.service.get_unit") as mock_fetch,
):
result = await get_or_fetch_eiendom_unit("test-code")
assert result.unit_code == "test-code"
mock_get.assert_called_once()
mock_fetch.assert_not_called()
@pytest.mark.asyncio
async def test_get_or_fetch_eiendom_unit_fetches_when_cache_miss():
"""Test that get_or_fetch_eiendom_unit fetches when cache is empty."""
mock_unit = EiendomUnit(unit_code="test-code")
with (
patch("finn_eiendom.service.init_db"),
patch("finn_eiendom.service.get_cached_eiendom_unit", return_value=None),
patch("finn_eiendom.service.get_unit", return_value=mock_unit) as mock_fetch,
patch("finn_eiendom.service.save_eiendom_unit") as mock_save,
):
result = await get_or_fetch_eiendom_unit("test-code")
assert result.unit_code == "test-code"
mock_fetch.assert_called_once_with("test-code")
mock_save.assert_called_once()