Skip to content

Commit 44bed29

Browse files
committedFeb 27, 2025
Merge branch 'develop' into task/gh-64
2 parents 5657390 + f560983 commit 44bed29

File tree

9 files changed

+682
-5
lines changed

9 files changed

+682
-5
lines changed
 

‎.github/workflows/django_test.yml

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Django CI
2+
3+
on:
4+
push:
5+
branches: [ "develop" ]
6+
pull_request:
7+
branches: [ "develop" ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: ubuntu-latest
13+
env:
14+
POSTGRES_USER: dbuser
15+
POSTGRES_PASSWORD: dbpassword
16+
POSTGRES_NAME: dbname
17+
POSTGRES_PORT: 5432
18+
POSTGRES_HOST: localhost
19+
services:
20+
db:
21+
image: postgres:16
22+
env:
23+
POSTGRES_USER: ${{ env.POSTGRES_USER }}
24+
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
25+
POSTGRES_DB: ${{ env.POSTGRES_NAME }}
26+
ports:
27+
- 5432:5432
28+
options: >-
29+
--health-cmd pg_isready
30+
--health-interval 10s
31+
--health-timeout 5s
32+
--health-retries 5
33+
strategy:
34+
max-parallel: 4
35+
matrix:
36+
python-version: [3.12, 3.13]
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
- name: Set up Python ${{ matrix.python-version }}
41+
uses: actions/setup-python@v3
42+
with:
43+
python-version: ${{ matrix.python-version }}
44+
- name: Install Dependencies
45+
run: |
46+
python -m pip install --upgrade pip
47+
pip install -r requirements.txt
48+
- name: Run Tests
49+
run: |
50+
python manage.py test

‎edrop/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
'NAME': os.environ.get('POSTGRES_NAME'),
9595
'USER': os.environ.get('POSTGRES_USER'),
9696
'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
97-
'HOST': 'db',
97+
'HOST': os.environ.get('POSTGRES_HOST', 'db'),
9898
'PORT': os.environ.get('POSTGRES_PORT'),
9999
}
100100
}

‎track/gbf.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def create_order(order, adress_data):
3434
logger.info(message)
3535

3636
# make order with GBF
37-
return _place_order_with_GBF(order_json, order_number)
37+
order_response = _place_order_with_GBF(order_json, order_number)
38+
39+
return _check_order_response(order_response, order_number)
3840

3941
def _generate_order_number(order):
4042
"""
@@ -100,6 +102,9 @@ def _place_order_with_GBF(order_json, order_number):
100102
log_manager.append_to_gbf_log(LogManager.LEVEL_INFO, response, order_number)
101103
logger.info(response)
102104

105+
return response
106+
107+
def _check_order_response(response, order_number):
103108
response_body = response.json()
104109
log_manager.append_to_gbf_log(LogManager.LEVEL_DEBUG, response_body, order_number)
105110
logger.debug(response_body)
@@ -142,7 +147,7 @@ def get_order_confirmations(order_numbers):
142147
143148
GBF sends json like this:
144149
{
145-
"success": true,
150+
"success": True,
146151
"dataArray": [
147152
{
148153
"format": "json",
@@ -184,7 +189,7 @@ def get_order_confirmations(order_numbers):
184189
message = err
185190
log_manager.append_to_gbf_log(LogManager.LEVEL_ERROR, message)
186191
logger.error(message)
187-
return None
192+
return None
188193

189194
response_body = response.json()
190195
# if for some reason GBF does not return a success response
@@ -218,6 +223,10 @@ def get_order_confirmations(order_numbers):
218223
return None
219224

220225
confirmations = json.loads(data_object["data"])
226+
227+
return _extract_tracking_info(confirmations)
228+
229+
def _extract_tracking_info(confirmations):
221230
tracking_info = {}
222231
if "ShippingConfirmations" in confirmations:
223232
for shipping_confirmation in confirmations['ShippingConfirmations']:

‎track/redcap.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def set_order_number(record_id, order_number):
124124
if r.status_code != HTTPStatus.OK:
125125
logger.error(f'HTTP Status: {r.status_code}')
126126
logger.error(r.json())
127+
raise REDCapError(f"REDCap returned {r.status_code}.")
127128
else:
128129
logger.debug("Succesfully send order number to REDCap.")
129130

@@ -156,7 +157,6 @@ def set_tracking_info(order_objects):
156157
return
157158

158159
for order in order_objects:
159-
160160
# in case an order has not been shipped yet, we don't update REDcap
161161
if not order.ship_date:
162162
continue
@@ -196,6 +196,7 @@ def set_tracking_info(order_objects):
196196
message = r.json()
197197
log_manager.append_to_redcap_log(LogManager.LEVEL_ERROR, message)
198198
logger.error(message)
199+
raise REDCapError(f"REDCap returned {r.status_code}.")
199200
else:
200201
message = f"Succesfully sent tracking information to REDCap for the following records: {[order.record_id for order in order_objects]}."
201202
log_manager.append_to_redcap_log(LogManager.LEVEL_INFO, message)

‎track/tests/__init__.py

Whitespace-only changes.

‎track/tests/test_gbf.py

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import logging
2+
from unittest.mock import patch, MagicMock
3+
from django.test import TestCase, override_settings
4+
import json
5+
6+
import track.gbf as gbf
7+
from track.models import *
8+
9+
logger = logging.getLogger(__name__)
10+
11+
order_json = {
12+
"test": "true",
13+
"orders": [
14+
{
15+
"orderNumber": "EDROP-00014",
16+
"shippingInfo": {
17+
"address": {
18+
"company": "John Doe",
19+
"addressLine1": "1234 Main Street",
20+
"addressLine2": "PO Box 5",
21+
"city": "Phoenix",
22+
"state": "AZ",
23+
"zipCode": "00000",
24+
"country": "United States",
25+
"phone": "1111111111",
26+
"residential": True
27+
},
28+
"shipMethod": "FedEx Ground",
29+
},
30+
"lineItems": [
31+
{
32+
"itemNumber": "111",
33+
"itemQuantity": 1.0,
34+
}
35+
]
36+
}
37+
]
38+
}
39+
40+
address_data = {
41+
"first_name": "John",
42+
"last_name": "Doe",
43+
"street1": "1234 Main Street",
44+
"street2": "PO Box 5",
45+
"city": "Phoenix",
46+
"state": "AZ",
47+
"zip": "00000",
48+
"phone": "1111111111",
49+
}
50+
51+
class OrderResponse:
52+
def __init__(self, status_code, json_data=None):
53+
self.status_code = status_code
54+
self.json_data = json_data
55+
56+
def json(self):
57+
return self.json_data
58+
59+
confirmation_response_json = {
60+
"success": True,
61+
"dataArray": [
62+
{
63+
"format": "json",
64+
"data": "{\r\n \"ShippingConfirmations\": [\r\n {\r\n \"OrderNumber\": \"EDROP-00014\",\r\n \"Shipper\": \"\",\r\n \"ShipVia\": \"FedEx Ground\",\r\n \"ShipDate\": \"2025-01-23\",\r\n \"ClientID\": \"\",\r\n \"Tracking\": [\r\n \"270000004830\"\r\n ],\r\n \"Items\": [\r\n {\r\n \"ItemNumber\": \"K-BAN-001\",\r\n \"SerialNumber\": \"EV-05FCSG\",\r\n \"ShippedQty\": 1,\r\n \"ReturnTracking\": [\r\n \"XXXXXXXXXXXX\"\r\n ],\r\n \"TubeSerial\": [\r\n \"SIHIRJT5786\"\r\n ]\r\n }\r\n ]\r\n }\r\n ]\r\n}"
65+
}
66+
]
67+
}
68+
69+
tracking_info = {
70+
'EDROP-00014': {
71+
'date_kit_shipped': '2025-01-23',
72+
'kit_tracking_n': ['270000004830'],
73+
'return_tracking_n': ['XXXXXXXXXXXX'],
74+
'tube_serial_n': ['SIHIRJT5786']
75+
}
76+
}
77+
78+
@override_settings(GBF_URL="http://host.docker.internal:3000/")
79+
class TestGBF(TestCase):
80+
def setUp(self):
81+
self.mock_order_number = "EDROP-00014"
82+
self.mock_order_json = order_json
83+
self.mock_order_response = OrderResponse(200, {'success': True, 'message': 'EXM-0000XX_RDQYD_20250115_154237.xml'})
84+
self.order_object = Order.objects.create(pk=14, project_id=1, order_number=None)
85+
self.address_data = address_data
86+
self.order_response_json = {'success': True, 'message': 'EXM-0000XX_RDQYD_20250115_154237.xml'}
87+
self.order_numbers = ["EDROP-00014", "EDROP-00015"]
88+
self.confirmation_response_json = confirmation_response_json
89+
self.tracking_info = tracking_info
90+
91+
@patch("track.gbf._place_order_with_GBF")
92+
@patch("track.gbf._generate_order_json")
93+
@patch("track.gbf._generate_order_number")
94+
def test_create_order(self, mock_generate_order_number, mock_generate_order_json, mock_place_order_with_GBF):
95+
mock_generate_order_number.return_value = self.mock_order_number
96+
mock_generate_order_json.return_value = self.mock_order_json
97+
mock_place_order_with_GBF.side_effect = lambda json, order_number: self.mock_order_response
98+
99+
result = gbf.create_order(self.order_object, self.address_data)
100+
101+
updated_order_object = Order.objects.filter(pk=self.order_object.id).first()
102+
103+
self.assertEqual(updated_order_object.order_number, "EDROP-00014")
104+
self.assertEqual(result, True)
105+
106+
logger.debug(f'Order {updated_order_object.order_number} was successfully created.')
107+
108+
def test_generate_order_number(self):
109+
result = gbf._generate_order_number(self.order_object)
110+
self.assertEqual(result, "EDROP-00014")
111+
112+
def test_generate_order_json(self):
113+
self.order_object.order_number = "EDROP-00014"
114+
115+
result = gbf._generate_order_json(self.order_object, self.address_data)
116+
result_data = json.loads(result)
117+
118+
self.assertIn("test", result_data)
119+
self.assertIn("orders", result_data)
120+
self.assertIn("orderNumber", result_data['orders'][0])
121+
self.assertIn("shippingInfo", result_data['orders'][0])
122+
self.assertIn("lineItems", result_data['orders'][0])
123+
self.assertIn("address", result_data['orders'][0]['shippingInfo'])
124+
self.assertIn("shipMethod", result_data['orders'][0]['shippingInfo'])
125+
self.assertEqual(result_data['orders'][0]['orderNumber'], "EDROP-00014")
126+
self.assertEqual(result_data['orders'][0]['shippingInfo']['address']['company'], "John Doe")
127+
self.assertEqual(result_data['orders'][0]['shippingInfo']['address']['zipCode'], "00000")
128+
129+
@patch("track.gbf.requests.post")
130+
def test_place_order_with_GBF_success(self, mock_request):
131+
mock_response = MagicMock()
132+
mock_response.status_code = 200
133+
mock_response.json.return_value = self.order_response_json
134+
mock_request.return_value = mock_response
135+
136+
result = gbf._place_order_with_GBF(self.mock_order_json, self.mock_order_number)
137+
result_body = result.json()
138+
139+
mock_request.assert_called_once()
140+
self.assertEqual(result.status_code, 200)
141+
self.assertIn("success", result_body)
142+
self.assertIn("message", result_body)
143+
self.assertEqual(result_body["success"], True)
144+
145+
logger.debug(f'Order {self.mock_order_json['orders'][0]['orderNumber']} was successfully placed.')
146+
147+
@patch("track.gbf.requests.post")
148+
def test_place_order_with_GBF_failure(self, mock_request):
149+
mock_response = MagicMock()
150+
mock_response.status_code = 400
151+
mock_response.json.return_value = {"success": False, "error": "Bad Request"}
152+
mock_request.return_value = mock_response
153+
154+
result = gbf._place_order_with_GBF(self.mock_order_json, self.mock_order_number)
155+
156+
mock_request.assert_called_once()
157+
self.assertEqual(result.status_code, 400)
158+
159+
logger.error('The order was unable to be placed due to a bad request.')
160+
161+
@patch("track.gbf._extract_tracking_info")
162+
@patch("track.gbf.requests.post")
163+
def test_get_order_confirmations_success(self, mock_request, mock_extract_tracking_info):
164+
mock_response = MagicMock()
165+
mock_response.status_code = 200
166+
mock_response.raise_for_status.return_value = None
167+
mock_response.json.return_value = self.confirmation_response_json
168+
mock_request.return_value = mock_response
169+
mock_extract_tracking_info.return_value = self.tracking_info
170+
171+
result = gbf.get_order_confirmations(self.order_numbers)
172+
173+
mock_request.assert_called_once()
174+
mock_extract_tracking_info.assert_called_once()
175+
self.assertIn("EDROP-00014", result)
176+
self.assertIn("2025-01-23", result['EDROP-00014']['date_kit_shipped'])
177+
self.assertIn("270000004830", result['EDROP-00014']['kit_tracking_n'])
178+
self.assertIn('XXXXXXXXXXXX', result['EDROP-00014']['return_tracking_n'])
179+
self.assertIn('SIHIRJT5786', result['EDROP-00014']['tube_serial_n'])
180+
181+
logger.debug(f'The following order numbers were successfully checked for order confirmation: {self.order_numbers}')
182+
183+
@patch("track.gbf.requests.post")
184+
def test_get_order_confirmations_failure(self, mock_request):
185+
mock_response = MagicMock()
186+
mock_response.status_code = 400
187+
mock_response.json.return_value = {"success": False, "error": "Bad Request"}
188+
mock_request.return_value = mock_response
189+
190+
result = gbf.get_order_confirmations(self.order_numbers)
191+
192+
mock_request.assert_called_once()
193+
self.assertEqual(result, None)
194+
195+
logger.error('The order confirmation failed due to a bad request.')

‎track/tests/test_orders.py

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import logging
2+
from django.test import TestCase, override_settings
3+
from unittest.mock import patch, MagicMock
4+
from track.models import Order
5+
from track.orders import (
6+
place_order,
7+
store_order_number_in_redcap,
8+
check_orders_shipping_info,
9+
_update_orders_with_shipping_info
10+
)
11+
12+
# Create a logger for this test module.
13+
logger = logging.getLogger(__name__)
14+
15+
# Override the setting used in orders to check for record completeness.
16+
@override_settings(REDCAP_FIELD_TO_BE_COMPLETE="contact_complete")
17+
class TestOrders(TestCase):
18+
def setUp(self):
19+
# Setup common test parameters.
20+
self.record_id = "123"
21+
self.project_id = "proj_1"
22+
self.project_url = "http://example.com/project"
23+
logger.debug("TestOrders: setUp complete with record_id=%s, project_id=%s", self.record_id, self.project_id)
24+
25+
@patch("track.orders.redcap.get_record_info")
26+
def test_place_order_incomplete_record(self, mock_get_record_info):
27+
"""
28+
Test that if the REDCap record does not have the complete flag (i.e. contact_complete != '2'),
29+
place_order returns None.
30+
"""
31+
# Simulate a record with incomplete data.
32+
logger.debug("Running test_place_order_incomplete_record: Simulating incomplete REDCap record.")
33+
mock_get_record_info.return_value = {"contact_complete": "1"}
34+
35+
# Attempt to place the order.
36+
order = place_order(self.record_id, self.project_id, self.project_url)
37+
logger.debug("Order placement result for incomplete record: %s", order)
38+
39+
# Expect no order to be created.
40+
self.assertIsNone(order)
41+
42+
@patch("track.orders.gbf.create_order")
43+
@patch("track.orders.redcap.set_order_number")
44+
@patch("track.orders.redcap.get_record_info")
45+
def test_place_order_success(self, mock_get_record_info, mock_set_order_number, mock_create_order):
46+
"""
47+
Test that when the record is complete (contact_complete == '2') and GBF order creation
48+
succeeds, place_order creates an Order with status INITIATED and calls redcap.set_order_number.
49+
"""
50+
logger.debug("Running test_place_order_success: Simulating complete REDCap record and successful GBF order creation.")
51+
# Provide complete address data.
52+
address_data = {
53+
"contact_complete": "2",
54+
"first_name": "John",
55+
"last_name": "Doe",
56+
"street_1": "742 Evergreen Terrace",
57+
"street_2": "",
58+
"city": "Springfield",
59+
"state": "IL",
60+
"zip": "62704",
61+
}
62+
mock_get_record_info.return_value = address_data
63+
64+
# Define a fake create_order that simulates generating an order number.
65+
def fake_create_order(order, address_data):
66+
logger.debug("fake_create_order: Generating order number for order with record_id=%s", order.record_id)
67+
order.order_number = "EDROP-00003"
68+
order.save()
69+
return True
70+
71+
# Set side effect to simulate a successful order creation.
72+
mock_create_order.side_effect = fake_create_order
73+
74+
# Place the order.
75+
order = place_order(self.record_id, self.project_id, self.project_url)
76+
logger.debug("Order created: %s", order)
77+
78+
# Verify that an order was created and its status is INITIATED.
79+
self.assertIsNotNone(order)
80+
self.assertEqual(order.order_status, Order.INITIATED)
81+
mock_create_order.assert_called_once()
82+
logger.debug("GBF.create_order was called successfully.")
83+
84+
# Verify that redcap.set_order_number is called with the correct parameters.
85+
mock_set_order_number.assert_called_once_with(self.record_id, "EDROP-00003")
86+
logger.debug("redcap.set_order_number was called with record_id=%s and order_number=%s", self.record_id, "EDROP-00003")
87+
88+
@patch("track.orders.gbf.create_order")
89+
@patch("track.orders.redcap.set_order_number")
90+
@patch("track.orders.redcap.get_record_info")
91+
def test_place_order_failure_gbf(self, mock_get_record_info, mock_set_order_number, mock_create_order):
92+
"""
93+
Test that if GBF fails to create the order (returns False), place_order creates the Order
94+
but then resets its status back to PENDING and does not call redcap.set_order_number.
95+
"""
96+
logger.debug("Running test_place_order_failure_gbf: Simulating complete record but GBF order creation failure.")
97+
address_data = {
98+
"contact_complete": "2",
99+
"first_name": "John",
100+
"last_name": "Doe",
101+
"street_1": "742 Evergreen Terrace",
102+
"street_2": "",
103+
"city": "Springfield",
104+
"state": "IL",
105+
"zip": "62704",
106+
}
107+
mock_get_record_info.return_value = address_data
108+
mock_create_order.return_value = False # Simulate failure from GBF
109+
110+
order = place_order(self.record_id, self.project_id, self.project_url)
111+
logger.debug("Order created with GBF failure: %s", order)
112+
113+
self.assertIsNotNone(order)
114+
self.assertEqual(order.order_status, Order.PENDING)
115+
mock_set_order_number.assert_not_called()
116+
logger.debug("redcap.set_order_number was not called due to GBF failure.")
117+
118+
@patch("track.orders.redcap.set_order_number")
119+
def test_store_order_number_in_redcap(self, mock_set_order_number):
120+
"""
121+
Test that store_order_number_in_redcap calls redcap.set_order_number with the correct record_id
122+
and order.order_number.
123+
"""
124+
logger.debug("Running test_store_order_number_in_redcap.")
125+
order = Order.objects.create(
126+
record_id=self.record_id,
127+
project_id=self.project_id,
128+
project_url=self.project_url,
129+
order_status=Order.INITIATED,
130+
order_number="EDROP-00001"
131+
)
132+
store_order_number_in_redcap(self.record_id, order)
133+
mock_set_order_number.assert_called_once_with(self.record_id, order.order_number)
134+
logger.debug("redcap.set_order_number was called with record_id=%s and order_number=%s", self.record_id, order.order_number)
135+
136+
@patch("track.orders.redcap.set_tracking_info")
137+
@patch("track.orders.gbf.get_order_confirmations")
138+
def test_check_orders_shipping_info(self, mock_get_order_confirmations, mock_set_tracking_info):
139+
"""
140+
Test that check_orders_shipping_info:
141+
- Retrieves order numbers for orders with status INITIATED.
142+
- Calls gbf.get_order_confirmations with the list of order numbers.
143+
- Updates the order with the shipping info.
144+
- Calls redcap.set_tracking_info with the updated orders.
145+
"""
146+
logger.debug("Running test_check_orders_shipping_info: Creating order with status INITIATED.")
147+
# Create an order with status INITIATED.
148+
order = Order.objects.create(
149+
record_id=self.record_id,
150+
project_id=self.project_id,
151+
project_url=self.project_url,
152+
order_status=Order.INITIATED,
153+
order_number="EDROP-00002"
154+
)
155+
# Simulate tracking info returned from GBF.
156+
tracking_info = {
157+
"EDROP-00002": {
158+
"date_kit_shipped": "2025-03-01",
159+
"kit_tracking_n": ["TRACK123"],
160+
"return_tracking_n": ["RET123"],
161+
"tube_serial_n": ["TUBE123"]
162+
}
163+
}
164+
mock_get_order_confirmations.return_value = tracking_info
165+
166+
# Execute the function to check shipping info.
167+
check_orders_shipping_info()
168+
logger.debug("check_orders_shipping_info executed.")
169+
170+
updated_order = Order.objects.get(order_number="EDROP-00002")
171+
self.assertEqual(updated_order.ship_date, "2025-03-01")
172+
self.assertEqual(updated_order.order_status, Order.SHIPPED)
173+
self.assertEqual(updated_order.tracking_nrs, ["TRACK123"])
174+
self.assertEqual(updated_order.return_tracking_nrs, ["RET123"])
175+
self.assertEqual(updated_order.tube_serials, ["TUBE123"])
176+
logger.debug("Order updated with shipping info: %s", updated_order)
177+
178+
# Verify that redcap.set_tracking_info was called with the updated order.
179+
mock_set_tracking_info.assert_called_once()
180+
# Verify that gbf.get_order_confirmations was called with the correct order number list.
181+
mock_get_order_confirmations.assert_called_once_with(["EDROP-00002"])
182+
logger.debug("gbf.get_order_confirmations and redcap.set_tracking_info were called as expected.")
183+
184+
def test_update_orders_with_shipping_info(self):
185+
"""
186+
Test that _update_orders_with_shipping_info updates an order's shipping fields correctly
187+
and returns a list of order numbers that have been updated.
188+
"""
189+
logger.debug("Running test_update_orders_with_shipping_info.")
190+
order = Order.objects.create(
191+
record_id=self.record_id,
192+
project_id=self.project_id,
193+
project_url=self.project_url,
194+
order_status=Order.INITIATED,
195+
order_number="EDROP-00003"
196+
)
197+
tracking_info = {
198+
"EDROP-00003": {
199+
"date_kit_shipped": "2025-04-01",
200+
"kit_tracking_n": ["TRACK999"],
201+
"return_tracking_n": ["RET999"],
202+
"tube_serial_n": ["TUBE999"]
203+
}
204+
}
205+
shipped_orders = _update_orders_with_shipping_info(tracking_info)
206+
logger.debug("Shipped orders returned: %s", shipped_orders)
207+
208+
self.assertIn("EDROP-00003", shipped_orders)
209+
updated_order = Order.objects.get(order_number="EDROP-00003")
210+
self.assertEqual(updated_order.ship_date, "2025-04-01")
211+
self.assertEqual(updated_order.order_status, Order.SHIPPED)
212+
self.assertEqual(updated_order.tracking_nrs, ["TRACK999"])
213+
self.assertEqual(updated_order.return_tracking_nrs, ["RET999"])
214+
self.assertEqual(updated_order.tube_serials, ["TUBE999"])
215+
logger.debug("Order %s successfully updated with shipping info.", updated_order.order_number)

‎track/tests/test_redcap.py

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import json
2+
import logging
3+
from unittest.mock import patch, MagicMock
4+
from django.test import TestCase, override_settings
5+
from django.conf import settings
6+
7+
from track.models import Order
8+
from track.redcap import (
9+
get_record_info,
10+
set_order_number,
11+
set_tracking_info
12+
)
13+
from track.exceptions import REDCapError
14+
15+
# Set up a logger for this module.
16+
logger = logging.getLogger(__name__)
17+
18+
# Override the REDCAP_URL setting to use a test URL instead of the real one.
19+
# This ensures we don't make actual HTTP requests to REDCap during testing.
20+
@override_settings(REDCAP_URL="http://testurl")
21+
class TestRedcapFunctions(TestCase):
22+
23+
def setUp(self):
24+
logger.debug("TestRedcapFunctions: Setting up test data.")
25+
# Common test data.
26+
self.record_id = "123"
27+
self.order_number = "ORD-456"
28+
self.mock_record_data = [{
29+
"record_id": "123",
30+
"first_name": "John",
31+
"last_name": "Doe",
32+
"city": "Springfield",
33+
"state": "IL",
34+
"zip": "62704",
35+
"street_1": "742 Evergreen Terrace",
36+
"street_2": "",
37+
"consent_complete": "2",
38+
"contact_complete": "2"
39+
}]
40+
41+
# Create an Order in the DB for shipping tests.
42+
self.order_for_shipping = Order.objects.create(
43+
project_id="ABC123",
44+
record_id=self.record_id,
45+
order_status=Order.INITIATED,
46+
ship_date="2025-02-14",
47+
tracking_nrs=["1Z12345", "1Z67890"],
48+
return_tracking_nrs=["999999"],
49+
tube_serials=["TUBE-001", "TUBE-002"]
50+
)
51+
logger.debug("TestRedcapFunctions: Created order_for_shipping with record_id=%s", self.record_id)
52+
53+
@patch("track.redcap.requests.post")
54+
def test_get_record_info_success(self, mock_post):
55+
"""
56+
Test get_record_info returns a dict when the request is successful (HTTP 200).
57+
"""
58+
logger.debug("Running test_get_record_info_success.")
59+
# Configure the mock response for a successful POST request.
60+
mock_response = MagicMock()
61+
mock_response.status_code = 200
62+
mock_response.json.return_value = self.mock_record_data
63+
mock_post.return_value = mock_response
64+
65+
result = get_record_info(self.record_id)
66+
logger.debug("get_record_info returned: %s", result)
67+
68+
# Verify the function returns the expected data.
69+
self.assertIsNotNone(result)
70+
self.assertEqual(result["record_id"], "123")
71+
self.assertEqual(result["first_name"], "John")
72+
self.assertEqual(result["last_name"], "Doe")
73+
74+
# Inspect the positional arguments of the POST call.
75+
args, kwargs = mock_post.call_args
76+
logger.debug("POST call args: %s, kwargs: %s", args, kwargs)
77+
# The first positional argument should be the URL.
78+
self.assertEqual(args[0], settings.REDCAP_URL)
79+
# The POST call should include a 'data' parameter.
80+
self.assertIn("data", kwargs)
81+
self.assertIn("records[0]", kwargs["data"])
82+
self.assertEqual(kwargs["data"]["records[0]"], self.record_id)
83+
logger.debug("test_get_record_info_success completed successfully.")
84+
85+
@patch("track.redcap.requests.post")
86+
def test_get_record_info_failure(self, mock_post):
87+
"""
88+
Test get_record_info raises REDCapError if the response is not HTTP 200.
89+
"""
90+
logger.debug("Running test_get_record_info_failure.")
91+
# Configure a failed response.
92+
mock_response = MagicMock()
93+
mock_response.status_code = 500
94+
mock_response.json.return_value = {"error": "Internal Server Error"}
95+
mock_post.return_value = mock_response
96+
97+
# Test should raise REDCapError
98+
with self.assertRaises(REDCapError) as context:
99+
get_record_info(self.record_id)
100+
101+
self.assertEqual(str(context.exception), "REDCap returned 500.")
102+
logger.debug("test_get_record_info_failure completed successfully.")
103+
104+
@patch("track.redcap.requests.post")
105+
def test_set_order_number_success(self, mock_post):
106+
"""
107+
Test set_order_number logs success on a 200 response.
108+
"""
109+
logger.debug("Running test_set_order_number_success.")
110+
# Configure a successful response.
111+
mock_response = MagicMock()
112+
mock_response.status_code = 200
113+
mock_response.json.return_value = {"count": 1}
114+
mock_post.return_value = mock_response
115+
116+
set_order_number(self.record_id, self.order_number)
117+
logger.debug("set_order_number called with record_id=%s and order_number=%s", self.record_id, self.order_number)
118+
119+
# Verify that the POST call was made with the correct dummy URL.
120+
args, kwargs = mock_post.call_args
121+
logger.debug("POST call for set_order_number args: %s, kwargs: %s", args, kwargs)
122+
self.assertEqual(args[0], settings.REDCAP_URL)
123+
self.assertIn("data", kwargs)
124+
125+
# The XML payload should contain the record_id and kit_order_n.
126+
xml_payload = kwargs["data"]["data"]
127+
logger.debug("XML payload for set_order_number: %s", xml_payload)
128+
self.assertIn(f"<record_id>{self.record_id}</record_id>", xml_payload)
129+
self.assertIn(f"<kit_order_n>{self.order_number}</kit_order_n>", xml_payload)
130+
logger.debug("test_set_order_number_success completed successfully.")
131+
132+
@patch("track.redcap.requests.post")
133+
def test_set_order_number_failure(self, mock_post):
134+
"""
135+
Test set_order_number raises REDCapError on a non-200 response.
136+
"""
137+
logger.debug("Running test_set_order_number_failure.")
138+
mock_response = MagicMock()
139+
mock_response.status_code = 400
140+
mock_response.json.return_value = {"error": "Bad Request"}
141+
mock_post.return_value = mock_response
142+
143+
with self.assertRaises(REDCapError) as context:
144+
set_order_number(self.record_id, self.order_number)
145+
146+
self.assertEqual(str(context.exception), "REDCap returned 400.")
147+
mock_post.assert_called_once()
148+
logger.debug("test_set_order_number_failure completed successfully.")
149+
150+
@patch("track.redcap.requests.post")
151+
def test_set_tracking_info_success(self, mock_post):
152+
"""
153+
Test set_tracking_info builds correct XML and sends to REDCap.
154+
"""
155+
logger.debug("Running test_set_tracking_info_success.")
156+
mock_response = MagicMock()
157+
mock_response.status_code = 200
158+
mock_response.json.return_value = {"count": 1}
159+
mock_post.return_value = mock_response
160+
161+
# Use only orders with a ship_date.
162+
orders = Order.objects.filter(ship_date__isnull=False)
163+
logger.debug("Orders used for tracking info: %s", list(orders))
164+
set_tracking_info(orders)
165+
166+
# Verify the POST call.
167+
args, kwargs = mock_post.call_args
168+
logger.debug("POST call for set_tracking_info args: %s, kwargs: %s", args, kwargs)
169+
xml_payload = kwargs["data"]["data"]
170+
171+
# Check that the XML payload includes the correct order details.
172+
self.assertIn(f"<record_id>{self.record_id}</record_id>", xml_payload)
173+
self.assertIn("<kit_tracking_n>1Z12345, 1Z67890</kit_tracking_n>", xml_payload)
174+
self.assertIn("<kit_tracking_return_n>999999</kit_tracking_return_n>", xml_payload)
175+
self.assertIn("<tubeserial>TUBE-001, TUBE-002</tubeserial>", xml_payload)
176+
self.assertIn("<kit_status>TRN</kit_status>", xml_payload)
177+
logger.debug("test_set_tracking_info_success completed successfully.")
178+
179+
@patch("track.redcap.requests.post")
180+
def test_set_tracking_info_no_orders(self, mock_post):
181+
"""
182+
Test set_tracking_info does nothing if no shipped orders are provided.
183+
"""
184+
logger.debug("Running test_set_tracking_info_no_orders: Passing an empty list.")
185+
# Passing an empty list should result in no HTTP request.
186+
set_tracking_info([])
187+
mock_post.assert_not_called()
188+
logger.debug("test_set_tracking_info_no_orders completed successfully.")
189+
190+
@patch("track.redcap.requests.post")
191+
def test_set_tracking_info_failure(self, mock_post):
192+
"""
193+
Test set_tracking_info raises REDCapError on a non-200 response.
194+
"""
195+
logger.debug("Running test_set_tracking_info_failure.")
196+
mock_response = MagicMock()
197+
mock_response.status_code = 400
198+
mock_response.json.return_value = {"error": "Bad Request"}
199+
mock_post.return_value = mock_response
200+
201+
orders = Order.objects.filter(ship_date__isnull=False)
202+
with self.assertRaises(REDCapError) as context:
203+
set_tracking_info(orders)
204+
205+
self.assertEqual(str(context.exception), "REDCap returned 400.")
206+
mock_post.assert_called_once()
207+
logger.debug("test_set_tracking_info_failure completed successfully.")
File renamed without changes.

0 commit comments

Comments
 (0)
Please sign in to comment.