Files
avaair-e2e-tests/tests/test_flight_thr_mhd_e2e.py
Reza Esmaeili 9e69b14249 Init commit
2025-12-04 12:46:23 +03:30

516 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# test_flight_thr_mhd_e2e.py
import requests
import pytest
from conftest import (
BASE_URL,
TEST_EMAIL,
TEST_PHONE_NUMBER,
TRAVELER_FIRST_NAME,
TRAVELER_LAST_NAME,
TRAVELER_GENDER,
TRAVELER_NATIONAL_CODE,
TRAVELER_DATE_OF_BIRTH,
TRAVELER_PLACE_OF_BIRTH,
)
# =====================================================================================
# IATA / Airport discovery tests
# =====================================================================================
def test_iata_domestic_default_list(auth_header):
"""
Validate the default domestic IATA list.
Scenario:
• Perform GET /customer/iata with:
- keyword = "" (no filter)
- is_international = false
• Assert:
- Response is HTTP 200.
- "data" is a non-empty list.
- Both THR and MHD are present in the list (core domestic airports).
"""
res = requests.get(
f"{BASE_URL}/customer/iata",
params={
"keyword": "",
"is_international": "false",
"other_iata_code": "",
},
headers=auth_header,
timeout=10,
)
assert res.status_code == 200, "Expected HTTP 200 for domestic IATA default list"
data = res.json()
assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list"
assert data["data"], "Domestic IATA list should not be empty"
codes = {item["code"] for item in data["data"]}
assert "THR" in codes, "THR should be present in domestic IATA list"
assert "MHD" in codes, "MHD should be present in domestic IATA list"
def test_iata_search_origin_tehran(auth_header):
"""
Validate origin search for Tehran.
Scenario:
• Perform GET /customer/iata with:
- keyword = "tehran"
- is_international = false
• Assert:
- Response is HTTP 200.
- "data" is a non-empty list.
- THR is returned as one of the matches.
"""
res = requests.get(
f"{BASE_URL}/customer/iata",
params={
"keyword": "tehran",
"is_international": "false",
"other_iata_code": "",
},
headers=auth_header,
timeout=10,
)
assert res.status_code == 200, "Expected HTTP 200 for IATA search with keyword 'tehran'"
data = res.json()
assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list"
assert data["data"], "IATA search(tehran) should not be empty"
codes = {item["code"] for item in data["data"]}
assert "THR" in codes, "THR should be present in IATA search result for 'tehran'"
def test_iata_search_destination_mashhad_excluding_origin(auth_header):
"""
Validate destination search for Mashhad while excluding origin.
Scenario:
• Perform GET /customer/iata with:
- keyword = "mashhad"
- is_international = false
- other_iata_code = "THR"
• Assert:
- Response is HTTP 200.
- "data" is a non-empty list.
- MHD is present in results.
- THR is explicitly NOT present (origin and destination must differ).
"""
res = requests.get(
f"{BASE_URL}/customer/iata",
params={
"keyword": "mashhad",
"is_international": "false",
"other_iata_code": "THR",
},
headers=auth_header,
timeout=10,
)
assert res.status_code == 200, "Expected HTTP 200 for IATA search with keyword 'mashhad'"
data = res.json()
assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list"
assert data["data"], "IATA search(mashhad) should not be empty"
codes = {item["code"] for item in data["data"]}
assert "MHD" in codes, "MHD should be in Mashhad search result"
assert "THR" not in codes, "THR must NOT appear when other_iata_code=THR (origin must be excluded)"
# =====================================================================================
# General and refund rules
# =====================================================================================
def test_general_and_refund_rules(auth_header):
"""
Validate general rules and refund rules configuration.
Endpoints under test:
• GET /customer/setting/get/general-rules
• GET /customer/setting/get/refund-rules
Minimal structure validation:
• general-rules:
- "data.general-rules" exists and is non-empty (typically an HTML string).
• refund-rules:
- "data.refund_rules.domestic.data" is a non-empty list of items with
"description" and "penalty" fields.
- "data.refund_rules.international.data" behaves similarly.
"""
# --- general-rules ---
res_gen = requests.get(
f"{BASE_URL}/customer/setting/get/general-rules",
headers=auth_header,
timeout=10,
)
assert res_gen.status_code == 200, "Expected HTTP 200 for general-rules endpoint"
data_gen = res_gen.json()
assert "data" in data_gen, "general-rules response must contain 'data' field"
assert data_gen["data"].get("general-rules"), "general-rules must be present and non-empty"
# --- refund-rules ---
res_ref = requests.get(
f"{BASE_URL}/customer/setting/get/refund-rules",
headers=auth_header,
timeout=10,
)
assert res_ref.status_code == 200, "Expected HTTP 200 for refund-rules endpoint"
data_ref = res_ref.json()
refund_rules = data_ref.get("data", {}).get("refund_rules")
assert refund_rules is not None, "refund_rules should exist under data.refund_rules"
for key in ("domestic", "international"):
section = refund_rules.get(key)
assert section is not None, f"refund_rules.{key} should exist"
items = section.get("data")
assert isinstance(items, list) and items, f"refund_rules.{key}.data should be a non-empty list"
first = items[0]
assert "description" in first, f"refund_rules.{key}.data[0] must contain 'description'"
assert "penalty" in first, f"refund_rules.{key}.data[0] must contain 'penalty'"
# =====================================================================================
# E2E: THR → MHD single adult flow (search → book → issue → refund)
# =====================================================================================
@pytest.mark.e2e
def test_thr_mhd_single_adult_book_issue_refund_e2e(
auth_header,
thr_to_mhd_search_params,
api_wait,
):
"""
End-to-end happy path for a single adult THR → MHD domestic flight.
High-level flow:
1) Search for THR → MHD flights for (today + 7 days).
2) Select the first itinerary (sorted by lowest-price).
3) Book the itinerary for a single Adult passenger.
4) Verify that the user's wallet credit is >= booking price.
5) Issue the ticket using wallet credit (pay_with_credit = true).
6) Retrieve customer tickets and locate the ticket associated with this booking.
7) Call refund-penalty and validate:
- No penalty (penalty_percent == 0).
- Full amount is refundable (amount_that_should_pay_back_to_passenger == booking_price).
8) Perform the actual refund.
9) Retrieve tickets again and assert the ticket's refund_status reflects the refund.
"""
# -------------------------------------------------------------------------
# 1) Search THR → MHD
# -------------------------------------------------------------------------
print("\n[STEP 1] Searching for THR → MHD itineraries ...")
search_payload = {
"origin_destination_option_list": [
{
"OriginIataCode": thr_to_mhd_search_params["origin_iata"],
"DestinationIataCode": thr_to_mhd_search_params["destination_iata"],
"FlightDate": thr_to_mhd_search_params["flight_date"],
}
],
"page": thr_to_mhd_search_params["page"],
"page_size": thr_to_mhd_search_params["page_size"],
"sort_option": thr_to_mhd_search_params["sort_option"],
}
res_search = requests.post(
f"{BASE_URL}/customer/flight/search",
json=search_payload,
headers={**auth_header, "Content-Type": "application/json"},
timeout=15,
)
assert res_search.status_code == 200, "Expected HTTP 200 for flight search"
data_search = res_search.json()
itinerary_list = data_search.get("itinerary_list") or []
assert itinerary_list, "No itineraries returned for THR→MHD search"
itinerary = itinerary_list[0]
flight_id = itinerary.get("flight_id")
assert flight_id, "flight_id must be present for the selected itinerary"
# Minimal segment validation we assert the route is THR → MHD.
segments = itinerary.get("flight_segment_list") or []
assert segments, "flight_segment_list should not be empty"
first_segment = segments[0]
assert first_segment.get("origin_iata_code") == "THR", "Segment origin_iata_code must be THR"
assert first_segment.get("destination_iata_code") == "MHD", "Segment destination_iata_code must be MHD"
# Ticket must not be free ensure we are dealing with a priced product.
price_info = itinerary.get("price_info") or {}
total_fare = price_info.get("total_fare")
assert isinstance(total_fare, (int, float)) and total_fare > 0, "total_fare must be a positive number"
print(
f"[STEP 1] Selected itinerary flight_id={flight_id} "
f"with total_fare={total_fare} and {len(segments)} segment(s)."
)
api_wait(0.5)
# -------------------------------------------------------------------------
# 2) Book the selected itinerary for a single Adult passenger
# -------------------------------------------------------------------------
print("[STEP 2] Booking selected itinerary for a single adult passenger ...")
book_payload = {
"country_code": "98",
"email": TEST_EMAIL,
"flight_id": flight_id,
"phone_number": TEST_PHONE_NUMBER,
"travelers": [
{
"date_of_birth": TRAVELER_DATE_OF_BIRTH,
"first_name": TRAVELER_FIRST_NAME,
"gender": TRAVELER_GENDER,
"last_name": TRAVELER_LAST_NAME,
"national_code": TRAVELER_NATIONAL_CODE,
"place_of_birth": TRAVELER_PLACE_OF_BIRTH,
"type": "ADT",
}
],
}
res_book = requests.post(
f"{BASE_URL}/customer/flight/book",
json=book_payload,
headers={**auth_header, "Content-Type": "application/json"},
timeout=15,
)
assert res_book.status_code == 200, "Expected HTTP 200 for flight book"
data_book = res_book.json()
booking_id = data_book.get("id")
expires_at = data_book.get("expires_at")
booking_price = data_book.get("price")
assert isinstance(booking_id, int) and booking_id > 0, "Booking id must be a positive integer"
assert isinstance(expires_at, int) and expires_at > 0, "Booking expiry (minutes) must be a positive integer"
assert isinstance(booking_price, (int, float)) and booking_price > 0, "Booking price must be > 0"
print(
f"[STEP 2] Booking created successfully. "
f"id={booking_id}, price={booking_price}, expires_in={expires_at} minutes."
)
api_wait(1.0)
# -------------------------------------------------------------------------
# 3) Validate wallet credit is sufficient for the booking price
# -------------------------------------------------------------------------
print("[STEP 3] Verifying wallet credit is sufficient for booking price ...")
res_credit = requests.get(
f"{BASE_URL}/admin/user/credit",
headers=auth_header,
timeout=10,
)
assert res_credit.status_code == 200, "Expected HTTP 200 for user credit endpoint"
data_credit = res_credit.json()
credit = data_credit.get("credit")
assert isinstance(credit, (int, float)), "credit must be numeric"
assert credit >= booking_price, "User credit is not sufficient for booking price"
print(f"[STEP 3] Wallet credit={credit} is sufficient for booking_price={booking_price}.")
api_wait(0.5)
# -------------------------------------------------------------------------
# 4) Issue the ticket using wallet credit
# -------------------------------------------------------------------------
print("[STEP 4] Issuing the ticket using wallet credit ...")
issue_payload = {
"pay_with_credit": True,
"pay_with_flight_coin": False,
"payment_gateway": "",
"pay_for": "ticket",
"platform": "mobile",
}
res_issue = requests.post(
f"{BASE_URL}/customer/flight/book/{booking_id}/issue",
json=issue_payload,
headers={**auth_header, "Content-Type": "application/json"},
timeout=30,
)
assert res_issue.status_code == 204, "Expected HTTP 204 (No Content) for ticket issue"
print("[STEP 4] Ticket issue request completed with HTTP 204. Backend is finalizing issuance ...")
api_wait(2.0)
# -------------------------------------------------------------------------
# 5) Retrieve tickets and locate the one associated with this booking
# -------------------------------------------------------------------------
print("[STEP 5] Fetching active tickets and locating the issued one ...")
res_tickets_after_issue = requests.get(
f"{BASE_URL}/customer/ticket",
params={
"page": 1,
"page_size": 20,
"is_past": "false",
"statuses": "confirmed",
"sort": "issue_date",
"sort_dir": "DESC",
},
headers=auth_header,
timeout=15,
)
assert res_tickets_after_issue.status_code == 200, "Expected HTTP 200 for ticket list after issue"
tickets_data = res_tickets_after_issue.json()
ticket_records = tickets_data.get("data") or []
assert ticket_records, "Ticket list after issue should not be empty"
# For simplicity, locate the first ticket whose total matches our booking price.
ticket = None
for t in ticket_records:
if t.get("total") == booking_price:
ticket = t
break
assert ticket is not None, "Issued ticket with matching price not found in ticket list"
ticket_id = ticket["id"]
print(f"[STEP 5] Located ticket id={ticket_id} with total={ticket.get('total')}.")
travelers = ticket.get("travelers") or []
assert travelers, "Ticket must have at least one traveler"
traveler = travelers[0] # Single adult in this scenario
ticket_traveler_id = traveler["id"]
segments = ticket.get("segments") or []
assert segments, "Ticket must have at least one segment"
segment = segments[0]
ticket_segment_id = segment["id"]
print(
f"[STEP 5] Using ticket_segment_id={ticket_segment_id}, "
f"ticket_traveler_id={ticket_traveler_id} for refund operations."
)
api_wait(1.5)
# -------------------------------------------------------------------------
# 6) Call refund-penalty and validate zero-penalty scenario
# -------------------------------------------------------------------------
print("[STEP 6] Calculating refund penalty and validating zero-penalty scenario ...")
refund_penalty_payload = {
"ticket_segment_id": ticket_segment_id,
"ticket_traveler_ids": [ticket_traveler_id],
}
res_refund_penalty = requests.post(
f"{BASE_URL}/customer/flight/refund-penalty",
json=refund_penalty_payload,
headers={**auth_header, "Content-Type": "application/json"},
timeout=15,
)
assert res_refund_penalty.status_code == 200, "Expected HTTP 200 for refund-penalty endpoint"
data_refund_penalty = res_refund_penalty.json()
penalty_items = data_refund_penalty.get("data") or []
assert penalty_items, "refund-penalty data list must not be empty"
pitem = penalty_items[0]
payable = pitem.get("payable")
amount_back = pitem.get("amount_that_should_pay_back_to_passenger")
penalty_percent = pitem.get("penalty_percent")
returned_traveler_id = pitem.get("ticket_traveler_id")
assert isinstance(payable, (int, float)) and payable > 0, "payable must be > 0"
assert isinstance(amount_back, (int, float)) and amount_back > 0, "amount_that_should_pay_back_to_passenger must be > 0"
assert penalty_percent == 0, "This E2E scenario is expected to have zero penalty"
assert amount_back == booking_price, "Full amount should be refunded to passenger"
assert returned_traveler_id == ticket_traveler_id, "ticket_traveler_id must match the requested one"
print(
f"[STEP 6] Refund penalty validated. penalty_percent={penalty_percent}, "
f"amount_back={amount_back}, payable={payable}."
)
api_wait(0.5)
# -------------------------------------------------------------------------
# 7) Perform the actual refund
# -------------------------------------------------------------------------
print("[STEP 7] Performing actual refund ...")
refund_payload = {
"ticket_segment_id": ticket_segment_id,
"ticket_traveler_ids": [ticket_traveler_id],
}
res_refund = requests.post(
f"{BASE_URL}/customer/flight/refund",
json=refund_payload,
headers={**auth_header, "Content-Type": "application/json"},
timeout=30,
)
assert res_refund.status_code == 200, "Expected HTTP 200 for refund endpoint"
print("[STEP 7] Refund request accepted. Waiting for status to converge ...")
api_wait(2.0)
# -------------------------------------------------------------------------
# 8) Verify that the ticket is now marked as refunded/refunded_partially
# -------------------------------------------------------------------------
print("[STEP 8] Fetching tickets after refund and validating refund_status ...")
res_tickets_after_refund = requests.get(
f"{BASE_URL}/customer/ticket",
params={
"page": 1,
"page_size": 20,
"is_past": "true",
"statuses": "confirmed,refunded_partially,refunded",
"sort": "issue_date",
"sort_dir": "DESC",
},
headers=auth_header,
timeout=15,
)
assert res_tickets_after_refund.status_code == 200, "Expected HTTP 200 for ticket list after refund"
tickets_after_ref_data = res_tickets_after_refund.json()
tickets_after_ref = tickets_after_ref_data.get("data") or []
refunded_ticket = None
for t in tickets_after_ref:
if t.get("id") == ticket_id:
refunded_ticket = t
break
assert refunded_ticket is not None, "Refunded ticket not found in ticket list after refund"
refund_status = refunded_ticket.get("refund_status")
assert refund_status in ("refunded", "refunded_partially"), (
f"Unexpected refund_status for ticket {ticket_id}: {refund_status}"
)
print(
f"[STEP 8] Refund flow completed successfully for ticket_id={ticket_id} "
f"with refund_status={refund_status}.\n"
)