Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
When upgrading to a new version, make sure to follow the directions under the "Upgrading" header of the corresponding version.
If there is no "Upgrading" header for that version, no post-upgrade actions need to be performed.

### Upcoming

#### Bug Fixes

- Fixed an issue causing the second flight on the same day to fail check-in. Now counts only flights with passengers who have valid boarding groups (#266, ).

## 8.3 (2025-03-10)
### Improvements
Expand Down
12 changes: 11 additions & 1 deletion lib/checkin_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,17 @@ def _attempt_check_in(self) -> JSON:

reservation = self._check_in_to_flight()
flights = reservation["checkInConfirmationPage"]["flights"]
if len(flights) >= expected_flights:

# Same-day flights may be received without passengers being assigned a boarding group,
# so we ensure the check-in only stops when all flights have at least one passenger with
# an assigned boarding group.
checked_in_count = sum(
any(p.get("boardingGroup") is not None for p in f.get("passengers", []))
for f in flights
)
logger.debug("Checked in %d/%d flights", checked_in_count, expected_flights)

if checked_in_count >= expected_flights:
logger.debug("Successfully checked in after %d attempts", attempts)
return reservation

Expand Down
73 changes: 62 additions & 11 deletions tests/integration/test_check_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,21 @@ def test_check_in(
handler.first_name = "Garry"
handler.last_name = "Lin"

post_response1 = {
first_post_response = {
"checkInViewReservationPage": {
"_links": {"checkIn": {"body": {"test": "checkin"}, "href": "/post_check_in"}}
}
}

post_response2 = {
final_post_response = {
"checkInConfirmationPage": {
"flights": [
{
"passengers": [
{"boardingGroup": "A", "boardingPosition": "42", "name": "Garry Lin"},
{"boardingGroup": "A", "boardingPosition": "43", "name": "Erin Lin"},
# Lap children are not assigned a boarding group
{"boardingGroup": None, "boardingPosition": None, "name": "Linda Lin"},
]
}
]
Expand All @@ -70,23 +72,72 @@ def test_check_in(

requests_mock.post(
BASE_URL + CHECKIN_URL + "TEST",
[{"json": post_response1, "status_code": 200}],
[{"json": first_post_response, "status_code": 200}],
)
requests_mock.post(
BASE_URL + "mobile-air-operations/post_check_in",
[{"json": post_response2, "status_code": 200}],
[{"json": final_post_response, "status_code": 200}],
)

if same_day_flight:
# Add a flight before to make sure a same day flight selects the second flight
same_day_post_response = copy.deepcopy(post_response2)
same_day_post_response["checkInConfirmationPage"]["flights"].insert(0, {})
# First, keep just the one flight. This simulates a response before the actual check-in time
same_day_post_response1 = copy.deepcopy(final_post_response)

# Add the same day flight with no boarding group assigned. This simulates a response after
# the actual check-in time, but before the check-in goes through
same_day_post_response2 = copy.deepcopy(final_post_response)
same_day_post_response2["checkInConfirmationPage"]["flights"].append(
{
"passengers": [
{
"boardingGroup": None,
"boardingPosition": None,
"name": "Garry Lin",
},
{
"boardingGroup": None,
"boardingPosition": None,
"name": "Erin Lin",
},
{
"boardingGroup": None,
"boardingPosition": None,
"name": "Linda Lin",
},
]
},
)

# Final response is the check-in has gone through
final_post_response = copy.deepcopy(final_post_response)
final_post_response["checkInConfirmationPage"]["flights"].append(
{
"passengers": [
{
"boardingGroup": "B",
"boardingPosition": "12",
"name": "Garry Lin",
},
{
"boardingGroup": "B",
"boardingPosition": "13",
"name": "Erin Lin",
},
{
"boardingGroup": None,
"boardingPosition": None,
"name": "Linda Lin",
},
]
},
)

requests_mock.post(
BASE_URL + "mobile-air-operations/post_check_in",
[
{"json": post_response2, "status_code": 200},
{"json": same_day_post_response, "status_code": 200},
{"json": same_day_post_response1, "status_code": 200},
{"json": same_day_post_response2, "status_code": 200},
{"json": final_post_response, "status_code": 200},
],
)

Expand All @@ -99,6 +150,6 @@ def test_check_in(
mock_successful_checkin = handler.notification_handler.successful_checkin
mock_successful_checkin.assert_called_once()

# Ensure all flights have been checked in
# Ensure all flights have been checked in and the expected response was returned
checked_in_flights = mock_successful_checkin.call_args[0][0]["flights"]
assert len(checked_in_flights) == 2 if same_day_flight else 1
assert checked_in_flights == final_post_response["checkInConfirmationPage"]["flights"]
55 changes: 43 additions & 12 deletions tests/unit/test_checkin_handler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import signal
from datetime import datetime
from unittest import mock
Expand All @@ -8,6 +9,18 @@
from lib.checkin_handler import MAX_CHECK_IN_ATTEMPTS, CheckInHandler
from lib.utils import AirportCheckInError, DriverTimeoutError, RequestError

POST_RESPONSE = {
"checkInConfirmationPage": {
"flights": [
{
"passengers": [
{"boardingGroup": "A", "boardingPosition": "1", "name": "Test Passenger"}
]
}
]
}
}


class TestCheckInHandler:
"""Contains common tests between the CheckInHandler and the SameDayCheckInHandler"""
Expand Down Expand Up @@ -196,37 +209,55 @@ def test_check_in_sends_success_notification_on_successful_check_in(
def test_attempt_check_in_succeeds_first_time_when_flight_is_not_same_day(
self, mocker: MockerFixture
) -> None:
post_response = {"checkInConfirmationPage": {"flights": ["flight1"]}}
mock_check_in_to_flight = mocker.patch.object(
CheckInHandler, "_check_in_to_flight", return_value=post_response
CheckInHandler, "_check_in_to_flight", return_value=POST_RESPONSE
)

self.handler.flight.is_same_day = False
reservation = self.handler._attempt_check_in()

mock_check_in_to_flight.assert_called_once()
assert reservation == post_response
assert reservation == POST_RESPONSE

def test_attempt_check_in_succeeds_after_multiple_attempts_same_day_flight(
self, mocker: MockerFixture
) -> None:
# The same-day flight check-in time is available, but the check-in hasn't gone through yet
second_post_response = copy.deepcopy(POST_RESPONSE)
second_post_response["checkInConfirmationPage"]["flights"].append(
{
"passengers": [
{"boardingGroup": None, "boardingPosition": None, "name": "Test Passenger"}
]
}
)

# The same-day flight is now checked in
third_post_response = copy.deepcopy(POST_RESPONSE)
third_post_response["checkInConfirmationPage"]["flights"].append(
{
"passengers": [
{"boardingGroup": "B", "boardingPosition": "2", "name": "Test Passenger"}
]
}
)

def test_submit_check_in_succeeds_after_multiple_attempts(self, mocker: MockerFixture) -> None:
first_post_response = {"checkInConfirmationPage": {"flights": ["flight1"]}}
second_post_response = {"checkInConfirmationPage": {"flights": ["flight1", "flight2"]}}
mocker.patch.object(
CheckInHandler,
"_check_in_to_flight",
side_effect=[first_post_response, second_post_response],
side_effect=[POST_RESPONSE, second_post_response, third_post_response],
)
mock_sleep = mocker.patch("time.sleep")

self.handler.flight.is_same_day = True
reservation = self.handler._attempt_check_in()

assert reservation == second_post_response
mock_sleep.assert_called_once()
assert reservation == third_post_response
assert mock_sleep.call_count == 2

def test_submit_check_in_fails_when_max_attempts_reached(self, mocker: MockerFixture) -> None:
post_response = {"checkInConfirmationPage": {"flights": ["flight1"]}}
def test_attempt_check_in_fails_when_max_attempts_reached(self, mocker: MockerFixture) -> None:
mock_check_in_to_flight = mocker.patch.object(
CheckInHandler, "_check_in_to_flight", return_value=post_response
CheckInHandler, "_check_in_to_flight", return_value=POST_RESPONSE
)
mocker.patch("time.sleep")

Expand Down