11import csv
22import re
3- from io import StringIO
3+ import zipfile
4+ from io import BytesIO , StringIO
45
56import pytest
67from fastapi import status
1011from app .db .configurations .model import DbConfigurationCustomCode
1112
1213
14+ def get_csv_from_zip (content : bytes , filename_pattern : str ) -> str :
15+ """Extract a CSV file from a zip response by filename pattern."""
16+ with zipfile .ZipFile (BytesIO (content )) as zf :
17+ for name in zf .namelist ():
18+ if re .search (filename_pattern , name ):
19+ return zf .read (name ).decode ("utf-8" )
20+ raise FileNotFoundError (f"No file matching { filename_pattern !r} found in zip" )
21+
22+
1323@pytest .mark .integration
1424@pytest .mark .asyncio
1525class TestConfigurationExport :
@@ -21,11 +31,11 @@ async def test_export_returns_404_for_unknown_id(self, setup, authed_client):
2131 response = await authed_client .get (f"/api/v1/configurations/{ dummy_id } /export" )
2232 assert response .status_code == status .HTTP_404_NOT_FOUND
2333
24- async def test_export_returns_csv_with_correct_headers (
34+ async def test_export_returns_zip_with_correct_headers (
2535 self , setup , authed_client , get_condition_id , create_config
2636 ):
2737 """
28- CSV should be returned in correct form when given a valid config.
38+ Zip should be returned in correct form when given a valid config.
2939 """
3040 condition_id = await get_condition_id ("Colorado tick fever" )
3141 config = await create_config (condition_id )
@@ -34,14 +44,33 @@ async def test_export_returns_csv_with_correct_headers(
3444 )
3545
3646 assert response .status_code == status .HTTP_200_OK
37- assert response .headers ["content-type" ].startswith ("text/csv " )
47+ assert response .headers ["content-type" ].startswith ("application/zip " )
3848
3949 cd_header = response .headers .get ("content-disposition" , "" )
4050 assert re .search (
41- r'filename=".+_Code_Export_ \d{6}_\d{2}_\d{2}_\d{2}\.csv "' ,
51+ r'filename=".+_Configuration_Export_ \d{6}_\d{2}_\d{2}_\d{2}\.zip "' ,
4252 cd_header ,
4353 ), f"Unexpected Content-Disposition: { cd_header !r} "
4454
55+ async def test_export_zip_contains_expected_files (
56+ self , setup , authed_client , get_condition_id , create_config
57+ ):
58+ """
59+ Zip should contain a codes CSV and a sections CSV.
60+ """
61+ condition_id = await get_condition_id ("Colorado tick fever" )
62+ config = await create_config (condition_id )
63+ response = await authed_client .get (
64+ f"/api/v1/configurations/{ config ['id' ]} /export"
65+ )
66+
67+ assert response .status_code == status .HTTP_200_OK
68+
69+ with zipfile .ZipFile (BytesIO (response .content )) as zf :
70+ names = zf .namelist ()
71+ assert any (re .search (r"Code_Export.+\.csv" , n ) for n in names )
72+ assert any (re .search (r"Section_Export.+\.csv" , n ) for n in names )
73+
4574 async def test_export_includes_all_codes_from_multiple_codesets (
4675 self ,
4776 setup ,
@@ -72,7 +101,7 @@ async def test_export_includes_all_codes_from_multiple_codesets(
72101 response = await authed_client .get (f"/api/v1/configurations/{ config_id } /export" )
73102 assert response .status_code == status .HTTP_200_OK
74103
75- content = response .text
104+ content = get_csv_from_zip ( response .content , r"Code_Export" )
76105 lines = [line for line in content .splitlines () if line .strip ()]
77106 assert len (lines ) == expected_code_rows + 1 # header + codes
78107
@@ -87,7 +116,6 @@ async def test_export_csv_conditions_column_correct(
87116 """
88117 CSV "Condition" column should only contain the associated condition names.
89118 """
90-
91119 amebiasis_id = await get_condition_id ("Amebiasis" )
92120 config = await create_config (amebiasis_id )
93121 config_id = config ["id" ]
@@ -98,9 +126,9 @@ async def test_export_csv_conditions_column_correct(
98126 response = await authed_client .get (f"/api/v1/configurations/{ config_id } /export" )
99127 assert response .status_code == status .HTTP_200_OK
100128
101- reader = csv . DictReader ( StringIO ( response .text ) )
102- column_name = "Condition"
103- conditions_in_csv = {row [column_name ] for row in reader if row [column_name ]}
129+ content = get_csv_from_zip ( response .content , r"Code_Export" )
130+ reader = csv . DictReader ( StringIO ( content ))
131+ conditions_in_csv = {row ["Condition" ] for row in reader if row ["Condition" ]}
104132
105133 assert conditions_in_csv == {"Amebiasis" , "Byssinosis" }
106134
@@ -132,17 +160,18 @@ async def test_export_csv_code_systems_valid(
132160 response = await authed_client .get (f"/api/v1/configurations/{ config_id } /export" )
133161 assert response .status_code == status .HTTP_200_OK
134162
135- reader = csv .DictReader (StringIO (response .text ))
136- column_name = "Code System"
137- code_systems_in_csv = {row [column_name ] for row in reader if row [column_name ]}
163+ content = get_csv_from_zip (response .content , r"Code_Export" )
164+ reader = csv .DictReader (StringIO (content ))
165+ code_systems_in_csv = {
166+ row ["Code System" ] for row in reader if row ["Code System" ]
167+ }
138168
139169 code_systems = await get_all_code_systems_db (db = db_pool )
140170 expected_systems = {cs .display_name for cs in code_systems .values ()}
141171
142- # csv systems must be a subset of the expected systems set
143172 assert code_systems_in_csv <= expected_systems
144173
145- async def test_export_custom_codes_have_blank_code_system (
174+ async def test_export_custom_codes_have_blank_condition (
146175 self ,
147176 setup ,
148177 authed_client ,
@@ -169,18 +198,19 @@ async def test_export_custom_codes_have_blank_code_system(
169198 response = await authed_client .get (f"/api/v1/configurations/{ config_id } /export" )
170199 assert response .status_code == status .HTTP_200_OK
171200
172- reader = csv .DictReader (StringIO (response .text ))
201+ content = get_csv_from_zip (response .content , r"Code_Export" )
202+ reader = csv .DictReader (StringIO (content ))
173203 for row in reader :
174204 if row ["Code Type" ] == "Custom code" :
175205 assert row ["Condition" ] == "" , (
176- f"Expected blank Code System for custom code, got { row ['Code System ' ]!r} "
206+ f"Expected blank Condition for custom code, got { row ['Condition ' ]!r} "
177207 )
178208
179209 async def test_export_csv_body_is_non_empty (
180210 self , setup , authed_client , get_condition_id , create_config
181211 ):
182212 """
183- CSV should contain at least a header row.
213+ Codes CSV should contain at least a header row.
184214 """
185215 condition_id = await get_condition_id ("Cholera" )
186216 config = await create_config (condition_id )
@@ -189,6 +219,6 @@ async def test_export_csv_body_is_non_empty(
189219 )
190220
191221 assert response .status_code == status .HTTP_200_OK
192- content = response .text
222+ content = get_csv_from_zip ( response .content , r"Code_Export" )
193223 lines = [line for line in content .splitlines () if line .strip ()]
194224 assert len (lines ) >= 1 , "Expected at least a CSV header row in the response"
0 commit comments