| """Service-level tests β pure logic, no DB, no HTTP. |
| |
| These guard the domain rules that our routes depend on. If a crisis |
| keyword stops being detected, a patient mid-crisis gets a generic reply |
| instead of 999. If CPR validation drifts, onboarding starts accepting |
| malformed IDs. Guard them directly, not indirectly through routes. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from app.core import localization as loc |
| from app.services import chat as chat_svc |
|
|
| |
| |
| |
|
|
|
|
| def _contains_crisis(text: str) -> bool: |
| """Mirrors the same lowercase-substring check the route uses.""" |
| lowered = text.lower() |
| return any(kw in lowered for kw in chat_svc.CRISIS_KEYWORDS) |
|
|
|
|
| @pytest.mark.parametrize( |
| "text", |
| [ |
| "i want to kill myself", |
| "I'm suicidal today", |
| "I want to die", |
| "I've been thinking about ending it all", |
| "I don't want to be here anymore", |
| "life isn't worth living", |
| "I made a plan for tonight", |
| "I've been cutting myself", |
| "I have the means", |
| ], |
| ) |
| def test_crisis_keyword_detected(text): |
| assert _contains_crisis(text), f"Should have flagged: {text!r}" |
|
|
|
|
| @pytest.mark.parametrize( |
| "text", |
| [ |
| "I feel sad today", |
| "I'm tired of feeling this way", |
| "work has been really hard", |
| "I keep replaying that conversation", |
| "my family doesn't understand", |
| "I had a tough week", |
| ], |
| ) |
| def test_non_crisis_text_not_flagged(text): |
| assert not _contains_crisis(text), f"Should NOT have flagged: {text!r}" |
|
|
|
|
| def test_crisis_response_is_bahrain_localized(): |
| """The static crisis fallback must mention Bahrain resources, not US ones.""" |
| resp = chat_svc.CRISIS_RESPONSE |
| assert "999" in resp, "Bahrain national emergency number missing" |
| |
| assert "988" not in resp |
| assert "Crisis Text Line" not in resp |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.parametrize( |
| "phone,expected", |
| [ |
| ("+97332223333", "+97332223333"), |
| ("32223333", "+97332223333"), |
| ("00973 3222 3333", "+97332223333"), |
| ("+973 3222 3333", "+97332223333"), |
| ("973-3222-3333", "+97332223333"), |
| ], |
| ) |
| def test_normalize_phone_bahrain(phone, expected): |
| assert loc.normalize_phone(phone) == expected |
|
|
|
|
| @pytest.mark.parametrize( |
| "bad_phone", |
| [ |
| "+14155551234", |
| "not-a-phone", |
| "12345", |
| "", |
| ], |
| ) |
| def test_normalize_phone_rejects_non_bahrain(bad_phone): |
| with pytest.raises(ValueError): |
| loc.normalize_phone(bad_phone) |
|
|
|
|
| def test_format_phone_display_adds_spacing(): |
| assert loc.format_phone_display("+97332223333") == "+973 3222 3333" |
|
|
|
|
| def test_format_date_dd_mm_yyyy(): |
| from datetime import date |
|
|
| assert loc.format_date(date(2026, 4, 15)) == "15/04/2026" |
|
|
|
|
| def test_format_datetime_dd_mm_yyyy_hhmm(): |
| from datetime import datetime |
|
|
| assert loc.format_datetime(datetime(2026, 4, 15, 14, 30)) == "15/04/2026 14:30" |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_cpr_rejects_wrong_length(): |
| assert loc.validate_cpr("12345") is False |
| assert loc.validate_cpr("1234567890") is False |
|
|
|
|
| def test_cpr_rejects_non_digits(): |
| assert loc.validate_cpr("85A423456") is False |
|
|
|
|
| def test_cpr_rejects_invalid_month(): |
| |
| assert loc.validate_cpr("851323456") is False |
|
|
|
|
| def test_cpr_accepts_plausible_format(): |
| |
| assert loc.validate_cpr("850423456") is True |
|
|
|
|
| def test_cpr_accepts_separators(): |
| |
| assert loc.validate_cpr("8504-2345-6") is True |
| assert loc.validate_cpr("8504 2345 6") is True |
|
|
|
|
| def test_cpr_display_formatting(): |
| assert loc.format_cpr_display("850423456") == "8504-2345-6" |
|
|
|
|
| def test_cpr_extract_dob(): |
| result = loc.extract_dob_from_cpr("850423456") |
| assert result is not None |
| year, month = result |
| assert month == 4 |
| |
| assert year == 1985 |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_calculate_age_is_stable_for_past_dob(): |
| from datetime import date |
|
|
| |
| |
| age = loc.calculate_age(date(1990, 1, 1)) |
| assert age >= 30 |
|
|
|
|
| def test_calculate_age_handles_today_as_dob(): |
| from datetime import date |
|
|
| |
| assert loc.calculate_age(date.today()) == 0 |
|
|