1
1
import os
2
2
import uuid
3
3
from datetime import datetime , timedelta
4
+ from io import BytesIO
4
5
5
6
import pytest
6
7
from dotenv import load_dotenv
@@ -129,6 +130,47 @@ async def test_list_virtual_cards(self, extend):
129
130
for card in response ["virtualCards" ]:
130
131
assert card ["status" ] == "CLOSED"
131
132
133
+ @pytest .mark .asyncio
134
+ async def test_list_virtual_cards_with_sorting (self , extend ):
135
+ """Test listing virtual cards with various sorting options"""
136
+
137
+ # Test sorting by display name ascending
138
+ asc_response = await extend .virtual_cards .get_virtual_cards (
139
+ sort_field = "displayName" ,
140
+ sort_direction = "ASC" ,
141
+ per_page = 50 # Ensure we get enough cards to compare
142
+ )
143
+
144
+ # Test sorting by display name descending
145
+ desc_response = await extend .virtual_cards .get_virtual_cards (
146
+ sort_field = "displayName" ,
147
+ sort_direction = "DESC" ,
148
+ per_page = 50 # Ensure we get enough cards to compare
149
+ )
150
+
151
+ # Verify responses contain cards
152
+ assert "virtualCards" in asc_response
153
+ assert "virtualCards" in desc_response
154
+
155
+ # If sufficient cards exist, just verify the orders are different
156
+ # rather than trying to implement our own sorting logic
157
+ if len (asc_response ["virtualCards" ]) > 1 and len (desc_response ["virtualCards" ]) > 1 :
158
+ asc_ids = [card ["id" ] for card in asc_response ["virtualCards" ]]
159
+ desc_ids = [card ["id" ] for card in desc_response ["virtualCards" ]]
160
+
161
+ # Verify the orders are different for different sort directions
162
+ assert asc_ids != desc_ids , "ASC and DESC sorting should produce different results"
163
+
164
+ # Test other sort fields
165
+ for field in ["createdAt" , "updatedAt" , "balanceCents" , "status" , "type" ]:
166
+ # Test both directions for each field
167
+ for direction in ["ASC" , "DESC" ]:
168
+ response = await extend .virtual_cards .get_virtual_cards (
169
+ sort_field = field ,
170
+ sort_direction = direction
171
+ )
172
+ assert "virtualCards" in response , f"Sorting by { field } { direction } should return virtual cards"
173
+
132
174
133
175
@pytest .mark .integration
134
176
class TestTransactions :
@@ -152,6 +194,54 @@ async def test_list_transactions(self, extend):
152
194
for field in required_fields :
153
195
assert field in transaction , f"Transaction should contain '{ field } ' field"
154
196
197
+ @pytest .mark .asyncio
198
+ async def test_list_transactions_with_sorting (self , extend ):
199
+ """Test listing transactions with various sorting options"""
200
+
201
+ # Define sort fields - positive for ASC, negative (prefixed with -) for DESC
202
+ sort_fields = [
203
+ "recipientName" , "-recipientName" ,
204
+ "merchantName" , "-merchantName" ,
205
+ "amount" , "-amount" ,
206
+ "date" , "-date"
207
+ ]
208
+
209
+ # Test each sort field
210
+ for sort_field in sort_fields :
211
+ # Get transactions with this sort
212
+ response = await extend .transactions .get_transactions (
213
+ sort_field = sort_field ,
214
+ per_page = 10
215
+ )
216
+
217
+ # Verify response contains transactions and basic structure
218
+ assert isinstance (response , dict ), f"Response for sort { sort_field } should be a dictionary"
219
+ assert "transactions" in response , f"Response for sort { sort_field } should contain 'transactions' key"
220
+
221
+ # If we have enough data, test opposite sort direction for comparison
222
+ if len (response ["transactions" ]) > 1 :
223
+ # Determine the field name and opposite sort field
224
+ is_desc = sort_field .startswith ("-" )
225
+ field_name = sort_field [1 :] if is_desc else sort_field
226
+ opposite_sort = field_name if is_desc else f"-{ field_name } "
227
+
228
+ # Get transactions with opposite sort
229
+ opposite_response = await extend .transactions .get_transactions (
230
+ sort_field = opposite_sort ,
231
+ per_page = 10
232
+ )
233
+
234
+ # Get IDs in both sort orders for comparison
235
+ sorted_ids = [tx ["id" ] for tx in response ["transactions" ]]
236
+ opposite_sorted_ids = [tx ["id" ] for tx in opposite_response ["transactions" ]]
237
+
238
+ # If we have the same set of transactions in both responses,
239
+ # verify that different sort directions produce different orders
240
+ if set (sorted_ids ) == set (opposite_sorted_ids ) and len (sorted_ids ) > 1 :
241
+ assert sorted_ids != opposite_sorted_ids , (
242
+ f"Different sort directions for { field_name } should produce different results"
243
+ )
244
+
155
245
156
246
@pytest .mark .integration
157
247
class TestRecurringCards :
@@ -303,6 +393,98 @@ async def test_get_expense_categories_and_labels(self, extend):
303
393
assert "expenseLabels" in labels
304
394
305
395
396
+ @pytest .mark .integration
397
+ class TestTransactionExpenseData :
398
+ """Integration tests for updating transaction expense data using a specific expense category and label"""
399
+
400
+ @pytest .mark .asyncio
401
+ async def test_update_transaction_expense_data_with_specific_category_and_label (self , extend ):
402
+ """Test updating the expense data for a transaction using a specific expense category and label."""
403
+ # Retrieve available expense categories (active ones)
404
+ categories_response = await extend .expense_data .get_expense_categories (active = True )
405
+ assert "expenseCategories" in categories_response , "Response should include 'expenseCategories'"
406
+ expense_categories = categories_response ["expenseCategories" ]
407
+ assert expense_categories , "No expense categories available for testing"
408
+
409
+ # For this test, pick the first expense category
410
+ category = expense_categories [0 ]
411
+ category_id = category ["id" ]
412
+
413
+ # Retrieve the labels for the chosen expense category
414
+ labels_response = await extend .expense_data .get_expense_category_labels (
415
+ category_id = category_id ,
416
+ page = 0 ,
417
+ per_page = 10
418
+ )
419
+ assert "expenseLabels" in labels_response , "Response should include 'expenseLabels'"
420
+ expense_labels = labels_response ["expenseLabels" ]
421
+ assert expense_labels , "No expense labels available for the selected category"
422
+
423
+ # Pick the first label from the list
424
+ label = expense_labels [0 ]
425
+ label_id = label ["id" ]
426
+
427
+ # Retrieve at least one transaction to update expense data
428
+ transactions_response = await extend .transactions .get_transactions (per_page = 1 )
429
+ assert transactions_response .get ("transactions" ), "No transactions available for testing expense data update"
430
+ transaction = transactions_response ["transactions" ][0 ]
431
+ transaction_id = transaction ["id" ]
432
+
433
+ # Prepare the expense data payload with the specific category and label
434
+ update_payload = {
435
+ "expenseDetails" : [
436
+ {
437
+ "categoryId" : category_id ,
438
+ "labelId" : label_id
439
+ }
440
+ ]
441
+ }
442
+
443
+ # Call the update_transaction_expense_data method
444
+ response = await extend .transactions .update_transaction_expense_data (transaction_id , update_payload )
445
+
446
+ # Verify the response contains the transaction id and expected expense details
447
+ assert "id" in response , "Response should include the transaction id"
448
+ if "expenseDetails" in response :
449
+ # Depending on the API response, the structure might vary; adjust assertions accordingly
450
+ assert response ["expenseDetails" ] == update_payload ["expenseDetails" ], (
451
+ "Expense details in the response should match the update payload"
452
+ )
453
+
454
+
455
+ @pytest .mark .integration
456
+ class TestReceiptAttachments :
457
+ """Integration tests for receipt attachment operations"""
458
+
459
+ @pytest .mark .asyncio
460
+ async def test_create_receipt_attachment (self , extend ):
461
+ """Test creating a receipt attachment via multipart upload."""
462
+ # Create a dummy PNG file in memory
463
+ # This is a minimal PNG header plus extra bytes to simulate file content.
464
+ png_header = b'\x89 PNG\r \n \x1a \n '
465
+ dummy_content = png_header + b'\x00 ' * 100
466
+ file_obj = BytesIO (dummy_content )
467
+ # Optionally set a name attribute for file identification in the upload
468
+ file_obj .name = f"test_receipt_{ uuid .uuid4 ()} .png"
469
+
470
+ # Retrieve a valid transaction id from existing transactions
471
+ transactions_response = await extend .transactions .get_transactions (page = 0 , per_page = 1 )
472
+ assert transactions_response .get ("transactions" ), "No transactions available for testing receipt attachment"
473
+ transaction_id = transactions_response ["transactions" ][0 ]["id" ]
474
+
475
+ # Call the receipt attachment upload method
476
+ response = await extend .receipt_attachments .create_receipt_attachment (
477
+ transaction_id = transaction_id ,
478
+ file = file_obj
479
+ )
480
+
481
+ # Assert that the response contains expected keys
482
+ assert "id" in response , "Receipt attachment should have an id"
483
+ assert "urls" in response , "Receipt attachment should include urls"
484
+ assert "contentType" in response , "Receipt attachment should include a content type"
485
+ assert response ["contentType" ] == "image/png" , "Content type should be 'image/png'"
486
+
487
+
306
488
def test_environment_variables ():
307
489
"""Test that required environment variables are set"""
308
490
assert os .getenv ("EXTEND_API_KEY" ), "EXTEND_API_KEY environment variable is required"
0 commit comments