"""Tests for rental request router (T05).""" import pytest from unittest.mock import AsyncMock, patch from sqlalchemy import select from app.models.rental_request import RentalRequest, RentalRequestItem VALID_RENTAL_PAYLOAD = { "event_name": "Sommerfestival 2026", "date_start": "2026-08-15", "date_end": "2026-08-17", "location": "Berlin", "person_count": 500, "contact_name": "Max Veranstalter", "contact_company": "Event GmbH", "contact_email": "max@event.de", "contact_phone": "+49 30 1234567", "contact_street": "Hauptstr. 42", "contact_postalcode": "10115", "contact_city": "Berlin", "message": "Bitte um Angebot", "items": [{"equipment_id": 1, "quantity": 2}], } @pytest.mark.asyncio async def test_rental_valid_returns_201(client, seeded_equipment): """POST /api/rental-requests with valid data returns 201 with reference number.""" with patch( "app.services.rentman_service.RentmanService.create_project_request", new_callable=AsyncMock, return_value={"id": "rentman-123"}, ), patch( "app.services.rentman_service.RentmanService.add_equipment_to_request", new_callable=AsyncMock, return_value={"id": "eq-1"}, ), patch( "app.services.email_service.EmailService.send_rental_confirmation", new_callable=AsyncMock, return_value=True, ): resp = await client.post("/api/rental-requests", json=VALID_RENTAL_PAYLOAD) assert resp.status_code == 201 data = resp.json() assert "reference_number" in data ref = data["reference_number"] assert ref.startswith("HMS-") parts = ref.split("-") assert len(parts) == 3 assert parts[1].isdigit() assert len(parts[2]) == 5 assert parts[2].isdigit() assert data["status"] == "pending" @pytest.mark.asyncio async def test_rental_date_end_before_start_returns_422(client, seeded_equipment): """POST /api/rental-requests with date_end < date_start returns 422.""" payload = {**VALID_RENTAL_PAYLOAD, "date_start": "2026-08-20", "date_end": "2026-08-15"} resp = await client.post("/api/rental-requests", json=payload) assert resp.status_code == 422 @pytest.mark.asyncio async def test_rental_empty_items_returns_422(client, seeded_equipment): """POST /api/rental-requests with empty items array returns 422.""" payload = {**VALID_RENTAL_PAYLOAD, "items": []} resp = await client.post("/api/rental-requests", json=payload) assert resp.status_code == 422 @pytest.mark.asyncio async def test_rental_saved_to_db(client, seeded_equipment, test_db): """POST /api/rental-requests saves to rental_requests and rental_request_items tables.""" with patch( "app.services.rentman_service.RentmanService.create_project_request", new_callable=AsyncMock, return_value={"id": "rentman-456"}, ), patch( "app.services.rentman_service.RentmanService.add_equipment_to_request", new_callable=AsyncMock, return_value={"id": "eq-2"}, ), patch( "app.services.email_service.EmailService.send_rental_confirmation", new_callable=AsyncMock, return_value=True, ): resp = await client.post("/api/rental-requests", json=VALID_RENTAL_PAYLOAD) assert resp.status_code == 201 ref_number = resp.json()["reference_number"] result = await test_db.execute( select(RentalRequest).where(RentalRequest.reference_number == ref_number) ) rental = result.scalar_one_or_none() assert rental is not None assert rental.event_name == "Sommerfestival 2026" assert rental.contact_name == "Max Veranstalter" assert rental.status == "pending" assert rental.rentman_request_id == "rentman-456" assert rental.rentman_sync_status == "success" items_result = await test_db.execute( select(RentalRequestItem).where(RentalRequestItem.rental_request_id == rental.id) ) items = items_result.scalars().all() assert len(items) == 1 assert items[0].equipment_id == 1 assert items[0].quantity == 2 assert items[0].equipment_name == "L-Acoustics K2" @pytest.mark.asyncio async def test_rental_triggers_rentman(client, seeded_equipment): """POST /api/rental-requests triggers Rentman create_project_request.""" with patch( "app.services.rentman_service.RentmanService.create_project_request", new_callable=AsyncMock, return_value={"id": "rentman-789"}, ) as mock_create, patch( "app.services.rentman_service.RentmanService.add_equipment_to_request", new_callable=AsyncMock, return_value={"id": "eq-3"}, ) as mock_add, patch( "app.services.email_service.EmailService.send_rental_confirmation", new_callable=AsyncMock, return_value=True, ): resp = await client.post("/api/rental-requests", json=VALID_RENTAL_PAYLOAD) assert resp.status_code == 201 mock_create.assert_awaited_once() mock_add.assert_awaited_once() @pytest.mark.asyncio async def test_rental_triggers_email(client, seeded_equipment): """POST /api/rental-requests triggers confirmation email.""" with patch( "app.services.rentman_service.RentmanService.create_project_request", new_callable=AsyncMock, return_value={"id": "rentman-email-test"}, ), patch( "app.services.rentman_service.RentmanService.add_equipment_to_request", new_callable=AsyncMock, return_value={"id": "eq-4"}, ), patch( "app.services.email_service.EmailService.send_rental_confirmation", new_callable=AsyncMock, return_value=True, ) as mock_email: resp = await client.post("/api/rental-requests", json=VALID_RENTAL_PAYLOAD) assert resp.status_code == 201 mock_email.assert_awaited_once() call_kwargs = mock_email.call_args.kwargs assert call_kwargs["to_addr"] == "max@event.de" assert call_kwargs["event_name"] == "Sommerfestival 2026" assert call_kwargs["reference_number"].startswith("HMS-") assert len(call_kwargs["items"]) == 1 @pytest.mark.asyncio async def test_rental_rentman_failure_still_returns_201(client, seeded_equipment): """POST /api/rental-requests still returns 201 when Rentman API fails.""" with patch( "app.services.rentman_service.RentmanService.create_project_request", new_callable=AsyncMock, side_effect=Exception("Rentman API error"), ), patch( "app.services.email_service.EmailService.send_rental_confirmation", new_callable=AsyncMock, return_value=True, ): resp = await client.post("/api/rental-requests", json=VALID_RENTAL_PAYLOAD) assert resp.status_code == 201