feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

34
backend/tests/conftest.py Normal file
View 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()

View 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)

View 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