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

View File

@@ -0,0 +1,515 @@
# 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"
)