Init commit

This commit is contained in:
Reza Esmaeili
2025-12-04 12:46:23 +03:30
commit 9e69b14249
6 changed files with 1316 additions and 0 deletions

548
README.md Normal file
View File

@@ -0,0 +1,548 @@
# AvaAir E2E Flight Flow Test Suite
This test suite exercises the **end-to-end customer journey** for AvaAirs domestic flights (THR → MHD) using real API calls against a live environment (staging/production-like).
It is intentionally designed to:
* Validate **critical customer flows**: airport discovery, rules endpoints, flight search, booking, ticket issuance, and refund.
* Run both **interactively** (developer manually enters OTP) and **fully automated** (CI with pre-configured OTP).
* Be **readable and presentable** as a “production-grade” test harness.
---
## 1. High-level Architecture
The test suite is composed of:
* `conftest.py`
Global configuration and reusable fixtures shared across tests:
* Environment-driven configuration.
* Customer authentication (OTP flow).
* Authorization header fixture.
* Canonical THR → MHD search parameters.
* A small `api_wait` pacing helper for stabilizing async flows.
* `test_flight_thr_mhd_e2e.py`
Actual test cases:
* IATA / airport discovery tests.
* Rules endpoints sanity checks.
* A full end-to-end scenario for:
* Search → Book → Wallet check → Issue → Refund penalty → Refund → Post-refund validation.
The design intentionally centralizes configuration and “plumbing” in `conftest.py` so that individual tests remain concise and focused on business logic.
---
## 2. Environment & Configuration
All configuration is driven through environment variables with reasonable defaults, so the suite can be used:
* Quickly on a developer machine (with defaults).
* Safely in CI (with explicit overrides).
### 2.1 Base API URL
```bash
API_BASE_URL=http://192.168.19.19:8200/api/v1
```
* Default: `http://192.168.19.19:8200/api/v1`
* Use this to point the suite to **staging**, **pre-prod**, or **production**.
---
### 2.2 Test User Mobile & OTP
```bash
TEST_MOBILE_COUNTRY_CODE=98
TEST_MOBILE_NUMBER=9014332990
TEST_OTP_CODE=<optional pre-shared OTP>
```
* `TEST_MOBILE_COUNTRY_CODE` and `TEST_MOBILE_NUMBER` define the test customer account used for OTP login.
* `TEST_OTP_CODE` controls how OTP is resolved:
#### Interactive mode (local dev)
If `TEST_OTP_CODE` is **not** set:
* The suite sends an OTP to the configured mobile.
* The developer is prompted in the terminal:
```text
➡️ Enter OTP received on the test device:
```
* The entered OTP is then used for `/users/otp/check`.
#### Non-interactive mode (CI / automated)
If `TEST_OTP_CODE` **is** set:
* The suite **does not** prompt.
* The value is used directly in the OTP verify call.
* This enables fully headless execution in pipelines.
---
### 2.3 Booking Contact Info
```bash
TEST_BOOK_EMAIL=iamrezazoom@gmail.com
TEST_BOOK_PHONE=9014332990
```
* Email and phone number used in `/customer/flight/book` payloads.
* Defaults match the test users profile but can be overridden in more advanced setups.
---
### 2.4 Traveler Profile
These fields define the passive traveler object used during booking:
```bash
TEST_TRAVELER_FIRST_NAME=REZA
TEST_TRAVELER_LAST_NAME=ESMAEILI
TEST_TRAVELER_GENDER=male
TEST_TRAVELER_NATIONAL_CODE=0890500363
TEST_TRAVELER_DATE_OF_BIRTH=1998-05-20
TEST_TRAVELER_PLACE_OF_BIRTH=IRN
```
All of these have sensible defaults in `conftest.py`, but **in a real production-like setup** you should:
* Override them with a dedicated test passenger identity.
* Ensure the national ID and passport-related fields are valid for your environment.
---
## 3. Core Fixtures (conftest.py)
### 3.1 `_today_plus_days(days: int) -> str`
Utility that returns an ISO date (`YYYY-MM-DD`) relative to “today”:
* Used to always search for flights **7 days in the future**.
* Prevents hitting past or expired inventory.
```python
def _today_plus_days(days: int) -> str:
return (dt.date.today() + dt.timedelta(days=days)).isoformat()
```
---
### 3.2 `api_wait` Fixture
A small timing helper used to **pace** calls in E2E flows:
```python
@pytest.fixture
def api_wait():
import time
def _do(seconds: float = 1.0) -> None:
time.sleep(seconds)
return _do
```
Why it exists:
* Some backend operations are not purely synchronous:
* ticket issuance,
* wallet balance updates,
* ticket status changes.
* Adding short, explicit waits between operations makes the E2E flow **deterministic** even on real environments.
Usage example (from the E2E test):
```python
res_issue = requests.post(...)
assert res_issue.status_code == 204
api_wait(2.0) # Let backend finalize issuance before querying tickets
```
---
### 3.3 `customer_auth` Fixture
This is the **entry point** into the tested system from a customer perspective.
High-level behavior:
1. Sends OTP request:
```http
POST /users/otp/request
```
2. Resolves OTP from:
* `TEST_OTP_CODE` (if set), or
* interactive `input()` prompt.
3. Validates OTP via:
```http
POST /users/otp/check
```
4. Asserts the response contains:
* `auth_token`
* `refresh_token`
* `user`
5. Returns an auth context dict:
```python
{
"auth_token": "...",
"refresh_token": "...",
"user": {...},
}
```
If anything in this flow changes on the backend (contract change, field rename), the fixture fails **fast** with a clear assertion.
---
### 3.4 `auth_header` Fixture
Thin wrapper that exposes the **canonical Authorization header**:
```python
@pytest.fixture(scope="session")
def auth_header(customer_auth):
return {
"Authorization": f"Bearer {customer_auth['auth_token']}",
"accept": "application/json",
}
```
All tests consume this instead of re-implementing their own header logic.
---
### 3.5 `thr_to_mhd_search_params` Fixture
Single source of truth for THR → MHD search parameters:
```python
@pytest.fixture(scope="session")
def thr_to_mhd_search_params():
return {
"origin_iata": "THR",
"destination_iata": "MHD",
"flight_date": _today_plus_days(7),
"page": 1,
"page_size": 20,
"sort_option": "lowest-price",
}
```
Used by the E2E test to:
* Always search a known route (THR → MHD).
* Always target `today + 7` days.
* Always sort by `lowest-price` to make the first itinerary deterministic.
---
## 4. Test Cases Overview (`test_flight_thr_mhd_e2e.py`)
### 4.1 IATA / Airport Discovery Tests
These cover the **airport search layer** exposed to the customer:
#### `test_iata_domestic_default_list`
* **Endpoint**: `GET /customer/iata`
* **Parameters**:
* `keyword = ""`
* `is_international = false`
* **Validations**:
* HTTP 200.
* `data` is a non-empty list.
* Both `THR` and `MHD` are present in the code list.
Purpose:
Ensures the default domestic IATA catalog is healthy and includes key airports.
---
#### `test_iata_search_origin_tehran`
* **Endpoint**: `GET /customer/iata`
* **Parameters**:
* `keyword = "tehran"`
* `is_international = false`
* **Validations**:
* HTTP 200.
* `data` is a non-empty list.
* `THR` appears in results.
Purpose:
Validates keyword search for origin airports (Tehran).
---
#### `test_iata_search_destination_mashhad_excluding_origin`
* **Endpoint**: `GET /customer/iata`
* **Parameters**:
* `keyword = "mashhad"`
* `is_international = false`
* `other_iata_code = "THR"`
* **Validations**:
* HTTP 200.
* `data` is a non-empty list.
* `MHD` appears.
* `THR` **does not** appear (origin must be excluded).
Purpose:
Confirms that destination search respects the “other airport” exclusion logic.
---
### 4.2 General & Refund Rules Test
#### `test_general_and_refund_rules`
Endpoints:
* `GET /customer/setting/get/general-rules`
* `GET /customer/setting/get/refund-rules`
Validations:
* `general-rules`:
* `data.general-rules` exists and is non-empty.
* `refund-rules`:
* `data.refund_rules.domestic.data` is a non-empty list of objects.
* `data.refund_rules.international.data` the same.
* First item in each has `description` and `penalty`.
Purpose:
Quickly validates that the **configuration surface** (general rules and refund rules shown to customers) is available and structurally sound.
---
### 4.3 E2E: THR → MHD Single Adult Flow
#### `@pytest.mark.e2e test_thr_mhd_single_adult_book_issue_refund_e2e(...)`
This is the **flagship scenario**.
It simulates a real customer:
1. **Search flights (THR → MHD, D+7)**
* `POST /customer/flight/search`
* Validates:
* HTTP 200.
* Non-empty `itinerary_list`.
* First itinerary has:
* valid `flight_id`,
* at least one segment,
* segment from `THR` to `MHD`,
* `total_fare > 0`.
2. **Book the first itinerary**
* `POST /customer/flight/book`
* Payload includes:
* Contact info (`email`, `phone_number`).
* Single ADT traveler (driven by env / defaults).
* Validates:
* HTTP 200.
* `id` (booking_id) is positive.
* `expires_at > 0`.
* `price > 0` (booking_price).
3. **Verify wallet credit**
* `GET /admin/user/credit`
* Validates:
* HTTP 200.
* `credit` is numeric.
* `credit >= booking_price`.
* This step guarantees the issue step using wallet credit will succeed and is not blocked by insufficient balance.
4. **Issue the ticket using wallet credit**
* `POST /customer/flight/book/{booking_id}/issue`
* Payload:
* `pay_with_credit = true`
* `pay_for = "ticket"`
* `platform = "mobile"`
* Validates:
* HTTP 204 (No Content).
* Followed by `api_wait(2.0)` to allow backend to finalize issuance.
5. **Fetch active tickets and locate the issued one**
* `GET /customer/ticket` with:
* `is_past = false`
* `statuses = confirmed`
* Validates:
* HTTP 200.
* Non-empty ticket list.
* Ticket with `total == booking_price` exists.
* Extracts:
* `ticket_id`
* `ticket_traveler_id` (first traveler)
* `ticket_segment_id` (first segment)
6. **Calculate refund penalty**
* `POST /customer/flight/refund-penalty`
* Payload:
* `ticket_segment_id`
* `[ticket_traveler_id]`
* Validations (for this no-penalty scenario):
* HTTP 200.
* Non-empty `data` array.
* First item:
* `payable > 0`
* `amount_that_should_pay_back_to_passenger > 0`
* `penalty_percent == 0`
* `amount_that_should_pay_back_to_passenger == booking_price`
* `ticket_traveler_id` matches the one sent.
7. **Perform refund**
* `POST /customer/flight/refund`
* Payload:
* same `ticket_segment_id` and `ticket_traveler_id`.
* Validates:
* HTTP 200.
* Followed by `api_wait(2.0)` to allow status propagation.
8. **Validate post-refund ticket state**
* `GET /customer/ticket` with:
* `is_past = true`
* `statuses = confirmed,refunded_partially,refunded`
* Locates the same `ticket_id`.
* Validates:
* `refund_status` is in `("refunded", "refunded_partially")`.
Throughout the flow, the test logs each step with clear `[STEP X]` messages, making it easy to follow in CI logs or during a live demo.
---
## 5. Running the Tests
### 5.1 Install dependencies
```bash
pip install pytest requests
```
(Or include them in your projects `requirements.txt` / `poetry` / `pipenv`.)
---
### 5.2 Run all tests
```bash
pytest -q -s
```
* `-s` keeps the step-by-step console output:
* OTP flow logs,
* `[STEP X]` E2E narration.
---
### 5.3 Run only the E2E scenario
The E2E test is marked with `@pytest.mark.e2e`. To run only that:
```bash
pytest -q -s -m e2e
```
To avoid Pytests “unknown mark” warning, add a `pytest.ini` in the repo root:
```ini
# pytest.ini
[pytest]
markers =
e2e: end-to-end tests that hit real external services (search, book, issue, refund)
```
---
## 6. Production Safety Notes
Because this suite can be pointed to a **live environment**:
* It **does**:
* Create real bookings.
* Issue real tickets.
* Immediately refund them.
* It **assumes**:
* The test accounts wallet has enough credit.
* The fare and policy selected result in **zero penalty** for the scenario.
Recommendations:
* Use a **dedicated test customer** with:
* Preloaded credit.
* No penalty restrictions.
* Run E2E tests:
* On staging/pre-prod when possible.
* On production only as part of **controlled smoke** runs.
---
## 7. Summary
This test suite gives you:
* A **high-signal E2E validation** of the critical THR → MHD domestic flow.
* A set of **supporting tests** for IATA and rules configuration.
* A **presentable**, narrative output that clearly shows each phase:
* Authentication
* Discovery
* Booking
* Wallet validation
* Issuance
* Refund calculation
* Final refund state verification