feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
34
backend/tests/conftest.py
Normal file
34
backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import pytest
|
||||
|
||||
from app import create_app
|
||||
from app.extensions import db as _db
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app():
|
||||
"""Create application for testing."""
|
||||
flask_app = create_app("testing")
|
||||
flask_app.config["TESTING"] = True
|
||||
|
||||
with flask_app.app_context():
|
||||
_db.create_all()
|
||||
yield flask_app
|
||||
_db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db(app):
|
||||
"""Provide a clean database for each test."""
|
||||
with app.app_context():
|
||||
yield _db
|
||||
_db.session.rollback()
|
||||
# Clean all tables
|
||||
for table in reversed(_db.metadata.sorted_tables):
|
||||
_db.session.execute(table.delete())
|
||||
_db.session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(app, db):
|
||||
"""Create test client."""
|
||||
return app.test_client()
|
||||
46
backend/tests/test_homepage.py
Normal file
46
backend/tests/test_homepage.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.models.homepage import HomepageConfig
|
||||
from app.schemas.homepage import HomepageConfigIn
|
||||
|
||||
|
||||
def test_get_homepage_config_returns_200(client, db):
|
||||
"""GET /api/v1/homepage-config returns 200 with required fields when config exists."""
|
||||
config = HomepageConfig(
|
||||
hero_headline="Teste Headline",
|
||||
hero_subheadline="Subtítulo de teste",
|
||||
hero_cta_label="Ver Imóveis",
|
||||
hero_cta_url="/imoveis",
|
||||
featured_properties_limit=6,
|
||||
)
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/homepage-config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.get_json()
|
||||
assert data["hero_headline"] == "Teste Headline"
|
||||
assert data["hero_subheadline"] == "Subtítulo de teste"
|
||||
assert data["hero_cta_label"] == "Ver Imóveis"
|
||||
assert data["hero_cta_url"] == "/imoveis"
|
||||
assert data["featured_properties_limit"] == 6
|
||||
|
||||
|
||||
def test_get_homepage_config_returns_404_when_empty(client, db):
|
||||
"""GET /api/v1/homepage-config returns 404 when no config record exists."""
|
||||
response = client.get("/api/v1/homepage-config")
|
||||
assert response.status_code == 404
|
||||
|
||||
data = response.get_json()
|
||||
assert "error" in data
|
||||
|
||||
|
||||
def test_homepage_config_in_rejects_empty_headline():
|
||||
"""HomepageConfigIn raises ValidationError when hero_headline is empty."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
HomepageConfigIn(hero_headline="")
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("hero_headline" in str(e) for e in errors)
|
||||
229
backend/tests/test_properties.py
Normal file
229
backend/tests/test_properties.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import uuid
|
||||
|
||||
from app.models.property import Property, PropertyPhoto
|
||||
|
||||
|
||||
def _make_property(slug: str, featured: bool = True, **kwargs) -> Property:
|
||||
defaults = dict(
|
||||
id=uuid.uuid4(),
|
||||
title=f"Imóvel {slug}",
|
||||
slug=slug,
|
||||
address="Rua Teste, 1",
|
||||
price="500000.00",
|
||||
type="venda",
|
||||
bedrooms=2,
|
||||
bathrooms=1,
|
||||
area_m2=80,
|
||||
is_featured=featured,
|
||||
is_active=True,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Property(**defaults)
|
||||
|
||||
|
||||
def test_get_properties_featured_returns_200_with_array(client, db):
|
||||
"""GET /api/v1/properties?featured=true returns 200 with an array."""
|
||||
prop = _make_property("imovel-featured-1")
|
||||
db.session.add(prop)
|
||||
db.session.flush()
|
||||
|
||||
photo = PropertyPhoto(
|
||||
property_id=prop.id,
|
||||
url="https://picsum.photos/seed/test/800/450",
|
||||
alt_text="Foto de teste",
|
||||
display_order=0,
|
||||
)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?featured=true")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.get_json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
|
||||
|
||||
def test_get_properties_response_contains_required_fields(client, db):
|
||||
"""Response contains all required fields: id, title, slug, price, type, etc."""
|
||||
prop = _make_property("imovel-fields-check")
|
||||
db.session.add(prop)
|
||||
db.session.flush()
|
||||
|
||||
photo = PropertyPhoto(
|
||||
property_id=prop.id,
|
||||
url="https://picsum.photos/seed/fields/800/450",
|
||||
alt_text="Foto de campos",
|
||||
display_order=0,
|
||||
)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?featured=true")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.get_json()
|
||||
assert len(data) >= 1
|
||||
|
||||
item = data[0]
|
||||
required_fields = [
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"price",
|
||||
"type",
|
||||
"bedrooms",
|
||||
"bathrooms",
|
||||
"area_m2",
|
||||
"photos",
|
||||
]
|
||||
for field in required_fields:
|
||||
assert field in item, f"Missing field: {field}"
|
||||
|
||||
assert isinstance(item["photos"], list)
|
||||
|
||||
|
||||
def test_get_properties_featured_empty_returns_empty_array(client, db):
|
||||
"""GET /api/v1/properties?featured=true returns [] when no featured properties exist."""
|
||||
# Add a non-featured active property
|
||||
prop = _make_property("imovel-not-featured", featured=False)
|
||||
db.session.add(prop)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?featured=true")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.get_json()
|
||||
assert isinstance(data, list)
|
||||
assert data == []
|
||||
|
||||
|
||||
# ── Text search (q param) ─────────────────────────────────────────────────────
|
||||
|
||||
def test_q_matches_title(client, db):
|
||||
"""?q=<word> returns properties whose title contains that word."""
|
||||
p1 = _make_property("casa-praia", title="Casa na Praia", price="400000.00")
|
||||
p2 = _make_property("apto-centro", title="Apartamento Centro", price="300000.00")
|
||||
db.session.add_all([p1, p2])
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?q=praia")
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
ids = [item["id"] for item in data["items"]]
|
||||
assert str(p1.id) in ids
|
||||
assert str(p2.id) not in ids
|
||||
|
||||
|
||||
def test_q_is_case_insensitive(client, db):
|
||||
"""?q search is case-insensitive."""
|
||||
p = _make_property("casa-jardins", title="Casa nos Jardins", price="600000.00")
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
for term in ("jardins", "JARDINS", "Jardins"):
|
||||
response = client.get(f"/api/v1/properties?q={term}")
|
||||
assert response.status_code == 200
|
||||
ids = [item["id"] for item in response.get_json()["items"]]
|
||||
assert str(p.id) in ids, f"Expected to find property with q={term!r}"
|
||||
|
||||
|
||||
def test_q_matches_address(client, db):
|
||||
"""?q search matches against the address field."""
|
||||
p = _make_property("rua-flores", address="Rua das Flores, 42", title="Imóvel Especial", price="350000.00")
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?q=Flores")
|
||||
assert response.status_code == 200
|
||||
ids = [item["id"] for item in response.get_json()["items"]]
|
||||
assert str(p.id) in ids
|
||||
|
||||
|
||||
def test_q_no_match_returns_empty(client, db):
|
||||
"""?q with a term that matches nothing returns total=0 and empty items."""
|
||||
p = _make_property("imovel-comum", title="Imóvel Comum", price="200000.00")
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?q=xyznaomatch999")
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data["total"] == 0
|
||||
assert data["items"] == []
|
||||
|
||||
|
||||
def test_q_truncated_to_200_chars(client, db):
|
||||
"""?q longer than 200 chars is accepted without error (truncated server-side)."""
|
||||
long_q = "a" * 300
|
||||
response = client.get(f"/api/v1/properties?q={long_q}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# ── Sort param ────────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_props_for_sort(db):
|
||||
"""Helper: add three properties with different price/area for sort tests."""
|
||||
props = [
|
||||
_make_property("sort-cheap", title="Barato", price="100000.00", area_m2=50),
|
||||
_make_property("sort-mid", title="Médio", price="300000.00", area_m2=80),
|
||||
_make_property("sort-exp", title="Caro", price="600000.00", area_m2=120),
|
||||
]
|
||||
db.session.add_all(props)
|
||||
db.session.commit()
|
||||
return props
|
||||
|
||||
|
||||
def test_sort_price_asc(client, db):
|
||||
"""?sort=price_asc returns properties ordered from cheapest to most expensive."""
|
||||
_add_props_for_sort(db)
|
||||
response = client.get("/api/v1/properties?sort=price_asc")
|
||||
assert response.status_code == 200
|
||||
items = response.get_json()["items"]
|
||||
prices = [float(item["price"]) for item in items]
|
||||
assert prices == sorted(prices), "Items should be sorted price ascending"
|
||||
|
||||
|
||||
def test_sort_price_desc(client, db):
|
||||
"""?sort=price_desc returns properties from most to least expensive."""
|
||||
_add_props_for_sort(db)
|
||||
response = client.get("/api/v1/properties?sort=price_desc")
|
||||
assert response.status_code == 200
|
||||
items = response.get_json()["items"]
|
||||
prices = [float(item["price"]) for item in items]
|
||||
assert prices == sorted(prices, reverse=True), "Items should be sorted price descending"
|
||||
|
||||
|
||||
def test_sort_area_desc(client, db):
|
||||
"""?sort=area_desc returns properties from largest to smallest area."""
|
||||
_add_props_for_sort(db)
|
||||
response = client.get("/api/v1/properties?sort=area_desc")
|
||||
assert response.status_code == 200
|
||||
items = response.get_json()["items"]
|
||||
areas = [item["area_m2"] for item in items]
|
||||
assert areas == sorted(areas, reverse=True), "Items should be sorted area descending"
|
||||
|
||||
|
||||
def test_sort_unknown_value_falls_back_gracefully(client, db):
|
||||
"""?sort=<invalid> returns 200 (falls back to default sort)."""
|
||||
p = _make_property("sort-fallback", price="200000.00")
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?sort=invalid_value")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_paginated_response_shape(client, db):
|
||||
"""Paginated listing endpoint returns items, total, page, per_page, pages."""
|
||||
p = _make_property("paginated-shape", price="250000.00")
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
response = client.get("/api/v1/properties?per_page=5")
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
for key in ("items", "total", "page", "per_page", "pages"):
|
||||
assert key in data, f"Missing key in paginated response: {key}"
|
||||
assert isinstance(data["items"], list)
|
||||
assert data["total"] >= 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue