-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathtest_file_attachments.py
More file actions
582 lines (474 loc) · 23.1 KB
/
Copy pathtest_file_attachments.py
File metadata and controls
582 lines (474 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
"""Pytest tests for the File Attachments API.
These tests demonstrate and validate the usage of the File Attachments API including:
- Basic file attachment operations (get, list, upload, download)
- File attachment filtering by entity
- File attachment updates and deletion
- Error handling and edge cases
"""
import os
import tempfile
from pathlib import Path
import pytest
import pytest_asyncio
from sift_client import SiftClient
from sift_client.resources import FileAttachmentsAPI, FileAttachmentsAPIAsync
from sift_client.sift_types.file_attachment import (
FileAttachment,
FileAttachmentUpdate,
RemoteFileEntityType,
)
pytestmark = pytest.mark.integration
def test_client_binding(sift_client):
"""Test that file_attachments API is properly bound to the client."""
assert sift_client.file_attachments
assert isinstance(sift_client.file_attachments, FileAttachmentsAPI)
assert sift_client.async_.file_attachments
assert isinstance(sift_client.async_.file_attachments, FileAttachmentsAPIAsync)
@pytest.fixture
def file_attachments_api_async(sift_client: SiftClient):
"""Get the async file attachments API instance."""
return sift_client.async_.file_attachments
@pytest.fixture
def file_attachments_api_sync(sift_client: SiftClient):
"""Get the synchronous file attachments API instance."""
return sift_client.file_attachments
@pytest.fixture
def test_run(sift_client: SiftClient):
"""Get a test run to attach files to."""
runs = sift_client.runs.list_(limit=1)
if runs:
return runs[0]
pytest.fail("No runs available for testing, please add test test runs")
@pytest.fixture
def test_asset(sift_client: SiftClient):
"""Get a test asset to attach files to."""
assets = sift_client.assets.list_(limit=1)
if assets:
return assets[0]
pytest.fail("No assets available for testing, please add test test assets")
@pytest_asyncio.fixture
async def uploaded_file_attachment(file_attachments_api_async, test_run):
"""Upload a test file and return the file attachment, cleaning up after test."""
# Create a temporary test file
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("Test file content for integration tests\n")
tmp.write("This file should be deleted after the test\n")
tmp_path = tmp.name
try:
# Upload the file
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_run,
description="Integration test file attachment",
)
yield file_attachment
# Cleanup: delete the uploaded file
try:
await file_attachments_api_async.delete(file_attachments=file_attachment)
except Exception:
pass # If deletion fails, it's okay for test cleanup
finally:
# Cleanup: delete the temporary local file
if os.path.exists(tmp_path):
os.unlink(tmp_path)
class TestFileAttachmentsAPIAsync:
"""Test suite for the async File Attachments API functionality."""
class TestUpload:
"""Tests for the async upload method."""
@pytest.mark.asyncio
async def test_upload_to_run(self, file_attachments_api_async, test_run):
"""Test uploading a file attachment to a run."""
# Create a temporary test file
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("Test file content\n")
tmp_path = tmp.name
try:
# Upload the file
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_run,
description="Test upload to run",
)
# Verify the upload
assert isinstance(file_attachment, FileAttachment)
assert file_attachment.id_ is not None
assert file_attachment.file_name is not None
assert file_attachment.description == "Test upload to run"
assert file_attachment.entity_id == test_run.id_
# Cleanup: delete the uploaded file
await file_attachments_api_async.delete(file_attachments=file_attachment)
finally:
# Cleanup: delete the temporary local file
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_upload_to_asset(self, file_attachments_api_async, test_asset):
"""Test uploading a file attachment to an asset."""
# Create a temporary test file
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as tmp:
tmp.write("col1,col2,col3\n")
tmp.write("1,2,3\n")
tmp_path = tmp.name
try:
# Upload the file
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_asset,
description="Test CSV upload to asset",
metadata={"test_key": "test_value"},
)
# Verify the upload
assert isinstance(file_attachment, FileAttachment)
assert file_attachment.id_ is not None
assert file_attachment.entity_id == test_asset.id_
assert file_attachment.description == "Test CSV upload to asset"
# Cleanup
await file_attachments_api_async.delete(file_attachments=file_attachment)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_upload_with_pathlib(self, file_attachments_api_async, test_run):
"""Test uploading using pathlib.Path instead of string."""
# Create a temporary test file
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("Test pathlib upload\n")
tmp_path = Path(tmp.name)
try:
# Upload using Path object
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_run,
description="Test pathlib upload",
)
assert isinstance(file_attachment, FileAttachment)
assert file_attachment.id_ is not None
# Cleanup
await file_attachments_api_async.delete(file_attachments=file_attachment)
finally:
if tmp_path.exists():
tmp_path.unlink()
class TestGet:
"""Tests for the async get method."""
@pytest.mark.asyncio
async def test_get_by_id(self, file_attachments_api_async, uploaded_file_attachment):
"""Test getting a file attachment by ID."""
retrieved = await file_attachments_api_async.get(
file_attachment_id=uploaded_file_attachment.id_
)
assert retrieved is not None
assert retrieved.id_ == uploaded_file_attachment.id_
assert retrieved.file_name == uploaded_file_attachment.file_name
@pytest.mark.asyncio
async def test_get_nonexistent_raises_error(self, file_attachments_api_async):
"""Test that getting a non-existent file attachment raises an error."""
# Should raise an error for non-existent file attachment
with pytest.raises(Exception, match="invalid input syntax for type uuid"):
await file_attachments_api_async.get(file_attachment_id="nonexistent-file-id-12345")
class TestList:
"""Tests for the async list_ method."""
@pytest.mark.asyncio
async def test_basic_list(self, file_attachments_api_async):
"""Test basic file attachment listing functionality."""
file_attachments = await file_attachments_api_async.list_(limit=5)
# Verify we get a list
assert isinstance(file_attachments, list)
# If we have file attachments, verify their structure
if file_attachments:
fa = file_attachments[0]
assert isinstance(fa, FileAttachment)
assert fa.id_ is not None
@pytest.mark.asyncio
async def test_list_by_entity(
self, file_attachments_api_async, uploaded_file_attachment, test_run
):
"""Test listing file attachments filtered by entity."""
file_attachments = await file_attachments_api_async.list_(
entities=[test_run],
limit=100,
)
assert isinstance(file_attachments, list)
# Should find our uploaded file
found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments)
assert found, "Uploaded file attachment not found in entity list"
# All returned attachments should belong to the test run
for fa in file_attachments:
assert fa.entity_id == test_run.id_
@pytest.mark.asyncio
async def test_list_by_entity_id(
self, file_attachments_api_async, uploaded_file_attachment, test_run
):
"""Test listing file attachments filtered by entity_id."""
file_attachments = await file_attachments_api_async.list_(
entity_ids=[test_run.id_],
limit=100,
)
assert isinstance(file_attachments, list)
# Should find our uploaded file
found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments)
assert found, "Uploaded file attachment not found in entity list"
@pytest.mark.asyncio
async def test_list_by_entity_type(
self, file_attachments_api_async, uploaded_file_attachment, test_run
):
"""Test listing file attachments filtered by entity_type."""
# Test filtering by RUNS entity type
file_attachments = await file_attachments_api_async.list_(
entity_type=RemoteFileEntityType.RUNS,
limit=100,
)
assert isinstance(file_attachments, list)
# All returned attachments should be for RUNS
for fa in file_attachments:
assert fa.entity_type == RemoteFileEntityType.RUNS
# Test filtering by ASSETS entity type
file_attachments = await file_attachments_api_async.list_(
entity_type=RemoteFileEntityType.ASSETS,
limit=100,
)
assert isinstance(file_attachments, list)
# All returned attachments should be for ASSETS
for fa in file_attachments:
assert fa.entity_type == RemoteFileEntityType.ASSETS
# Test filtering by TEST_REPORTS entity type
file_attachments = await file_attachments_api_async.list_(
entity_type=RemoteFileEntityType.TEST_REPORTS,
limit=100,
)
assert isinstance(file_attachments, list)
# All returned attachments should be for TEST_REPORTS
for fa in file_attachments:
assert fa.entity_type == RemoteFileEntityType.TEST_REPORTS
# Test filtering by TEST_STEPS entity type
file_attachments = await file_attachments_api_async.list_(
entity_type=RemoteFileEntityType.TEST_STEPS,
limit=100,
)
assert isinstance(file_attachments, list)
# All returned attachments should be for TEST_STEPS
for fa in file_attachments:
assert fa.entity_type == RemoteFileEntityType.TEST_STEPS
@pytest.mark.asyncio
async def test_list_by_file_name(
self, file_attachments_api_async, uploaded_file_attachment
):
"""Test listing file attachments filtered by file name."""
file_attachments = await file_attachments_api_async.list_(
names=[uploaded_file_attachment.file_name],
)
assert isinstance(file_attachments, list)
# Should find at least our uploaded file
found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments)
assert found
# All returned attachments should have the specified file name
for fa in file_attachments:
assert fa.file_name == uploaded_file_attachment.file_name
@pytest.mark.asyncio
async def test_list_with_limit(self, file_attachments_api_async):
"""Test file attachment listing with different limits."""
# Test with limit of 1
fas_1 = await file_attachments_api_async.list_(limit=1)
assert isinstance(fas_1, list)
assert len(fas_1) <= 1
# Test with limit of 3
fas_3 = await file_attachments_api_async.list_(limit=3)
assert isinstance(fas_3, list)
assert len(fas_3) <= 3
class TestUpdate:
"""Tests for the async update method."""
@pytest.mark.asyncio
async def test_update_description(
self, file_attachments_api_async, uploaded_file_attachment
):
"""Test updating a file attachment's description."""
new_description = "Updated description for integration test"
update = FileAttachmentUpdate(description=new_description)
update.resource_id = uploaded_file_attachment.id_
updated = await file_attachments_api_async.update(file_attachment=update)
assert updated.id_ == uploaded_file_attachment.id_
assert updated.description == new_description
@pytest.mark.asyncio
async def test_update_with_dict(self, file_attachments_api_async, uploaded_file_attachment):
"""Test updating a file attachment using a dict."""
new_description = "Updated via dict"
# When using dict, the ID must be set via resource_id after creating the update object
update_dict = {"description": new_description}
update = FileAttachmentUpdate.model_validate(update_dict)
update.resource_id = uploaded_file_attachment.id_
updated = await file_attachments_api_async.update(file_attachment=update)
assert updated.id_ == uploaded_file_attachment.id_
assert updated.description == new_description
class TestDelete:
"""Tests for the async delete method."""
@pytest.mark.asyncio
async def test_delete_single_by_id(self, file_attachments_api_async, test_run):
"""Test deleting a single file attachment by ID string."""
# Upload a file to delete
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("File to delete\n")
tmp_path = tmp.name
try:
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_run,
description="File to delete",
)
# Delete by ID string
await file_attachments_api_async.delete(file_attachments=file_attachment.id_)
# Verify it's deleted by attempting to get it (should raise error)
with pytest.raises(Exception, match="An error occurred"):
await file_attachments_api_async.get(file_attachment_id=file_attachment.id_)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_delete_single_by_object(self, file_attachments_api_async, test_run):
"""Test deleting a single file attachment by FileAttachment object."""
# Upload a file to delete
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("File to delete\n")
tmp_path = tmp.name
try:
file_attachment = await file_attachments_api_async.upload(
path=tmp_path,
entity=test_run,
description="File to delete by object",
)
# Delete by FileAttachment object
await file_attachments_api_async.delete(file_attachments=file_attachment)
# Verify it's deleted by attempting to get it (should raise error)
with pytest.raises(Exception, match="An error occurred"):
await file_attachments_api_async.get(file_attachment_id=file_attachment.id_)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_delete_multiple(self, file_attachments_api_async, test_run):
"""Test deleting multiple file attachments at once."""
# Upload multiple files
file_attachments = []
tmp_paths = []
try:
for i in range(3):
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write(f"File {i} to delete\n")
tmp_paths.append(tmp.name)
fa = await file_attachments_api_async.upload(
path=tmp_paths[-1],
entity=test_run,
description=f"File {i} to delete",
)
file_attachments.append(fa)
# Delete all at once
await file_attachments_api_async.delete(file_attachments=file_attachments)
# Verify they're all deleted
for fa in file_attachments:
with pytest.raises(Exception, match="An error occurred"):
await file_attachments_api_async.get(file_attachment_id=fa.id_)
finally:
for tmp_path in tmp_paths:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_delete_list_of_ids(self, file_attachments_api_async, test_run):
"""Test deleting multiple file attachments using a list of ID strings."""
# Upload multiple files
file_attachments = []
tmp_paths = []
try:
for i in range(2):
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write(f"File {i} to delete\n")
tmp_paths.append(tmp.name)
fa = await file_attachments_api_async.upload(
path=tmp_paths[-1],
entity=test_run,
description=f"File {i} to delete by ID",
)
file_attachments.append(fa)
# Delete using list of IDs
ids = [fa.id_ for fa in file_attachments]
await file_attachments_api_async.delete(file_attachments=ids)
# Verify they're all deleted
for fa_id in ids:
with pytest.raises(Exception, match="An error occurred"):
await file_attachments_api_async.get(file_attachment_id=fa_id)
finally:
for tmp_path in tmp_paths:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
class TestDownload:
"""Tests for the async download methods."""
@pytest.mark.asyncio
async def test_get_download_url(self, file_attachments_api_async, uploaded_file_attachment):
"""Test getting a download URL for a file attachment."""
url = await file_attachments_api_async.get_download_url(
file_attachment=uploaded_file_attachment
)
assert isinstance(url, str)
assert len(url) > 0
# URL should be a valid HTTP/HTTPS URL
assert url.startswith("http://") or url.startswith("https://")
@pytest.mark.asyncio
async def test_get_download_url_by_id(
self, file_attachments_api_async, uploaded_file_attachment
):
"""Test getting a download URL using file attachment ID."""
url = await file_attachments_api_async.get_download_url(
file_attachment=uploaded_file_attachment.id_
)
assert isinstance(url, str)
assert len(url) > 0
class TestFileAttachmentsAPISync:
"""Test suite for the synchronous File Attachments API functionality.
Only includes a single test for basic sync generation. No specific sync behavior difference tests are needed.
"""
class TestList:
"""Tests for the sync list_ method."""
def test_basic_list(self, file_attachments_api_sync):
"""Test basic synchronous file attachment listing functionality."""
file_attachments = file_attachments_api_sync.list_(limit=5)
# Verify we get a list
assert isinstance(file_attachments, list)
# If we have file attachments, verify their structure
if file_attachments:
assert isinstance(file_attachments[0], FileAttachment)
class TestUpload:
"""Tests for the sync upload method."""
def test_upload_download_and_delete(self, file_attachments_api_sync, test_run):
"""Test synchronous upload, download, and cleanup."""
# Create a temporary test file
completed = False
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
tmp.write("Sync test file\n")
tmp_path = tmp.name
# Create a temporary download path
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
download_path = tmp.name
try:
# Upload using sync API
file_attachment = file_attachments_api_sync.upload(
path=tmp_path,
entity=test_run,
description="Sync upload test",
)
# Verify the upload
assert isinstance(file_attachment, FileAttachment)
assert file_attachment.id_ is not None
# Download the file
file_attachments_api_sync.download(
file_attachment=file_attachment, output_path=download_path
)
# Verify the downloaded content matches the original
with open(download_path) as f:
downloaded_content = f.read()
assert downloaded_content == "Sync test file\n"
# Cleanup
file_attachments_api_sync.delete(file_attachments=file_attachment)
completed = True
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
if os.path.exists(download_path):
os.unlink(download_path)
assert completed