test: cover propertyfinder matching rules
Some checks failed
CI / hygiene (push) Successful in 2s
Build and Deploy / build-and-deploy (push) Successful in 35s
CI / go (push) Successful in 46s
CI / python (push) Failing after 2s

This commit is contained in:
Grendgi
2026-06-17 17:12:49 +03:00
parent f73c9fba5f
commit cb8e290d8f
3 changed files with 216 additions and 0 deletions

View File

@@ -32,4 +32,6 @@ jobs:
needs: hygiene needs: hygiene
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: python3 -m pip install -r requirements.txt
- run: python3 -m compileall app - run: python3 -m compileall app
- run: python3 -m unittest discover -s tests

67
internal/pf/store_test.go Normal file
View File

@@ -0,0 +1,67 @@
package pf
import (
"strings"
"testing"
)
func strPtr(v string) *string {
return &v
}
func int64Ptr(v int64) *int64 {
return &v
}
func float64Ptr(v float64) *float64 {
return &v
}
func validProjectPayload() ProjectPayload {
return ProjectPayload{
Title: "Full Park View",
DealType: "sale",
OurPrice: float64Ptr(2500000),
DLDPermit: strPtr("7140504127"),
Building: strPtr("Harbour Gate Tower 2"),
Bedrooms: int64Ptr(2),
SizeSqft: float64Ptr(1081),
OurURL: strPtr(
"https://www.propertyfinder.ae/en/plp/buy/apartment-for-sale-dubai-dubai-creek-harbour-the-lagoons-harbour-gate-harbour-gate-tower-2-86176216.html",
),
}
}
func TestValidateProjectRequiredAcceptsConcretePropertyFinderListingURL(t *testing.T) {
payload := validProjectPayload()
if err := validateProjectRequired(payload); err != nil {
t.Fatalf("validateProjectRequired() returned unexpected error: %v", err)
}
}
func TestValidateProjectRequiredRejectsSearchPageAsOurURL(t *testing.T) {
payload := validProjectPayload()
payload.OurURL = strPtr("https://www.propertyfinder.ae/en/search?c=1&l=12345")
err := validateProjectRequired(payload)
if err == nil {
t.Fatal("validateProjectRequired() accepted a search page as our_url")
}
if !strings.Contains(err.Error(), "concrete PropertyFinder listing URL") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidateProjectRequiredRejectsListingLikeURLWithoutID(t *testing.T) {
payload := validProjectPayload()
payload.OurURL = strPtr("https://www.propertyfinder.ae/en/plp/buy/apartment-for-sale-dubai-dubai-creek-harbour.html")
err := validateProjectRequired(payload)
if err == nil {
t.Fatal("validateProjectRequired() accepted a listing-like URL without listing id")
}
if !strings.Contains(err.Error(), "concrete PropertyFinder listing URL") {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -0,0 +1,147 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db import Base
from app.models import CompetitorListing, DealType, Employee, ListingStatus, Project, Source
from app.scrapers.base import ScrapedListing
from app.scrapers.propertyfinder import PropertyFinderScraper
from app.services import monitor
PF_OWN_URL = (
"https://www.propertyfinder.ae/en/plp/buy/apartment-for-sale-dubai-dubai-creek-harbour-"
"the-lagoons-harbour-gate-harbour-gate-tower-2-86176216.html"
)
PF_COMPETITOR_URL = (
"https://www.propertyfinder.ae/en/plp/buy/apartment-for-sale-dubai-dubai-creek-harbour-"
"the-lagoons-harbour-gate-harbour-gate-tower-2-86170000.html"
)
def _listing(external_id: str, permit: str | None, url: str = PF_COMPETITOR_URL) -> ScrapedListing:
return ScrapedListing(
source="propertyfinder",
external_id=external_id,
url=url,
title=f"Listing {external_id}",
price=2_500_000,
currency="AED",
permit_number=permit,
agent_name="Agent",
agency_name="Agency",
is_active=True,
)
class MonitoringRulesTest(unittest.TestCase):
def setUp(self) -> None:
engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
self.Session = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
self.db = self.Session()
owner = Employee(name="Agent", portal_user_id="agent-1", tg_chat_id="100")
self.db.add(owner)
self.db.flush()
self.project = Project(
title="Full Park View",
deal_type=DealType.SALE,
our_price=2_500_000,
dld_permit="7140504127",
building="Harbour Gate Tower 2",
bedrooms=2,
size_sqft=1081,
our_url=PF_OWN_URL,
owner_id=owner.id,
)
self.db.add(self.project)
self.db.commit()
def tearDown(self) -> None:
self.db.close()
def test_propertyfinder_rejects_search_pages(self) -> None:
scraper = PropertyFinderScraper()
self.assertFalse(scraper.is_listing_url("https://www.propertyfinder.ae/en/search?c=1&l=12345"))
self.assertIsNone(scraper.fetch_listing("https://www.propertyfinder.ae/en/search?c=1&l=12345"))
@patch.object(monitor.PF, "get_permit", side_effect=["7140504127"])
@patch.object(
monitor.PF,
"search_similar",
return_value=[
_listing("86176216", None, url=PF_OWN_URL),
_listing("86170000", None, url=PF_COMPETITOR_URL),
],
)
def test_suggest_similar_excludes_own_listing(self, _search, _permit) -> None:
suggestions = monitor.suggest_similar(self.project, our_permit="7140504127")
self.assertEqual(["86170000"], [item.external_id for item in suggestions["propertyfinder"]])
@patch.object(
monitor,
"suggest_similar",
return_value={
"propertyfinder": [
_listing("86170000", "7140504127"),
_listing("86170001", "DIFFERENT"),
],
"bayut": [],
},
)
def test_sync_permit_competitors_adds_only_exact_permit_matches(self, _suggest) -> None:
changes, suggestions, permit = monitor.sync_permit_competitors(self.db, self.project)
listings = self.db.query(CompetitorListing).order_by(CompetitorListing.external_id).all()
self.assertEqual("7140504127", permit)
self.assertEqual(1, len(listings))
self.assertEqual("86170000", listings[0].external_id)
self.assertTrue(listings[0].auto_discovered)
self.assertEqual(["86170001"], [item.external_id for item in suggestions["propertyfinder"]])
self.assertEqual(1, len(changes))
@patch.object(monitor, "suggest_similar", return_value={"propertyfinder": [], "bayut": []})
def test_auto_permit_listing_is_removed_only_after_three_misses(self, _suggest) -> None:
listing = CompetitorListing(
project_id=self.project.id,
source=Source.PROPERTYFINDER,
external_id="86170000",
url=PF_COMPETITOR_URL,
title="Competitor",
permit_number="7140504127",
auto_discovered=True,
permit_missing_checks=0,
current_price=2_500_000,
currency="AED",
status=ListingStatus.ACTIVE,
)
self.db.add(listing)
self.db.commit()
changes, _, _ = monitor.sync_permit_competitors(self.db, self.project)
self.db.flush()
self.assertEqual([], changes)
self.assertEqual(1, self.db.query(CompetitorListing).count())
self.assertEqual(1, listing.permit_missing_checks)
changes, _, _ = monitor.sync_permit_competitors(self.db, self.project)
self.db.flush()
self.assertEqual([], changes)
self.assertEqual(1, self.db.query(CompetitorListing).count())
self.assertEqual(2, listing.permit_missing_checks)
changes, _, _ = monitor.sync_permit_competitors(self.db, self.project)
self.db.flush()
self.assertEqual(1, len(changes))
self.assertEqual(0, self.db.query(CompetitorListing).count())
if __name__ == "__main__":
unittest.main()