Skip to content

Commit 336bf32

Browse files
Create UpdateSchoolEmailDomainsJob
Set up a job to update Profile's school email domains
1 parent c3b877a commit 336bf32

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
class UpdateSchoolEmailDomainsJob < ApplicationJob
4+
class ConcurrencyExceededForSchool < StandardError; end
5+
6+
include GoodJob::ActiveJobExtensions::Concurrency
7+
8+
# Serialize runs per school (eg user edits domains while batch or another job is updating Profile).
9+
good_job_control_concurrency_with(
10+
perform_limit: 1,
11+
key: -> { "#{self.class.name}/#{concurrency_key_id}" }
12+
)
13+
14+
retry_on StandardError, wait: :polynomially_longer, attempts: 3 do |_job, e|
15+
Sentry.capture_exception(e)
16+
raise e
17+
end
18+
19+
# Don't retry...
20+
rescue_from ConcurrencyExceededForSchool do |e|
21+
Rails.logger.error "Only one job per school can be enqueued at a time: #{concurrency_key_id}"
22+
Sentry.capture_exception(e)
23+
raise e
24+
end
25+
26+
queue_as :update_school_email_domains_job
27+
28+
def self.attempt_perform_later(school_id:, school_email_domains:, token:)
29+
perform_later(school_id:, school_email_domains:, token:)
30+
end
31+
32+
def perform(school_id:, school_email_domains:, token:)
33+
ProfileApiClient.update_school_email_domains(token:, school_id:, school_email_domains:)
34+
end
35+
36+
private
37+
38+
def concurrency_key_id
39+
arguments.first&.with_indifferent_access&.dig(:school_id)
40+
end
41+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe UpdateSchoolEmailDomainsJob do
6+
include ActiveJob::TestHelper
7+
8+
let(:token) { UserProfileMock::TOKEN }
9+
let(:school) { create(:verified_school) }
10+
let(:school_email_domains) { ['student.example.edu', 'mail.example.org'] }
11+
let(:profile_api_base_url) { 'http://profile.com' }
12+
let(:profile_api_key) { 'api-key' }
13+
let(:update_school_email_domains_path) { "/api/v1/schools/#{school.id}" }
14+
let(:update_school_email_domains_url) { "#{profile_api_base_url}#{update_school_email_domains_path}" }
15+
16+
around do |example|
17+
ClimateControl.modify(
18+
IDENTITY_URL: profile_api_base_url,
19+
PROFILE_API_KEY: profile_api_key
20+
) do
21+
example.run
22+
end
23+
end
24+
25+
before do
26+
stub_request(:patch, update_school_email_domains_url)
27+
.to_return(
28+
status: 201,
29+
body: {
30+
id: school.id,
31+
schoolCode: '99-12-34',
32+
updatedAt: '2024-07-09T10:31:13.196Z',
33+
createdAt: '2024-07-09T10:31:13.196Z',
34+
discardedAt: nil,
35+
studentEmailDomains: school_email_domains
36+
}.to_json,
37+
headers: { 'Content-Type' => 'application/json' }
38+
)
39+
end
40+
41+
after do
42+
GoodJob::Job.delete_all
43+
end
44+
45+
describe 'GoodJob concurrency' do
46+
it 'uses a per-school concurrency key' do
47+
job = described_class.new
48+
job.arguments = [{ school_id: school.id, school_email_domains:, token: }]
49+
50+
expect(job.good_job_concurrency_key).to eq("#{described_class.name}/#{school.id}")
51+
end
52+
53+
it 'uses a different concurrency key for a different school' do
54+
other_school = create(:verified_school)
55+
job_a = described_class.new
56+
job_a.arguments = [{ school_id: school.id, school_email_domains:, token: }]
57+
job_b = described_class.new
58+
job_b.arguments = [{ school_id: other_school.id, school_email_domains:, token: }]
59+
60+
expect(job_a.good_job_concurrency_key).not_to eq(job_b.good_job_concurrency_key)
61+
end
62+
end
63+
64+
it 'PATCHes student email domains to the Profile API' do
65+
described_class.perform_now(token:, school_id: school.id, school_email_domains:)
66+
expect(WebMock).to have_requested(:patch, update_school_email_domains_url).with(
67+
body: { studentEmailDomains: school_email_domains }.to_json
68+
)
69+
end
70+
end

0 commit comments

Comments
 (0)