spr migrated books, and tester
This commit is contained in:
1
station/tools/tester/tests/example/__init__.py
Normal file
1
station/tools/tester/tests/example/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Example tests - used when no room-specific tests are configured
|
||||
36
station/tools/tester/tests/example/test_health.py
Normal file
36
station/tools/tester/tests/example/test_health.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Example health check test.
|
||||
|
||||
This is a fallback test that works without room-specific configuration.
|
||||
Replace with room tests via cfg/<room>/tester/tests/
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHealth:
|
||||
"""Basic health check tests."""
|
||||
|
||||
@pytest.fixture
|
||||
def base_url(self):
|
||||
"""Base URL for the API under test."""
|
||||
import os
|
||||
|
||||
return os.getenv("TEST_BASE_URL", "http://localhost:8000")
|
||||
|
||||
def test_health_endpoint(self, base_url):
|
||||
"""Test that /health endpoint responds."""
|
||||
try:
|
||||
response = httpx.get(f"{base_url}/health", timeout=5)
|
||||
assert response.status_code == 200
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("API not running - set TEST_BASE_URL or start the service")
|
||||
|
||||
def test_root_endpoint(self, base_url):
|
||||
"""Test that root endpoint responds."""
|
||||
try:
|
||||
response = httpx.get(base_url, timeout=5)
|
||||
assert response.status_code in [200, 301, 302, 307, 308]
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("API not running - set TEST_BASE_URL or start the service")
|
||||
@@ -1 +0,0 @@
|
||||
# Contract tests for mascotas app endpoints
|
||||
@@ -1,53 +0,0 @@
|
||||
"""
|
||||
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])
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
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"])
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
# Contract tests for productos app endpoints
|
||||
@@ -1,149 +0,0 @@
|
||||
"""
|
||||
Contract Tests: Cart API
|
||||
|
||||
Endpoint: /productos/api/v1/cart/
|
||||
App: productos
|
||||
|
||||
Related Tickets:
|
||||
- VET-538: Test creación de cart vinculado al petowner
|
||||
- VET-535: Establecer y definir test para las apis vinculadas al procesos de solicitar turno general
|
||||
|
||||
Context: In the turnero general flow (Step 2), a cart is created for the guest petowner.
|
||||
The cart holds selected services and calculates price summary (subtotals, discounts, total).
|
||||
|
||||
TBD: CartViewSet needs pagination/filtering - list endpoint hangs on large dataset.
|
||||
See productos/api/v1/viewsets.py:93
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
from ..helpers import sample_pet_owner, unique_email
|
||||
|
||||
|
||||
class TestCartCreate(ContractTestCase):
|
||||
"""POST /productos/api/v1/cart/
|
||||
|
||||
VET-538: Tests for cart creation linked to petowner (Step 2 of turnero flow)
|
||||
"""
|
||||
|
||||
def _create_petowner(self):
|
||||
"""Helper to create a pet owner"""
|
||||
data = sample_pet_owner(unique_email("cart_owner"))
|
||||
response = self.post(Endpoints.PET_OWNERS, data)
|
||||
return response.data["id"]
|
||||
|
||||
def test_create_cart_for_petowner(self):
|
||||
"""
|
||||
Creating a cart returns 201 and links to petowner.
|
||||
|
||||
Request (from production turnero):
|
||||
POST /productos/api/v1/cart/
|
||||
{
|
||||
"petowner": 12345,
|
||||
"services": []
|
||||
}
|
||||
|
||||
Response (201):
|
||||
{
|
||||
"id": 789,
|
||||
"petowner": 12345,
|
||||
"veterinarian": null,
|
||||
"items": [],
|
||||
"resume": [
|
||||
{"concept": "SUBTOTAL", "amount": "0.00", "order": 1},
|
||||
{"concept": "COSTO_SERVICIO", "amount": "0.00", "order": 2},
|
||||
{"concept": "DESCUENTO", "amount": "0.00", "order": 3},
|
||||
{"concept": "TOTAL", "amount": "0.00", "order": 4},
|
||||
{"concept": "ADELANTO", "amount": "0.00", "order": 5}
|
||||
],
|
||||
"extra_details": "",
|
||||
"pets": [],
|
||||
"pet_reasons": []
|
||||
}
|
||||
"""
|
||||
owner_id = self._create_petowner()
|
||||
data = {
|
||||
"petowner": owner_id,
|
||||
"services": []
|
||||
}
|
||||
|
||||
response = self.post(Endpoints.CART, data)
|
||||
|
||||
self.assert_status(response, 201)
|
||||
self.assert_has_fields(response.data, "id", "petowner", "items")
|
||||
self.assertEqual(response.data["petowner"], owner_id)
|
||||
|
||||
def test_cart_has_price_summary_fields(self):
|
||||
"""
|
||||
Cart response includes price summary fields.
|
||||
|
||||
These fields are critical for turnero flow - user needs to see:
|
||||
- resume: array with price breakdown (SUBTOTAL, DESCUENTO, TOTAL, etc)
|
||||
- items: cart items with individual pricing
|
||||
"""
|
||||
owner_id = self._create_petowner()
|
||||
data = {"petowner": owner_id, "services": []}
|
||||
|
||||
response = self.post(Endpoints.CART, data)
|
||||
|
||||
self.assert_status(response, 201)
|
||||
# Price fields should exist (may be 0 for empty cart)
|
||||
self.assert_has_fields(response.data, "resume", "items")
|
||||
|
||||
def test_empty_cart_has_zero_totals(self):
|
||||
"""
|
||||
Empty cart (no services) has zero price totals.
|
||||
|
||||
Validates initial state before services are added.
|
||||
"""
|
||||
owner_id = self._create_petowner()
|
||||
data = {"petowner": owner_id, "services": []}
|
||||
|
||||
response = self.post(Endpoints.CART, data)
|
||||
|
||||
self.assert_status(response, 201)
|
||||
# Empty cart should have resume with zero amounts
|
||||
self.assertIn("resume", response.data)
|
||||
# Find TOTAL concept in resume
|
||||
total_item = next((item for item in response.data["resume"] if item["concept"] == "TOTAL"), None)
|
||||
self.assertIsNotNone(total_item)
|
||||
self.assertEqual(total_item["amount"], "0.00")
|
||||
|
||||
|
||||
class TestCartRetrieve(ContractTestCase):
|
||||
"""GET /productos/api/v1/cart/{id}/"""
|
||||
|
||||
def _create_petowner_with_cart(self):
|
||||
"""Helper to create petowner and cart"""
|
||||
owner_data = sample_pet_owner(unique_email("cart_owner"))
|
||||
owner_response = self.post(Endpoints.PET_OWNERS, owner_data)
|
||||
owner_id = owner_response.data["id"]
|
||||
|
||||
cart_data = {"petowner": owner_id, "services": []}
|
||||
cart_response = self.post(Endpoints.CART, cart_data)
|
||||
return cart_response.data["id"]
|
||||
|
||||
def test_get_cart_by_id_returns_200(self):
|
||||
"""GET cart by ID returns cart details"""
|
||||
cart_id = self._create_petowner_with_cart()
|
||||
|
||||
response = self.get(Endpoints.CART_DETAIL.format(id=cart_id))
|
||||
|
||||
self.assert_status(response, 200)
|
||||
self.assertEqual(response.data["id"], cart_id)
|
||||
|
||||
def test_detail_returns_404_for_nonexistent(self):
|
||||
"""GET /cart/{id}/ returns 404 for non-existent cart"""
|
||||
response = self.get(Endpoints.CART_DETAIL.format(id=999999))
|
||||
self.assert_status(response, 404)
|
||||
|
||||
|
||||
class TestCartList(ContractTestCase):
|
||||
"""GET /productos/api/v1/cart/"""
|
||||
|
||||
@pytest.mark.skip(reason="TBD: Cart list hangs - needs pagination/filtering. Checking if dead code.")
|
||||
def test_list_returns_200(self):
|
||||
"""GET /cart/ returns 200"""
|
||||
response = self.get(Endpoints.CART)
|
||||
self.assert_status(response, 200)
|
||||
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Contract Tests: Categories API
|
||||
|
||||
Endpoint: /productos/api/v1/categories/
|
||||
App: productos
|
||||
|
||||
Returns service categories filtered by location availability.
|
||||
Categories without available services in location should be hidden.
|
||||
"""
|
||||
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
|
||||
|
||||
class TestCategoriesList(ContractTestCase):
|
||||
"""GET /productos/api/v1/categories/"""
|
||||
|
||||
def test_list_returns_200(self):
|
||||
"""GET categories returns 200"""
|
||||
response = self.get(Endpoints.CATEGORIES, params={"page_size": 10})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
|
||||
def test_returns_list(self):
|
||||
"""GET categories returns a list"""
|
||||
response = self.get(Endpoints.CATEGORIES, params={"page_size": 10})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
data = response.data
|
||||
# Handle paginated or non-paginated response
|
||||
categories = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(categories, list)
|
||||
|
||||
def test_categories_have_required_fields(self):
|
||||
"""
|
||||
Each category should have id, name, and description.
|
||||
|
||||
Request (from production turnero):
|
||||
GET /productos/api/v1/categories/
|
||||
|
||||
Response (200):
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Consulta General",
|
||||
"description": "Consultas veterinarias generales"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Vacunación",
|
||||
"description": "Servicios de vacunación"
|
||||
}
|
||||
]
|
||||
"""
|
||||
response = self.get(Endpoints.CATEGORIES, params={"page_size": 10})
|
||||
|
||||
data = response.data
|
||||
categories = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
|
||||
if len(categories) > 0:
|
||||
category = categories[0]
|
||||
self.assert_has_fields(category, "id", "name", "description")
|
||||
|
||||
def test_only_active_categories_returned(self):
|
||||
"""
|
||||
Only active categories are returned in the list.
|
||||
|
||||
Business rule: Inactive categories should not be visible to users.
|
||||
"""
|
||||
response = self.get(Endpoints.CATEGORIES, params={"page_size": 50})
|
||||
|
||||
data = response.data
|
||||
categories = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
|
||||
# All categories should be active (no 'active': False in response)
|
||||
# This is enforced at queryset level in CategoryViewSet
|
||||
self.assertIsInstance(categories, list)
|
||||
|
||||
|
||||
class TestCategoryRetrieve(ContractTestCase):
|
||||
"""GET /productos/api/v1/categories/{id}/"""
|
||||
|
||||
def test_get_category_by_id_returns_200(self):
|
||||
"""
|
||||
GET category by ID returns category details.
|
||||
|
||||
First fetch list to get a valid ID, then retrieve that category.
|
||||
"""
|
||||
# Get first category
|
||||
list_response = self.get(Endpoints.CATEGORIES, params={"page_size": 1})
|
||||
if list_response.status_code != 200:
|
||||
self.skipTest("No categories available for testing")
|
||||
|
||||
data = list_response.data
|
||||
categories = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
|
||||
if len(categories) == 0:
|
||||
self.skipTest("No categories available for testing")
|
||||
|
||||
category_id = categories[0]["id"]
|
||||
|
||||
# Test detail endpoint
|
||||
response = self.get(f"{Endpoints.CATEGORIES}{category_id}/")
|
||||
|
||||
self.assert_status(response, 200)
|
||||
self.assertEqual(response.data["id"], category_id)
|
||||
|
||||
def test_nonexistent_category_returns_404(self):
|
||||
"""GET non-existent category returns 404"""
|
||||
response = self.get(f"{Endpoints.CATEGORIES}999999/")
|
||||
|
||||
self.assert_status(response, 404)
|
||||
@@ -1,122 +0,0 @@
|
||||
"""
|
||||
Contract Tests: Services API
|
||||
|
||||
Endpoint: /productos/api/v1/services/
|
||||
App: productos
|
||||
|
||||
Returns available veterinary services filtered by pet type and location.
|
||||
Critical for vet assignment automation.
|
||||
"""
|
||||
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
from ..helpers import sample_pet_owner, unique_email, SAMPLE_CAT, SAMPLE_DOG
|
||||
|
||||
|
||||
class TestServicesList(ContractTestCase):
|
||||
"""GET /productos/api/v1/services/"""
|
||||
|
||||
def test_list_returns_200(self):
|
||||
"""GET services returns 200"""
|
||||
response = self.get(Endpoints.SERVICES, params={"page_size": 10})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
|
||||
def test_returns_list(self):
|
||||
"""GET services returns a list"""
|
||||
response = self.get(Endpoints.SERVICES, params={"page_size": 10})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
data = response.data
|
||||
# Handle paginated or non-paginated response
|
||||
services = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(services, list)
|
||||
|
||||
def test_services_have_required_fields(self):
|
||||
"""Each service should have id and name"""
|
||||
response = self.get(Endpoints.SERVICES, params={"page_size": 10})
|
||||
|
||||
data = response.data
|
||||
services = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
|
||||
if len(services) > 0:
|
||||
service = services[0]
|
||||
self.assert_has_fields(service, "id", "name")
|
||||
|
||||
def test_accepts_pet_id_filter(self):
|
||||
"""Services endpoint accepts pet_id parameter"""
|
||||
response = self.get(Endpoints.SERVICES, params={"pet_id": 1})
|
||||
|
||||
# Should not error (even if pet doesn't exist, endpoint should handle gracefully)
|
||||
self.assertIn(response.status_code, [200, 404])
|
||||
|
||||
|
||||
class TestServicesFiltering(ContractTestCase):
|
||||
"""GET /productos/api/v1/services/ with filters"""
|
||||
|
||||
def _create_owner_with_cat(self):
|
||||
"""Helper to create owner and cat"""
|
||||
owner_data = sample_pet_owner(unique_email("service_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 _create_owner_with_dog(self):
|
||||
"""Helper to create owner and dog"""
|
||||
owner_data = sample_pet_owner(unique_email("service_owner"))
|
||||
owner_response = self.post(Endpoints.PET_OWNERS, owner_data)
|
||||
owner_id = owner_response.data["id"]
|
||||
|
||||
pet_data = {**SAMPLE_DOG, "owner": owner_id}
|
||||
pet_response = self.post(Endpoints.PETS, pet_data)
|
||||
return pet_response.data["id"]
|
||||
|
||||
def test_filter_services_by_cat(self):
|
||||
"""
|
||||
Services filtered by cat pet_id returns appropriate services.
|
||||
|
||||
Request (from production turnero):
|
||||
GET /productos/api/v1/services/?pet_id=123
|
||||
|
||||
Response structure validates services available for CAT type.
|
||||
"""
|
||||
cat_id = self._create_owner_with_cat()
|
||||
response = self.get(Endpoints.SERVICES, params={"pet_id": cat_id, "page_size": 10})
|
||||
|
||||
# Should return services or handle gracefully
|
||||
self.assertIn(response.status_code, [200, 404])
|
||||
if response.status_code == 200:
|
||||
data = response.data
|
||||
services = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(services, list)
|
||||
|
||||
def test_filter_services_by_dog(self):
|
||||
"""
|
||||
Services filtered by dog pet_id returns appropriate services.
|
||||
|
||||
Different pet types may have different service availability.
|
||||
"""
|
||||
dog_id = self._create_owner_with_dog()
|
||||
response = self.get(Endpoints.SERVICES, params={"pet_id": dog_id, "page_size": 10})
|
||||
|
||||
self.assertIn(response.status_code, [200, 404])
|
||||
if response.status_code == 200:
|
||||
data = response.data
|
||||
services = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(services, list)
|
||||
|
||||
def test_services_without_pet_returns_all(self):
|
||||
"""
|
||||
Services without pet filter returns all available services.
|
||||
|
||||
Used for initial service browsing before pet selection.
|
||||
"""
|
||||
response = self.get(Endpoints.SERVICES, params={"page_size": 10})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
data = response.data
|
||||
services = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(services, list)
|
||||
@@ -1 +0,0 @@
|
||||
# Contract tests for solicitudes app endpoints
|
||||
@@ -1,56 +0,0 @@
|
||||
"""
|
||||
Contract Tests: Service Requests API
|
||||
|
||||
Endpoint: /solicitudes/service-requests/
|
||||
App: solicitudes
|
||||
|
||||
Creates and manages service requests (appointment bookings).
|
||||
"""
|
||||
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
|
||||
|
||||
class TestServiceRequestList(ContractTestCase):
|
||||
"""GET /solicitudes/service-requests/"""
|
||||
|
||||
def test_list_returns_200(self):
|
||||
"""GET should return list of service requests (with pagination)"""
|
||||
response = self.get(Endpoints.SERVICE_REQUESTS, params={"page_size": 1})
|
||||
|
||||
self.assert_status(response, 200)
|
||||
|
||||
def test_returns_list(self):
|
||||
"""GET should return a list (possibly paginated)"""
|
||||
response = self.get(Endpoints.SERVICE_REQUESTS, params={"page_size": 10})
|
||||
|
||||
data = response.data
|
||||
requests_list = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
self.assertIsInstance(requests_list, list)
|
||||
|
||||
|
||||
class TestServiceRequestFields(ContractTestCase):
|
||||
"""Field validation for service requests"""
|
||||
|
||||
def test_has_state_field(self):
|
||||
"""Service requests should have a state/status field"""
|
||||
response = self.get(Endpoints.SERVICE_REQUESTS, params={"page_size": 1})
|
||||
|
||||
data = response.data
|
||||
requests_list = data["results"] if isinstance(data, dict) and "results" in data else data
|
||||
|
||||
if len(requests_list) > 0:
|
||||
req = requests_list[0]
|
||||
has_state = "state" in req or "status" in req
|
||||
self.assertTrue(has_state, "Service request should have state/status field")
|
||||
|
||||
|
||||
class TestServiceRequestCreate(ContractTestCase):
|
||||
"""POST /solicitudes/service-requests/"""
|
||||
|
||||
def test_create_requires_fields(self):
|
||||
"""Creating service request with empty data should fail"""
|
||||
response = self.post(Endpoints.SERVICE_REQUESTS, {})
|
||||
|
||||
# Should return 400 with validation errors
|
||||
self.assert_status(response, 400)
|
||||
@@ -1 +0,0 @@
|
||||
# Contract tests for frontend workflows (compositions of endpoint tests)
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
Workflow Test: General Turnero Flow
|
||||
|
||||
This is a COMPOSITION test that validates the full turnero flow
|
||||
by calling endpoints in sequence. Use this to ensure the flow works
|
||||
end-to-end, but individual endpoint behavior is tested in app folders.
|
||||
|
||||
Flow:
|
||||
1. Check coverage at address
|
||||
2. Create pet owner (guest with mock email)
|
||||
3. Create pet for owner
|
||||
4. Get available services for pet
|
||||
5. Create service request
|
||||
|
||||
Frontend route: /turnos/
|
||||
User type: Guest (invitado)
|
||||
"""
|
||||
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
from ..helpers import sample_pet_owner, unique_email, SAMPLE_CAT
|
||||
|
||||
|
||||
class TestTurneroGeneralFlow(ContractTestCase):
|
||||
"""
|
||||
End-to-end flow test for general turnero.
|
||||
|
||||
Note: This tests the SEQUENCE of calls, not individual endpoint behavior.
|
||||
Individual endpoint tests are in mascotas/, productos/, solicitudes/.
|
||||
"""
|
||||
|
||||
def test_full_flow_sequence(self):
|
||||
"""
|
||||
Complete turnero flow should work end-to-end.
|
||||
|
||||
This test validates that a guest user can complete the full
|
||||
appointment booking flow.
|
||||
"""
|
||||
# Step 0: Check coverage at address
|
||||
coverage_response = self.get(Endpoints.COVERAGE_CHECK, params={
|
||||
"lat": -34.6037,
|
||||
"lng": -58.3816,
|
||||
})
|
||||
self.assert_status(coverage_response, 200)
|
||||
|
||||
# Step 1: Create pet owner (frontend creates mock email for guest)
|
||||
mock_email = unique_email("invitado")
|
||||
owner_data = sample_pet_owner(mock_email)
|
||||
owner_response = self.post(Endpoints.PET_OWNERS, owner_data)
|
||||
self.assert_status(owner_response, 201)
|
||||
owner_id = owner_response.data["id"]
|
||||
|
||||
# Step 2: Create pet for owner
|
||||
pet_data = {**SAMPLE_CAT, "owner": owner_id}
|
||||
pet_response = self.post(Endpoints.PETS, pet_data)
|
||||
self.assert_status(pet_response, 201)
|
||||
pet_id = pet_response.data["id"]
|
||||
|
||||
# Step 3: Get services (optionally filtered by pet)
|
||||
services_response = self.get(Endpoints.SERVICES, params={"pet_id": pet_id})
|
||||
# Services endpoint may return 200 even without pet filter
|
||||
self.assertIn(services_response.status_code, [200, 404])
|
||||
|
||||
# Note: Steps 4-5 (select date/time, create service request) require
|
||||
# more setup (available times, cart, etc.) and are tested separately.
|
||||
Reference in New Issue
Block a user