Skip to content

Commit b59153e

Browse files
committed
Test coverage for CSV school import.
These tests cover the core functionality and some additional complex edge cases.
1 parent 2490205 commit b59153e

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Importing schools', type: :request do
6+
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
7+
8+
let(:csv_content) do
9+
<<~CSV
10+
name,website,address_line_1,municipality,country_code,owner_email
11+
Test School 1,https://test1.example.com,123 Main St,Springfield,US,[email protected]
12+
Test School 2,https://test2.example.com,456 Oak Ave,Boston,US,[email protected]
13+
CSV
14+
end
15+
16+
describe 'POST /api/schools/import' do
17+
let(:csv_file) do
18+
tempfile = Tempfile.new(['schools', '.csv'])
19+
tempfile.write(csv_content)
20+
tempfile.rewind
21+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
22+
end
23+
24+
context 'when user is an experience_cs_admin' do
25+
let(:admin_user) { create(:user, roles: 'experience-cs-admin') }
26+
27+
before do
28+
authenticated_in_hydra_as(admin_user)
29+
allow(UserInfoApiClient).to receive(:search_by_email).and_return([{ id: SecureRandom.uuid, email: '[email protected]' }])
30+
allow(ImportSchoolsJob).to receive(:perform_later).and_return(instance_double(ImportSchoolsJob, job_id: 'test-job-id'))
31+
end
32+
33+
it 'accepts CSV file and returns 202 Accepted' do
34+
post('/api/schools/import', headers:, params: { csv_file: csv_file })
35+
36+
expect(response).to have_http_status(:accepted)
37+
end
38+
39+
it 'responds with job_id and total_schools' do
40+
post('/api/schools/import', headers:, params: { csv_file: csv_file })
41+
42+
data = JSON.parse(response.body, symbolize_names: true)
43+
expect(data[:job_id]).to eq('test-job-id')
44+
expect(data[:total_schools]).to eq(2)
45+
expect(data[:message]).to include('Import job started')
46+
end
47+
48+
it 'enqueues ImportSchoolsJob' do
49+
post('/api/schools/import', headers:, params: { csv_file: csv_file })
50+
51+
expect(ImportSchoolsJob).to have_received(:perform_later)
52+
end
53+
end
54+
55+
context 'when CSV file is missing' do
56+
let(:admin_user) { create(:user, roles: 'experience-cs-admin') }
57+
58+
before do
59+
authenticated_in_hydra_as(admin_user)
60+
end
61+
62+
it 'responds 422 Unprocessable Entity' do
63+
post('/api/schools/import', headers:)
64+
65+
expect(response).to have_http_status(:unprocessable_entity)
66+
data = JSON.parse(response.body, symbolize_names: true)
67+
expect(data[:error_code]).to eq('CSV_FILE_REQUIRED')
68+
end
69+
end
70+
71+
context 'when CSV is invalid' do
72+
let(:admin_user) { create(:user, roles: 'experience-cs-admin') }
73+
let(:invalid_csv_content) do
74+
<<~CSV
75+
name,website
76+
Test School,https://test.example.com
77+
CSV
78+
end
79+
let(:invalid_csv_file) do
80+
tempfile = Tempfile.new(['schools_invalid', '.csv'])
81+
tempfile.write(invalid_csv_content)
82+
tempfile.rewind
83+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
84+
end
85+
86+
before do
87+
authenticated_in_hydra_as(admin_user)
88+
end
89+
90+
it 'responds 422 Unprocessable Entity with validation errors' do
91+
post('/api/schools/import', headers:, params: { csv_file: invalid_csv_file })
92+
93+
expect(response).to have_http_status(:unprocessable_entity)
94+
data = JSON.parse(response.body, symbolize_names: true)
95+
expect(data[:error_code]).to eq('CSV_INVALID_FORMAT')
96+
end
97+
end
98+
99+
context 'when user is not an admin' do
100+
let(:regular_user) { create(:user, roles: '') }
101+
102+
before do
103+
authenticated_in_hydra_as(regular_user)
104+
end
105+
106+
it 'responds 403 Forbidden' do
107+
post('/api/schools/import', headers:, params: { csv_file: csv_file })
108+
109+
expect(response).to have_http_status(:forbidden)
110+
end
111+
end
112+
113+
context 'when user is an editor-admin' do
114+
let(:admin_user) { create(:user, roles: 'editor-admin') }
115+
116+
before do
117+
authenticated_in_hydra_as(admin_user)
118+
allow(UserInfoApiClient).to receive(:search_by_email).and_return([{ id: SecureRandom.uuid, email: '[email protected]' }])
119+
end
120+
121+
it 'allows importing schools' do
122+
post('/api/schools/import', headers:, params: { csv_file: csv_file })
123+
124+
expect(response).to have_http_status(:accepted)
125+
end
126+
end
127+
end
128+
end
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'School Import - Edge Cases and Concurrency', type: :request do
6+
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
7+
let(:admin_user) { create(:user, roles: 'experience-cs-admin') }
8+
9+
before do
10+
authenticated_in_hydra_as(admin_user)
11+
end
12+
13+
describe 'Edge Cases' do
14+
context 'when CSV is empty (only headers)' do
15+
let(:csv_content) do
16+
<<~CSV
17+
name,website,address_line_1,municipality,country_code,owner_email
18+
CSV
19+
end
20+
21+
let(:csv_file) do
22+
tempfile = Tempfile.new(['schools_empty', '.csv'])
23+
tempfile.write(csv_content)
24+
tempfile.rewind
25+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
26+
end
27+
28+
it 'accepts empty CSV and returns 0 schools' do
29+
allow(ImportSchoolsJob).to receive(:perform_later).and_return(instance_double(ImportSchoolsJob, job_id: 'test-job-id'))
30+
31+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
32+
33+
expect(response).to have_http_status(:accepted)
34+
data = JSON.parse(response.body, symbolize_names: true)
35+
expect(data[:total_schools]).to eq(0)
36+
end
37+
end
38+
39+
context 'when CSV has duplicate owner emails' do
40+
let(:csv_content) do
41+
<<~CSV
42+
name,website,address_line_1,municipality,country_code,owner_email
43+
Test School 1,https://test1.example.com,123 Main St,Springfield,US,[email protected]
44+
Test School 2,https://test2.example.com,456 Oak Ave,Boston,US,[email protected]
45+
CSV
46+
end
47+
48+
let(:csv_file) do
49+
tempfile = Tempfile.new(['schools_duplicate', '.csv'])
50+
tempfile.write(csv_content)
51+
tempfile.rewind
52+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
53+
end
54+
55+
it 'rejects CSV with duplicate owner emails' do
56+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
57+
58+
expect(response).to have_http_status(:unprocessable_entity)
59+
data = JSON.parse(response.body, symbolize_names: true)
60+
expect(data[:error_code]).to eq('DUPLICATE_OWNER_EMAIL')
61+
expect(data[:details][:duplicate_emails]).to include('[email protected]')
62+
end
63+
end
64+
65+
context 'when CSV has whitespace in email addresses' do
66+
let(:csv_content) do
67+
<<~CSV
68+
name,website,address_line_1,municipality,country_code,owner_email
69+
Test School,https://test.example.com,123 Main St,Springfield,US, [email protected]#{' '}
70+
CSV
71+
end
72+
73+
let(:csv_file) do
74+
tempfile = Tempfile.new(['schools_whitespace', '.csv'])
75+
tempfile.write(csv_content)
76+
tempfile.rewind
77+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
78+
end
79+
80+
it 'handles whitespace in email addresses' do
81+
allow(UserInfoApiClient).to receive(:search_by_email).and_return(
82+
[{ id: SecureRandom.uuid, email: '[email protected]' }]
83+
)
84+
allow(ImportSchoolsJob).to receive(:perform_later).and_return(instance_double(ImportSchoolsJob, job_id: 'test-job-id'))
85+
86+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
87+
88+
expect(response).to have_http_status(:accepted)
89+
end
90+
end
91+
92+
context 'when CSV has invalid email format' do
93+
let(:csv_content) do
94+
<<~CSV
95+
name,website,address_line_1,municipality,country_code,owner_email
96+
Test School,https://test.example.com,123 Main St,Springfield,US,invalid-email
97+
CSV
98+
end
99+
100+
let(:csv_file) do
101+
tempfile = Tempfile.new(['schools_invalid_email', '.csv'])
102+
tempfile.write(csv_content)
103+
tempfile.rewind
104+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
105+
end
106+
107+
it 'rejects CSV with invalid email format' do
108+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
109+
110+
expect(response).to have_http_status(:unprocessable_entity)
111+
data = JSON.parse(response.body, symbolize_names: true)
112+
expect(data[:error_code]).to eq('CSV_VALIDATION_FAILED')
113+
expect(data[:details][:row_errors].first[:errors]).to include(
114+
hash_including(field: 'owner_email', message: 'invalid email format')
115+
)
116+
end
117+
end
118+
119+
context 'when CSV has special characters' do
120+
let(:csv_content) do
121+
<<~CSV
122+
name,website,address_line_1,municipality,country_code,owner_email
123+
"School with, comma",https://test.example.com,"123 Main St, Apt 2",Springfield,US,[email protected]
124+
CSV
125+
end
126+
127+
let(:csv_file) do
128+
tempfile = Tempfile.new(['schools_special_chars', '.csv'])
129+
tempfile.write(csv_content)
130+
tempfile.rewind
131+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
132+
end
133+
134+
it 'handles special characters correctly' do
135+
allow(UserInfoApiClient).to receive(:search_by_email).and_return(
136+
[{ id: SecureRandom.uuid, email: '[email protected]' }]
137+
)
138+
allow(ImportSchoolsJob).to receive(:perform_later).and_return(instance_double(ImportSchoolsJob, job_id: 'test-job-id'))
139+
140+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
141+
142+
expect(response).to have_http_status(:accepted)
143+
end
144+
end
145+
146+
context 'when CSV file is completely empty' do
147+
let(:csv_file) do
148+
tempfile = Tempfile.new(['schools_blank', '.csv'])
149+
tempfile.write('')
150+
tempfile.rewind
151+
Rack::Test::UploadedFile.new(tempfile.path, 'text/csv')
152+
end
153+
154+
it 'rejects empty file' do
155+
post('/api/schools/import', headers: headers, params: { csv_file: csv_file })
156+
157+
expect(response).to have_http_status(:unprocessable_entity)
158+
data = JSON.parse(response.body, symbolize_names: true)
159+
expect(data[:error_code]).to eq('CSV_INVALID_FORMAT')
160+
expect(data[:message]).to include('empty')
161+
end
162+
end
163+
end
164+
165+
describe 'Job Status Access Control' do
166+
let(:admin_1) { create(:user, roles: 'experience-cs-admin') }
167+
let(:admin_2) { create(:user, roles: 'experience-cs-admin') }
168+
let(:job_id) { SecureRandom.uuid }
169+
170+
before do
171+
# Create a job result owned by admin1
172+
SchoolImportResult.create!(
173+
job_id: job_id,
174+
user_id: admin_1.id,
175+
results: { successful: [], failed: [] }
176+
)
177+
178+
# Create a GoodJob execution
179+
GoodJob::Execution.create!(
180+
active_job_id: job_id,
181+
job_class: 'ImportSchoolsJob',
182+
serialized_params: {},
183+
queue_name: 'import_schools_job',
184+
created_at: Time.current
185+
)
186+
end
187+
188+
context 'when an admin tries to access another admins job' do
189+
it 'allows access' do
190+
authenticated_in_hydra_as(admin_2)
191+
192+
get("/api/school_import_jobs/#{job_id}", headers: headers)
193+
194+
expect(response).to have_http_status(:ok)
195+
end
196+
end
197+
198+
context 'when a non-admin tries to access an import job' do
199+
let(:non_admin) { create(:user, roles: '') }
200+
201+
it 'returns 403' do
202+
authenticated_in_hydra_as(non_admin)
203+
204+
get("/api/school_import_jobs/#{job_id}", headers: headers)
205+
206+
expect(response).to have_http_status(:forbidden)
207+
end
208+
end
209+
end
210+
end

0 commit comments

Comments
 (0)