Skip to content

Commit 73e5dcf

Browse files
authored
VED-234 E2E Proxy test with exponential back-off (#389)
Proxy e2e test to use exponential back-off
1 parent 5238659 commit 73e5dcf

File tree

2 files changed

+75
-48
lines changed

2 files changed

+75
-48
lines changed

e2e/test_proxy.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
import uuid
55
import requests
6-
6+
from utils.immunisation_api import ImmunisationApi
77
from lib.env import get_service_base_path, get_status_endpoint_api_key
88

99

@@ -19,14 +19,18 @@ def setUpClass(cls):
1919

2020
def test_ping(self):
2121
"""/_ping should return 200 if proxy is up and running"""
22-
response = requests.get(f"{self.proxy_url}/_ping")
22+
response = ImmunisationApi.make_request_with_backoff(http_method="GET", url=f"{self.proxy_url}/_ping")
2323
self.assertEqual(response.status_code, 200, response.text)
2424

2525
def test_status(self):
2626
"""/_status should return 200 if proxy can reach to the backend"""
27-
response = requests.get(f"{self.proxy_url}/_status", headers={"apikey": self.status_api_key})
27+
response = ImmunisationApi.make_request_with_backoff(http_method="GET",
28+
url=f"{self.proxy_url}/_status",
29+
headers={"apikey": self.status_api_key},
30+
is_status_check=True)
2831
self.assertEqual(response.status_code, 200, response.text)
2932
body = response.json()
33+
3034
self.assertEqual(body["status"].lower(), "pass",
3135
f"service is not healthy: status: {body['status']}")
3236

@@ -40,8 +44,11 @@ def test_mtls(self):
4044
backend_health = f"https://{backend_url}/status"
4145

4246
with self.assertRaises(requests.exceptions.RequestException) as e:
43-
requests.get(backend_health, headers={"X-Request-ID": str(uuid.uuid4())})
44-
self.assertTrue("RemoteDisconnected" in str(e.exception))
47+
ImmunisationApi.make_request_with_backoff(
48+
http_method="GET",
49+
url=backend_health,
50+
headers={"X-Request-ID": str(uuid.uuid4())})
51+
self.assertTrue("RemoteDisconnected" in str(e.exception))
4552

4653
@staticmethod
4754
def get_backend_url() -> str:
@@ -80,5 +87,8 @@ def setUpClass(cls):
8087

8188
def test_invalid_access_token(self):
8289
"""it should return 401 if access token is invalid"""
83-
response = requests.get(f"{self.proxy_url}/Immunization", headers={"X-Request-ID": str(uuid.uuid4())})
90+
response = ImmunisationApi.make_request_with_backoff(http_method="GET",
91+
url=f"{self.proxy_url}/Immunization",
92+
headers={"X-Request-ID": str(uuid.uuid4())},
93+
expected_status_code=401)
8494
self.assertEqual(response.status_code, 401, response.text)

e2e/utils/immunisation_api.py

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ def parse_location(location) -> Optional[str]:
2121

2222

2323
class ImmunisationApi:
24-
MAX_RETRIES = 5
25-
STANDARD_REQUEST_DELAY_SECONDS = 1
26-
2724
url: str
2825
headers: dict
2926
auth: BaseAuthentication
@@ -50,17 +47,38 @@ def __str__(self):
5047
# The e2e tests put pressure on both test environments from APIGEE and PDS
5148
# so the chances of having rate limiting errors are high especially during
5249
# the busy times of the day.
53-
def _make_request_with_backoff(
54-
self,
50+
@staticmethod
51+
def make_request_with_backoff(
5552
http_method: str,
5653
url: str,
57-
expected_status_code: int,
54+
headers: dict = None,
55+
expected_status_code: int = 200,
56+
expected_connection_failure: bool = False,
57+
max_retries: int = 5,
58+
is_status_check: bool = False,
5859
**kwargs
5960
):
60-
for attempt in range(self.MAX_RETRIES):
61+
for attempt in range(max_retries):
6162
try:
62-
response = requests.request(http_method, url, **kwargs)
63-
63+
response = requests.request(method=http_method, url=url, headers=headers, **kwargs)
64+
65+
# This property is false by default and only true during the mtls test to simulate a connection failure
66+
if expected_connection_failure:
67+
raise RuntimeError(
68+
f"Expected the connection to fail, "
69+
f"but it succeeded instead.\n"
70+
f"Request method: {http_method}\n"
71+
f"URL: {url}"
72+
)
73+
74+
# Sometimes it can take time for the new endpoint to activate
75+
if is_status_check:
76+
body = response.json()
77+
if body["status"].lower() != "pass":
78+
raise RuntimeError(f"Server status check at {url} returned status code {response.status_code}, "
79+
f"but status is: {body['status']}")
80+
81+
# Check if the response matches the expected status code to identify potential issues
6482
if response.status_code != expected_status_code:
6583
if response.status_code >= 500:
6684
raise RuntimeError(f"Server error: {response.status_code} during "
@@ -72,18 +90,17 @@ def _make_request_with_backoff(
7290
return response
7391

7492
except Exception as e:
75-
if attempt == self.MAX_RETRIES - 1:
93+
if expected_connection_failure or attempt == max_retries - 1:
7694
raise
7795

78-
wait = (2 ** attempt) + random.uniform(0, 0.5)
79-
total_wait_time = wait + self.STANDARD_REQUEST_DELAY_SECONDS
80-
96+
# This is will be used in the retry logic of the exponential backoff
97+
delay = (3 ** attempt) + random.uniform(0, 0.5)
8198
print(
8299
f"[{datetime.now():%Y-%m-%d %H:%M:%S}] "
83-
f"[Retry {attempt + 1}] {http_method.upper()} {url}{e} — retrying in {total_wait_time:.2f}s"
100+
f"[Retry {attempt + 1}] {http_method.upper()} {url}{e} — retrying in {delay:.2f}s"
84101
)
85102

86-
time.sleep(total_wait_time)
103+
time.sleep(delay)
87104

88105
def create_immunization_resource(self, resource: dict = None) -> str:
89106
"""creates an Immunization resource and returns the resource url"""
@@ -105,19 +122,19 @@ def create_a_deleted_immunization_resource(self, resource: dict = None) -> dict:
105122
return imms
106123

107124
def get_immunization_by_id(self, event_id, expected_status_code: int = 200):
108-
return self._make_request_with_backoff(
109-
"GET",
110-
f"{self.url}/Immunization/{event_id}",
111-
expected_status_code,
112-
headers=self._update_headers()
125+
return self.make_request_with_backoff(
126+
http_method="GET",
127+
url=f"{self.url}/Immunization/{event_id}",
128+
headers=self._update_headers(),
129+
expected_status_code=expected_status_code
113130
)
114131

115132
def create_immunization(self, imms, expected_status_code: int = 201):
116-
response = self._make_request_with_backoff(
117-
"POST",
118-
f"{self.url}/Immunization",
119-
expected_status_code,
133+
response = self.make_request_with_backoff(
134+
http_method="POST",
135+
url=f"{self.url}/Immunization",
120136
headers=self._update_headers(),
137+
expected_status_code=expected_status_code,
121138
json=imms
122139
)
123140

@@ -134,29 +151,29 @@ def create_immunization(self, imms, expected_status_code: int = 201):
134151
return response
135152

136153
def update_immunization(self, imms_id, imms, expected_status_code: int = 200):
137-
return self._make_request_with_backoff(
138-
"PUT",
139-
f"{self.url}/Immunization/{imms_id}",
140-
expected_status_code,
154+
return self.make_request_with_backoff(
155+
http_method="PUT",
156+
url=f"{self.url}/Immunization/{imms_id}",
141157
headers=self._update_headers(),
158+
expected_status_code=expected_status_code,
142159
json=imms
143160
)
144161

145162
def delete_immunization(self, imms_id, expected_status_code: int = 204):
146-
return self._make_request_with_backoff(
147-
"DELETE",
148-
f"{self.url}/Immunization/{imms_id}",
149-
expected_status_code,
150-
headers=self._update_headers()
163+
return self.make_request_with_backoff(
164+
http_method="DELETE",
165+
url=f"{self.url}/Immunization/{imms_id}",
166+
headers=self._update_headers(),
167+
expected_status_code=expected_status_code,
151168
)
152169

153170
def search_immunizations(self, patient_identifier: str, immunization_target: str, expected_status_code: int = 200):
154-
return self._make_request_with_backoff(
155-
"GET",
156-
f"{self.url}/Immunization?patient.identifier={patient_identifier_system}|{patient_identifier}"
171+
return self.make_request_with_backoff(
172+
http_method="GET",
173+
url=f"{self.url}/Immunization?patient.identifier={patient_identifier_system}|{patient_identifier}"
157174
f"&-immunization.target={immunization_target}",
158-
expected_status_code,
159-
headers=self._update_headers()
175+
headers=self._update_headers(),
176+
expected_status_code=expected_status_code
160177
)
161178

162179
def search_immunizations_full(
@@ -171,11 +188,11 @@ def search_immunizations_full(
171188
else:
172189
url = f"{self.url}/Immunization?{query_string}"
173190

174-
return self._make_request_with_backoff(
175-
http_method,
176-
url,
177-
expected_status_code,
191+
return self.make_request_with_backoff(
192+
http_method=http_method,
193+
url=url,
178194
headers=self._update_headers({"Content-Type": "application/x-www-form-urlencoded"}),
195+
expected_status_code=expected_status_code,
179196
data=body
180197
)
181198

0 commit comments

Comments
 (0)