diff --git a/.env.development b/.env.development index df2f6762dd..170475040c 100644 --- a/.env.development +++ b/.env.development @@ -12,4 +12,8 @@ VACANCY_SOURCE_EVERY_FEED_URL=http://example.com/feed.json VACANCY_SOURCE_MY_NEW_TERM_FEED_URL=https://example.com VACANCY_SOURCE_BROADBEAN_FEED_URL=http://example.com/feed.xml VACANCY_SOURCE_MY_NEW_TERM_API_KEY=api_key +VACANCY_SOURCE_ITRENT_FEED_URL=http://example.com/feed.json +VACANCY_SOURCE_ITRENT_UDF_URL=http://example.com/udf.json +VACANCY_SOURCE_ITRENT_AUTH_USER=user +VACANCY_SOURCE_ITRENT_AUTH_PASSWORD=password SECRET_KEY_BASE=352f2c2ccf5a9a77d8cc91fade3685dda45407f329c14ed0b8e4c2d6618d8475e4e4416bf20eeeec46558384ea9c9e2de97b85e20c47adb93d914576ed6b69d4 diff --git a/.env.test b/.env.test index ca662e69af..aaa1cb5703 100644 --- a/.env.test +++ b/.env.test @@ -14,6 +14,10 @@ VACANCY_SOURCE_MY_NEW_TERM_FEED_URL=http://example.com VACANCY_SOURCE_MY_NEW_TERM_API_KEY=test-api-key VACANCY_SOURCE_BROADBEAN_FEED_URL=http://example.com/feed.xml VACANCY_SOURCE_ARK_FEED_URL=http://example.com/feed.xml +VACANCY_SOURCE_ITRENT_FEED_URL=http://example.com/feed.json +VACANCY_SOURCE_ITRENT_UDF_URL=http://example.com/udf.json +VACANCY_SOURCE_ITRENT_AUTH_USER=user +VACANCY_SOURCE_ITRENT_AUTH_PASSWORD=password BIGQUERY_TABLE_NAME=test_events BIGQUERY_PROJECT_ID=teacher-vacancy-service BIGQUERY_DATASET=testing_dataset diff --git a/app/vacancy_sources/vacancy_source/source/itrent.rb b/app/vacancy_sources/vacancy_source/source/itrent.rb new file mode 100644 index 0000000000..b202be38b9 --- /dev/null +++ b/app/vacancy_sources/vacancy_source/source/itrent.rb @@ -0,0 +1,194 @@ +class VacancySource::Source::Itrent + class EveryImportError < StandardError; end + + include HTTParty + include Enumerable + include VacancySource::Parser + include VacancySource::Shared + + SOURCE_NAME = "itrent".freeze + ERROR_MESSAGE = "Something went wrong with iTrent Import. Response:".freeze + FEED_URL = ENV.fetch("VACANCY_SOURCE_ITRENT_FEED_URL").freeze + UDF_URL = ENV.fetch("VACANCY_SOURCE_ITRENT_UDF_URL").freeze + + FEED_HEADERS = { + "Connection" => "keep-alive", + "iTrent-Operation" => "requisition", + "PartyName" => "MHR", + "content-type" => "application/json", + }.freeze + + UDF_HEADERS = { + "Connection" => "keep-alive", + "iTrent-Operation" => "get_udfs", + "iTrent-Object-Type" => "REQUISITION", + "content-type" => "application/json", + "iTrent-UDF-ORG" => "MHR", + "iTrent-UDF-Category" => "TeachVacs", + }.freeze + + AUTH = { + username: ENV.fetch("VACANCY_SOURCE_ITRENT_AUTH_USER"), + password: ENV.fetch("VACANCY_SOURCE_ITRENT_AUTH_PASSWORD"), + }.freeze + + def self.source_name + SOURCE_NAME + end + + def each + results.each do |result| + result["udfs"] = user_defined_fields(result["requisitionreference"]) + schools = find_schools(result) + next if vacancy_listed_at_excluded_school_type?(schools) + + v = Vacancy.find_or_initialize_by( + external_source: SOURCE_NAME, + external_reference: result["requisitionreference"], + ) + + # An external vacancy is by definition always published + v.status = :published + + begin + v.assign_attributes(attributes_for(result, schools)) + rescue ArgumentError => e + v.errors.add(:base, e) + end + + yield v + end + end + + private + + def attributes_for(item, schools) + { + job_title: item["requisitionname"], + job_advert: item.dig("vacancyptysrchs", "vacancyptysrch", "jobdescription"), + salary: item.dig("vacancyptysrchs", "vacancyptysrch", "formattedsalarydescription"), + benefits_details: item.dig("udfs", "additionalallowances"), + expires_at: Time.zone.parse(item["applicationclosingdate"]), + external_advert_url: advert_url_for(item), + job_roles: job_roles_for(item), + ect_status: ect_status_for(item), + subjects: item.dig("udfs", "subjects").presence&.split("\n"), + working_patterns: item.dig("vacancyptysrchs", "vacancyptysrch", "basis").presence&.parameterize(separator: "_"), + contract_type: item.dig("udfs", "contracttype").presence&.parameterize(separator: "_"), + phases: phase_for(item), + key_stages: item.dig("udfs", "keystages").presence&.split(","), + visa_sponsorship_available: false, + publish_on: publish_on_for(item), + }.merge(organisation_fields(schools)) + .merge(start_date_fields(item)) + end + + # Consider publish_on date to be the first time we saw this vacancy come through + # (i.e. today, unless it already has a publish on date set) + def publish_on_for(item) + publish_on = item.dig("udfs", "publishon") + if publish_on.present? + Date.parse(publish_on) + else + Date.today + end + end + + def start_date_fields(item) + return {} if item["startdate"].blank? + + parsed_date = StartDate.new(item["startdate"]) + if parsed_date.specific? + { starts_on: parsed_date.date, start_date_type: parsed_date.type } + else + { other_start_date_details: parsed_date.date, start_date_type: parsed_date.type } + end + end + + def organisation_fields(schools) + first_school = schools.first + + { + organisations: schools, + readable_job_location: first_school&.name, + about_school: first_school&.description, + } + end + + def find_schools(item) + multi_academy_trust = SchoolGroup.trusts.find_by(uid: item.dig("udfs", "trustuid")) + school_urns = item.dig("udfs", "schoolurns")&.split(",") + + return [] if multi_academy_trust.blank? && school_urns.blank? + return Organisation.where(urn: school_urns) if multi_academy_trust.blank? + return Array(multi_academy_trust) if school_urns.blank? + + # When having both trust and schools, only return the schools that are in the trust if any. Otherwise, return the trust itself. + multi_academy_trust.schools.where(urn: school_urns).order(:created_at).presence || Array(multi_academy_trust) + end + + def multi_academy_trust(item) + SchoolGroup.trusts.find_by(uid: item.dig("udfs", "trustuid")) + end + + def job_roles_for(item) + role = item.dig("udfs", "jobrole")&.strip&.downcase + return [] if role.blank? + + # Translate legacy senior/middle leader into all the granular roles split from them + return Vacancy::SENIOR_LEADER_JOB_ROLES if role.include? "senior_leader" + return Vacancy::MIDDLE_LEADER_JOB_ROLES if role.include? "middle_leader" + + Array.wrap(role.gsub("deputy_headteacher_principal", "deputy_headteacher") + .gsub("assistant_headteacher_principal", "assistant_headteacher") + .gsub("headteacher_principal", "headteacher") + .gsub(/head_of_year_or_phase|head_of_year/, "head_of_year_or_phase") + .gsub(/learning_support|other_support|science_technician/, "education_support") + .gsub(/\s+/, "")) + end + + def ect_status_for(item) + item.dig("udfs", "ectsuitable").to_s == "true" ? "ect_suitable" : "ect_unsuitable" + end + + def phase_for(item) + return if item.dig("udfs", "phase").blank? + + item.dig("udfs", "phase") + .strip + .parameterize(separator: "_") + .gsub(/all_through|through_school/, "through") + .gsub(/16-19|16_19/, "sixth_form_or_college") + end + + def advert_url_for(item) + url_link = item.dig("vacancyptysrchs", "vacancyptysrch", "urllinks", "urllink") + if url_link.is_a?(Array) + url_link.first&.fetch("link", nil) + else + url_link&.fetch("link", nil) + end + end + + def results + feed.dig("RequisitionData", "requisition") + end + + def feed + response = HTTParty.get(FEED_URL, headers: FEED_HEADERS, basic_auth: AUTH) + raise HTTParty::ResponseError, ERROR_MESSAGE unless response.success? + + parsed_response = JSON.parse(response.body) + raise EveryImportError, ERROR_MESSAGE + parsed_response["error"] if parsed_response["error"] + + parsed_response + end + + def user_defined_fields(vacancy_ref) + headers = UDF_HEADERS.merge("iTrent-Object-Ref" => vacancy_ref) + response = HTTParty.get(UDF_URL, headers:, basic_auth: AUTH) + raise HTTParty::ResponseError, ERROR_MESSAGE unless response.success? + + JSON.parse(response.body)&.dig("itrent", "udfs")&.first + end +end diff --git a/spec/fixtures/files/vacancy_sources/itrent_requisition.json b/spec/fixtures/files/vacancy_sources/itrent_requisition.json new file mode 100644 index 0000000000..27054fa4cd --- /dev/null +++ b/spec/fixtures/files/vacancy_sources/itrent_requisition.json @@ -0,0 +1,45 @@ +{ + "RequisitionData": { + "requisition": [{ + "requisitionreference": "FF00067", + "requisitionname": "Class Teacher", + "startdate": "2023-05-01", + "enddate": "", + "applicationclosingdate": "2023-06-30", + "recruitingmanager": "Mr Daniel Andrews He/They", + "requisitionadministrator": "Mrs Kathryn Baxendale She/Her", + "speculativeenquiries": "F", + "vacancyptysrchs": { + "vacancyptysrch": { + "requisitioncategorytypes": { + "requisitioncategorytype": "Human Resources" + }, + "language": "USA", + "publish": "T", + "jobtitle": "HR Business Partner", + "jobdescription": "

Lorem Ipsum dolor sit amet

", + "salarysearchvalue": "24000", + "minimumsalary": "24000", + "maximumsalary": "30000", + "formattedsalarydescription": "£24,000 to £30,000 depending on working knowledge", + "package": "Company Pension, excellent employee benefits, 25 days holiday + bank holidays.", + "contractualhours": "37.00", + "basis": "Full time", + "region": "Central", + "location": "Birmingham", + "intranetonly": "F", + "keyword1": "HR", + "keyword2": "HUMAN RESOURCES", + "keyword3": "ASSISTANT", + "keyword4": "ADMINISTRATION", + "urllinks": { + "urllink": { + "webview": "MHR 2022 - 10.45", + "link": "https://teachvacs.itrentdemo.co.uk/ttrent_webrecruitment/wrd/run/ETREC179GF.open?WVID=2750500Wqk&VACANCY_ID=5116820Y9h" + } + } + } + } + }] + } +} diff --git a/spec/fixtures/files/vacancy_sources/itrent_udf.json b/spec/fixtures/files/vacancy_sources/itrent_udf.json new file mode 100644 index 0000000000..c394c4721a --- /dev/null +++ b/spec/fixtures/files/vacancy_sources/itrent_udf.json @@ -0,0 +1,20 @@ +{ + "itrent" : { + "udfs" : [{ + "category": "TeachVacs", + "udf" : "FF00083 - Maths Teacher", + "schoolurns" : "12345", + "trustuid" : "456789", + "jobrole" : "Teacher", + "contracttype" : "fixed term", + "phase" : "secondary", + "publishon" : "07\/08\/2023", + "additionalallowances" : "TLR2a", + "ectsuitable" : "true", + "keystages" : "ks2", + "subjects" : "maths\nadvanced maths" + }], + "messages" : [ + ] + } + } diff --git a/spec/vacancy_sources/vacancy_source/source/itrent_spec.rb b/spec/vacancy_sources/vacancy_source/source/itrent_spec.rb new file mode 100644 index 0000000000..395c22668c --- /dev/null +++ b/spec/vacancy_sources/vacancy_source/source/itrent_spec.rb @@ -0,0 +1,418 @@ +require "rails_helper" + +RSpec.describe VacancySource::Source::Itrent do + let!(:school1) { create(:school, name: "Test School", urn: "12345", phase: :primary) } + let!(:school_group) { create(:school_group, name: "E-ACT", uid: "456789", schools: trust_schools) } + let(:trust_schools) { [school1] } + + let(:response_requisition_body) { file_fixture("vacancy_sources/itrent_requisition.json").read } + let(:response_udf_body) { file_fixture("vacancy_sources/itrent_udf.json").read } + let(:requisition_response) { double("ItrentHttpResponse", success?: true, body: response_requisition_body) } + let(:udf_response) { double("ItrentHttpResponse", success?: true, body: response_udf_body) } + # let(:argument_error_response) { double("ItrentHttpResponse", success?: true, body: file_fixture("vacancy_sources/fusion_argument_error.json").read) } + + describe "enumeration" do + before do + expect(HTTParty).to receive(:get).with("http://example.com/feed.json", anything).and_return(requisition_response) + expect(HTTParty).to receive(:get).with("http://example.com/udf.json", anything).and_return(udf_response) + end + + let(:vacancy) { subject.first } + let(:expected_vacancy) do + { + job_title: "Class Teacher", + job_advert: "

Lorem Ipsum dolor sit amet

", + salary: "£24,000 to £30,000 depending on working knowledge", + job_roles: ["teacher"], + key_stages: %w[ks2], + working_patterns: %w[full_time], + contract_type: "fixed_term", + phases: %w[secondary], + subjects: ["maths", "advanced maths"], + visa_sponsorship_available: false, + benefits_details: "TLR2a", + ect_status: "ect_suitable", + } + end + + it "has the correct number of vacancies" do + expect(subject.count).to eq(1) + end + + it "yield a newly built vacancy the correct vacancy information" do + expect(vacancy).not_to be_persisted + expect(vacancy).to be_changed + end + + it "assigns correct attributes from the feed" do + expect(vacancy).to have_attributes(expected_vacancy) + end + + it "assigns the vacancy to the correct school and organisation" do + expect(vacancy.organisations.first).to eq(school1) + + expect(vacancy.external_source).to eq("itrent") + expect(vacancy.external_advert_url).to eq("https://teachvacs.itrentdemo.co.uk/ttrent_webrecruitment/wrd/run/ETREC179GF.open?WVID=2750500Wqk&VACANCY_ID=5116820Y9h") + expect(vacancy.external_reference).to eq("FF00067") + + expect(vacancy.organisations).to eq(trust_schools) + end + + it "sets important dates" do + expect(vacancy.expires_at).to eq(Time.zone.parse("2023-06-30T00:00:00")) + expect(vacancy.publish_on).to eq(Date.parse("07/08/2023")) + end + + describe "job roles mapping" do + let(:response_udf_body) { super().gsub("Teacher", source_role) } + + ["null", "", " "].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "the vacancy role is null" do + expect(vacancy.job_roles).to eq([]) + end + end + end + + %w[deputy_headteacher_principal deputy_headteacher].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "maps the source role to '[deputy_headteacher]' in the vacancy" do + expect(vacancy.job_roles).to eq(["deputy_headteacher"]) + end + end + end + + %w[assistant_headteacher_principal assistant_headteacher].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "maps the source role to '[assistant_headteacher]' in the vacancy" do + expect(vacancy.job_roles).to eq(["assistant_headteacher"]) + end + end + end + + %w[headteacher_principal headteacher].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "maps the source role to '[headteacher]' in the vacancy" do + expect(vacancy.job_roles).to eq(["headteacher"]) + end + end + end + + context "when the source role is 'senior_leader'" do + let(:source_role) { "senior_leader" } + + it "maps the source role to '[headteacher, assistant_headteacher, deputy_headteacher]' in the vacancy" do + expect(vacancy.job_roles).to contain_exactly("headteacher", "assistant_headteacher", "deputy_headteacher") + end + end + + %w[head_of_year_or_phase head_of_year].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "maps the source role to '[head_of_year_or_phase]' in the vacancy" do + expect(vacancy.job_roles).to eq(["head_of_year_or_phase"]) + end + end + end + + context "when the source role is 'head_of_department_or_curriculum'" do + let(:source_role) { "head_of_department_or_curriculum" } + + it "maps the source role to '[head_of_department_or_curriculum]' in the vacancy" do + expect(vacancy.job_roles).to eq(["head_of_department_or_curriculum"]) + end + end + + context "when the source role is 'middle_leader'" do + let(:source_role) { "middle_leader" } + + it "maps the source role to '[head_of_year_or_phase, head_of_department_or_curriculum]' in the vacancy" do + expect(vacancy.job_roles).to contain_exactly("head_of_year_or_phase", "head_of_department_or_curriculum") + end + end + + %w[learning_support other_support science_technician].each do |role| + context "when the source role is '#{role}'" do + let(:source_role) { role } + + it "maps the source role to '[education_support]' in the vacancy" do + expect(vacancy.job_roles).to eq(["education_support"]) + end + end + end + end + + describe "start date mapping" do + let(:fixture_date) { "2023-05-01" } + + it "stores the specific start date" do + expect(vacancy.starts_on.to_s).to eq "2023-05-01" + expect(vacancy.start_date_type).to eq "specific_date" + end + + context "when the start date is blank" do + let(:response_requisition_body) { super().gsub(fixture_date, "") } + + it "doesn't store a start date" do + expect(vacancy.starts_on).to be_nil + expect(vacancy.start_date_type).to eq nil + end + end + + context "when the start date is not present" do + let(:response_requisition_body) { super().gsub(/"#{fixture_date}"/, "null") } + + it "doesn't store a start date" do + expect(vacancy.starts_on).to be_nil + expect(vacancy.start_date_type).to eq nil + end + end + + context "when the start date is a date with extra data" do + let(:response_requisition_body) { super().gsub(fixture_date, "2022-11-21 or later") } + + it "stores it as other start date details" do + expect(vacancy.starts_on).to be_nil + expect(vacancy.other_start_date_details).to eq("2022-11-21 or later") + expect(vacancy.start_date_type).to eq "other" + end + end + + context "when the start date comes as a specific datetime" do + let(:response_requisition_body) { super().gsub(fixture_date, "2023-11-21T00:00:00") } + + it "stores it parsed as a specific date" do + expect(vacancy.starts_on.to_s).to eq("2023-11-21") + expect(vacancy.start_date_type).to eq "specific_date" + end + end + + context "when the start date comes as a specific date in a different format" do + let(:response_requisition_body) { super().gsub(fixture_date, "21.11.23") } + + it "stores it parsed as a specific date" do + expect(vacancy.starts_on.to_s).to eq("2023-11-21") + expect(vacancy.start_date_type).to eq "specific_date" + end + end + + context "when the start date is a text" do + let(:response_requisition_body) { super().gsub(fixture_date, "TBC") } + + it "stores it as other start date details" do + expect(vacancy.starts_on).to be_nil + expect(vacancy.other_start_date_details).to eq("TBC") + expect(vacancy.start_date_type).to eq "other" + end + end + end + + describe "phase mapping" do + let(:response_udf_body) { super().gsub("secondary", phase) } + + %w[16-19 16_19].each do |phase| + context "when the phase is '#{phase}'" do + let(:phase) { phase } + + it "maps the phase to '[sixth_form_or_college]' in the vacancy" do + expect(vacancy.phases).to eq(["sixth_form_or_college"]) + end + end + end + + context "when the phase is 'through_school'" do + let(:phase) { "through_school" } + + it "maps the phase to '[through]' in the vacancy" do + expect(vacancy.phases).to eq(["through"]) + end + end + end + + describe "ect suitability mapping" do + let(:response_udf_body) do + JSON.parse(super()).tap { |h| + h["itrent"]["udfs"].first["ectsuitable"] = ect_suitability + }.to_json + end + + context "when the vacancy is suitable for an ECT" do + let(:ect_suitability) { true } + + it "sets the vacancy as suitable for an ECT" do + expect(vacancy.ect_status).to eq("ect_suitable") + end + end + + context "when the vacancy is not suitable for an ECT" do + let(:ect_suitability) { false } + + it "sets the vacancy as not suitable for an ECT" do + expect(vacancy.ect_status).to eq("ect_unsuitable") + end + end + + context "when the vacancy suitability for an ECT is not provided" do + let(:ect_suitability) { nil } + + it "sets the vacancy as not suitable for an ECT" do + expect(vacancy.ect_status).to eq("ect_unsuitable") + end + end + end + + describe "external advert url parsing" do + let(:response_requisition_body) do + JSON.parse(super()).tap { |h| + h["RequisitionData"]["requisition"].first["vacancyptysrchs"]["vacancyptysrch"]["urllinks"]["urllink"] = url_link + }.to_json + end + + context "when the vacancy has a single link" do + let(:url_link) do + { webview: "MHR 2022 - 10.45", link: "https://adverturl.com" } + end + + it "uses the link for the parsed vacancy" do + expect(vacancy.external_advert_url).to eq "https://adverturl.com" + end + end + + context "when the vacancy has a multiple links" do + let(:url_link) do + [ + { + webview: "MHR Global", + link: "https://firstadverturl.com", + }, + { + webview: "MHR 2022 - 10.45", + link: "https://secondadverturl.com", + }, + ] + end + + it "uses the first link for the parsed vacancy" do + expect(vacancy.external_advert_url).to eq "https://firstadverturl.com" + end + end + end + + context "when the same vacancy has been imported previously" do + let!(:existing_vacancy) do + create( + :vacancy, + :external, + phases: %w[secondary], + external_source: "itrent", + external_reference: "FF00067", + organisations: trust_schools, + job_title: "Class Teacher", + ) + end + + it "yields the existing vacancy with updated information" do + expect(vacancy.id).to eq(existing_vacancy.id) + expect(vacancy).to be_persisted + expect(vacancy).to be_changed + + expect(vacancy.job_title).to eq("Class Teacher") + end + end + + describe "vacancy organisation parsing" do + let(:trust_uid) { school_group.uid } + let(:school_urns) { [school1.urn] } + + let(:response_udf_body) do + JSON.parse(super()).tap { |h| + h["itrent"]["udfs"].first["schoolurns"] = school_urns + h["itrent"]["udfs"].first["trustuid"] = trust_uid + }.to_json + end + + context "when the vacancy belongs to a single school" do + let(:school_urns) { [school1.urn] } + + it "assigns the vacancy to the correct school and organisation" do + expect(vacancy.organisations.first).to eq(school1) + + expect(vacancy.external_source).to eq("itrent") + expect(vacancy.external_advert_url).to eq("https://teachvacs.itrentdemo.co.uk/ttrent_webrecruitment/wrd/run/ETREC179GF.open?WVID=2750500Wqk&VACANCY_ID=5116820Y9h") + expect(vacancy.external_reference).to eq("FF00067") + end + end + + context "when associated with an out of scope school" do + let(:out_of_scope_school) { create(:school, detailed_school_type: "Other independent school", urn: "000000") } + let(:trust_schools) { [out_of_scope_school] } + let(:school_urns) { [out_of_scope_school.urn] } + + it "does not import vacancy" do + expect(subject.count).to eq(0) + end + end + + context "when the vacancy is associated with multiple schools from a trust" do + let!(:school2) { create(:school, name: "Test School 2", urn: "222222", phase: :primary) } + let(:trust_schools) { [school1, school2].sort_by(&:created_at) } + let(:school_urns) { [school1.urn, school2.urn] } + + it "assigns the vacancy to both schools" do + expect(vacancy.organisations).to contain_exactly(school1, school2) + end + + it "assigns the vacancy job location to the first school from the group" do + expect(vacancy.readable_job_location).to eq(school1.name) + end + end + + context "when the vacancy belongs to the central trust office instead of a particular/multiple school" do + let(:school_urns) { [] } + + before do + create(:school_group, name: "Wrong Trust", uid: "54321", schools: []) + end + + it "the vacancy is valid" do + expect(vacancy).to be_valid + end + + it "assigns the vacancy to the school group" do + expect(vacancy.organisations).to contain_exactly(school_group) + end + + it "assigns the vacancy job location to the school group" do + expect(vacancy.readable_job_location).to eq(school_group.name) + end + end + + context "when the school doesn't belong to a school group" do + let(:school2) { create(:school, name: "Test School 2", urn: "222222", phase: :primary) } + let(:school_urns) { [school2.urn] } + let(:trust_uid) { nil } + + it "the vacancy is valid" do + expect(vacancy).to be_valid + end + + it "assigns the vacancy to the school" do + expect(vacancy.organisations).to contain_exactly(school2) + end + + it "assigns the vacancy job location to the school" do + expect(vacancy.readable_job_location).to eq(school2.name) + end + end + end + end +end