major restructure

This commit is contained in:
buenosairesam
2026-01-20 05:31:26 -03:00
parent 27b32deba4
commit e4052374db
328 changed files with 1018 additions and 10018 deletions

View File

@@ -0,0 +1 @@
# Contract tests for mascotas app endpoints

View File

@@ -0,0 +1,53 @@
"""
Contract Tests: Coverage Check API
Endpoint: /mascotas/api/v1/coverage/check/
App: mascotas
Used to check if a location has veterinary coverage before proceeding with turnero.
"""
from ..base import ContractTestCase
from ..endpoints import Endpoints
class TestCoverageCheck(ContractTestCase):
"""GET /mascotas/api/v1/coverage/check/"""
def test_with_coordinates_returns_200(self):
"""Coverage check should accept lat/lng parameters"""
response = self.get(Endpoints.COVERAGE_CHECK, params={
"lat": -34.6037,
"lng": -58.3816,
})
self.assert_status(response, 200)
def test_returns_coverage_boolean(self):
"""Coverage check should return coverage boolean"""
response = self.get(Endpoints.COVERAGE_CHECK, params={
"lat": -34.6037,
"lng": -58.3816,
})
self.assert_status(response, 200)
self.assert_has_fields(response.data, "coverage")
self.assertIsInstance(response.data["coverage"], bool)
def test_returns_vet_count(self):
"""Coverage check should return number of available vets"""
response = self.get(Endpoints.COVERAGE_CHECK, params={
"lat": -34.6037,
"lng": -58.3816,
})
self.assert_status(response, 200)
self.assert_has_fields(response.data, "vet_count")
self.assertIsInstance(response.data["vet_count"], int)
def test_without_coordinates_fails(self):
"""Coverage check without coordinates should fail"""
response = self.get(Endpoints.COVERAGE_CHECK)
# Should return 400 or similar error
self.assertIn(response.status_code, [400, 422])

View File

@@ -0,0 +1,171 @@
"""
Contract Tests: Pet Owners API
Endpoint: /mascotas/api/v1/pet-owners/
App: mascotas
Related Tickets:
- VET-536: Paso 0 - Test creación del petowner invitado
- VET-535: Establecer y definir test para las apis vinculadas al procesos de solicitar turno general
Context: In the turnero general flow (guest booking), a "guest" pet owner is created
with a mock email (e.g., invitado-1759415377297@example.com). This user is fundamental
for subsequent steps as it provides the address used to filter available services.
TBD: PetOwnerViewSet needs pagination - currently loads all records on list().
See mascotas/views/api/v1/views/petowner_views.py:72
Using email filter in tests to avoid loading 14k+ records.
"""
import time
from ..base import ContractTestCase
from ..endpoints import Endpoints
from ..helpers import sample_pet_owner
class TestPetOwnerCreate(ContractTestCase):
"""POST /mascotas/api/v1/pet-owners/
VET-536: Tests for guest petowner creation (Step 0 of turnero flow)
"""
def test_create_returns_201(self):
"""
Creating a pet owner returns 201 with the created resource.
Request (from production turnero):
POST /mascotas/api/v1/pet-owners/
{
"first_name": "Juan",
"last_name": "Pérez",
"email": "invitado-1733929847293@example.com",
"phone": "1155667788",
"address": "Av. Santa Fe 1234, Buenos Aires",
"geo_latitude": -34.5955,
"geo_longitude": -58.4166
}
Response (201):
{
"id": 12345,
"first_name": "Juan",
"last_name": "Pérez",
"email": "invitado-1733929847293@example.com",
"phone": "1155667788",
"address": "Av. Santa Fe 1234, Buenos Aires",
"geo_latitude": -34.5955,
"geo_longitude": -58.4166,
"pets": [],
"created_at": "2024-12-11T15:30:47.293Z"
}
"""
data = sample_pet_owner()
response = self.post(Endpoints.PET_OWNERS, data)
self.assert_status(response, 201)
self.assert_has_fields(response.data, "id", "email", "first_name", "last_name")
self.assertEqual(response.data["email"], data["email"])
def test_requires_email(self):
"""
Pet owner creation requires email (current behavior).
Note: The turnero guest flow uses a mock email created by frontend
(e.g., invitado-1759415377297@example.com). The API always requires email.
This test ensures the contract enforcement - no petowner without email.
"""
data = {
"address": "Av. Corrientes 1234",
"first_name": "Invitado",
"last_name": str(int(time.time())),
}
response = self.post(Endpoints.PET_OWNERS, data)
self.assert_status(response, 400)
def test_duplicate_email_returns_existing(self):
"""
Creating pet owner with existing email returns the existing record.
Note: API has upsert behavior - returns 200 with existing record,
not 400 error. This allows frontend to "create or get" in one call.
Important for guest flow - if user refreshes/retries, we don't create duplicates.
"""
data = sample_pet_owner()
first_response = self.post(Endpoints.PET_OWNERS, data)
first_id = first_response.data["id"]
response = self.post(Endpoints.PET_OWNERS, data) # Same email
# Returns 200 with existing record (upsert behavior)
self.assert_status(response, 200)
self.assertEqual(response.data["id"], first_id)
def test_address_and_geolocation_persisted(self):
"""
Pet owner address and geolocation coordinates are persisted correctly.
The address is critical for the turnero flow - it's used to filter available
services by location. Geolocation (lat/lng) may be obtained from Google Maps API.
"""
data = sample_pet_owner()
response = self.post(Endpoints.PET_OWNERS, data)
self.assert_status(response, 201)
self.assert_has_fields(response.data, "address", "geo_latitude", "geo_longitude")
self.assertEqual(response.data["address"], data["address"])
# Verify geolocation fields are numeric (not null/empty)
self.assertIsNotNone(response.data.get("geo_latitude"))
self.assertIsNotNone(response.data.get("geo_longitude"))
class TestPetOwnerRetrieve(ContractTestCase):
"""GET /mascotas/api/v1/pet-owners/{id}/"""
def test_get_by_id_returns_200(self):
"""GET pet owner by ID returns owner details"""
# Create owner first
data = sample_pet_owner()
create_response = self.post(Endpoints.PET_OWNERS, data)
owner_id = create_response.data["id"]
response = self.get(Endpoints.PET_OWNER_DETAIL.format(id=owner_id))
self.assert_status(response, 200)
self.assertEqual(response.data["id"], owner_id)
self.assert_has_fields(response.data, "id", "first_name", "last_name", "address", "pets")
def test_nonexistent_returns_404(self):
"""GET non-existent owner returns 404"""
response = self.get(Endpoints.PET_OWNER_DETAIL.format(id=999999))
self.assert_status(response, 404)
class TestPetOwnerList(ContractTestCase):
"""GET /mascotas/api/v1/pet-owners/"""
def test_list_with_email_filter_returns_200(self):
"""GET pet owners filtered by email returns 200"""
# Filter by email to avoid loading 14k+ records (no pagination on this endpoint)
response = self.get(Endpoints.PET_OWNERS, params={"email": "nonexistent@test.com"})
self.assert_status(response, 200)
def test_list_filter_by_email_works(self):
"""Can filter pet owners by email"""
# Create a pet owner first
data = sample_pet_owner()
self.post(Endpoints.PET_OWNERS, data)
# Filter by that email
response = self.get(Endpoints.PET_OWNERS, params={"email": data["email"]})
self.assert_status(response, 200)
# Should find exactly one
results = response.data if isinstance(response.data, list) else response.data.get("results", [])
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["email"], data["email"])

View File

@@ -0,0 +1,171 @@
"""
Contract Tests: Pets API
Endpoint: /mascotas/api/v1/pets/
App: mascotas
Related Tickets:
- VET-537: Paso 1 - Test creación de la mascota vinculada al petowner invitado
- VET-535: Establecer y definir test para las apis vinculadas al procesos de solicitar turno general
Context: In the turnero general flow (Step 1), a pet is created and linked to the guest
pet owner. The pet data (type, name, neutered status) combined with the owner's address
is used to filter available services and veterinarians.
"""
from ..base import ContractTestCase
from ..endpoints import Endpoints
from ..helpers import (
sample_pet_owner,
unique_email,
SAMPLE_CAT,
SAMPLE_DOG,
SAMPLE_NEUTERED_CAT,
)
class TestPetCreate(ContractTestCase):
"""POST /mascotas/api/v1/pets/
VET-537: Tests for pet creation linked to guest petowner (Step 1 of turnero flow)
"""
def _create_owner(self):
"""Helper to create a pet owner"""
data = sample_pet_owner(unique_email("pet_owner"))
response = self.post(Endpoints.PET_OWNERS, data)
return response.data["id"]
def test_create_cat_returns_201(self):
"""
Creating a cat returns 201 with pet_type CAT.
Request (from production turnero):
POST /mascotas/api/v1/pets/
{
"name": "Luna",
"pet_type": "CAT",
"is_neutered": false,
"owner": 12345
}
Response (201):
{
"id": 67890,
"name": "Luna",
"pet_type": "CAT",
"is_neutered": false,
"owner": 12345,
"breed": null,
"birth_date": null,
"created_at": "2024-12-11T15:31:15.123Z"
}
"""
owner_id = self._create_owner()
data = {**SAMPLE_CAT, "owner": owner_id}
response = self.post(Endpoints.PETS, data)
self.assert_status(response, 201)
self.assert_has_fields(response.data, "id", "name", "pet_type", "owner")
self.assertEqual(response.data["pet_type"], "CAT")
self.assertEqual(response.data["name"], "TestCat")
def test_create_dog_returns_201(self):
"""
Creating a dog returns 201 with pet_type DOG.
Validates that both major pet types (CAT/DOG) are supported in the contract.
"""
owner_id = self._create_owner()
data = {**SAMPLE_DOG, "owner": owner_id}
response = self.post(Endpoints.PETS, data)
self.assert_status(response, 201)
self.assertEqual(response.data["pet_type"], "DOG")
def test_neutered_status_persisted(self):
"""
Neutered status is persisted correctly.
This is important business data that may affect service recommendations
or veterinarian assignments.
"""
owner_id = self._create_owner()
data = {**SAMPLE_NEUTERED_CAT, "owner": owner_id}
response = self.post(Endpoints.PETS, data)
self.assert_status(response, 201)
self.assertTrue(response.data["is_neutered"])
def test_requires_owner(self):
"""
Pet creation without owner should fail.
Enforces the required link between pet and petowner - critical for the
turnero flow where pets must be associated with the guest user.
"""
data = SAMPLE_CAT.copy()
response = self.post(Endpoints.PETS, data)
self.assert_status(response, 400)
def test_invalid_pet_type_rejected(self):
"""
Invalid pet_type should be rejected.
Currently only CAT and DOG are supported. This test ensures the contract
validates pet types correctly.
"""
owner_id = self._create_owner()
data = {
"name": "InvalidPet",
"pet_type": "HAMSTER",
"owner": owner_id,
}
response = self.post(Endpoints.PETS, data)
self.assert_status(response, 400)
class TestPetRetrieve(ContractTestCase):
"""GET /mascotas/api/v1/pets/{id}/"""
def _create_owner_with_pet(self):
"""Helper to create owner and pet"""
owner_data = sample_pet_owner(unique_email("pet_owner"))
owner_response = self.post(Endpoints.PET_OWNERS, owner_data)
owner_id = owner_response.data["id"]
pet_data = {**SAMPLE_CAT, "owner": owner_id}
pet_response = self.post(Endpoints.PETS, pet_data)
return pet_response.data["id"]
def test_get_by_id_returns_200(self):
"""GET pet by ID returns pet details"""
pet_id = self._create_owner_with_pet()
response = self.get(Endpoints.PET_DETAIL.format(id=pet_id))
self.assert_status(response, 200)
self.assertEqual(response.data["id"], pet_id)
def test_nonexistent_returns_404(self):
"""GET non-existent pet returns 404"""
response = self.get(Endpoints.PET_DETAIL.format(id=999999))
self.assert_status(response, 404)
class TestPetList(ContractTestCase):
"""GET /mascotas/api/v1/pets/"""
def test_list_returns_200(self):
"""GET pets list returns 200 (with pagination)"""
response = self.get(Endpoints.PETS, params={"page_size": 1})
self.assert_status(response, 200)