diff --git a/.env.example b/.env.example index df654703d..f423df83a 100644 --- a/.env.example +++ b/.env.example @@ -73,4 +73,5 @@ PARDOT_SUBSCRIPTION_URL= # https://developers.cloudflare.com/turnstile/troubleshooting/testing/. CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA -HOSTNAME=localhost \ No newline at end of file +HOSTNAME=localhost +LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN=changeme \ No newline at end of file diff --git a/Procfile b/Procfile index 394d20da5..3813fe26d 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec puma -C config/puma.rb -release: bundle exec rails db:migrate && bundle exec rake projects:create_experience_cs_examples +release: bundle exec rails db:migrate && bundle exec rake projects:create_experience_cs_examples integration_tests:dispatch worker: bundle exec good_job start --max-threads=8 diff --git a/lib/tasks/integration_tests.rake b/lib/tasks/integration_tests.rake new file mode 100644 index 000000000..06faeb68d --- /dev/null +++ b/lib/tasks/integration_tests.rake @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +namespace :integration_tests do + desc 'Trigger the learner-experience-integration-tests GitHub Actions workflow against the deployed test environment' + task dispatch: :environment do + next unless on_test_app? + + response = connection.post(dispatch_url, dispatch_payload) + raise "GitHub dispatch API returned status #{response.status}" unless response.status == 204 + rescue StandardError => e + Sentry.capture_exception(e) + end + + def dispatch_url + 'https://api.github.com/repos/RaspberryPiFoundation/learner-experience-integration-tests/dispatches' + end + + def on_test_app? + ENV.fetch('HEROKU_APP_NAME') == 'editor-api-test' + end + + def connection + Faraday.new do |faraday| + faraday.request :json + faraday.headers = { + 'Accept' => 'application/vnd.github+json', + 'Authorization' => "Bearer #{ENV.fetch('LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN')}" + } + end + end + + def dispatch_payload + sha = ENV.fetch('HEROKU_SLUG_COMMIT') + + { + event_type: 'learner-experience-test', + client_payload: { + repo: 'editor-api', + sha:, + ref: 'main', + commit_message: "https://github.com/RaspberryPiFoundation/editor-api/commit/#{sha}", + deploy_url: ENV.fetch('HOST_URL'), + triggered_at: Time.now.utc.iso8601 + } + } + end +end diff --git a/spec/lib/tasks/integration_tests_spec.rb b/spec/lib/tasks/integration_tests_spec.rb new file mode 100644 index 000000000..6ff2a89e8 --- /dev/null +++ b/spec/lib/tasks/integration_tests_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rake' + +RSpec.describe 'integration_tests', type: :task do + describe ':dispatch' do + let(:task) { Rake::Task['integration_tests:dispatch'] } + let(:dispatch_url) { 'https://api.github.com/repos/RaspberryPiFoundation/learner-experience-integration-tests/dispatches' } + let(:heroku_app_name) { 'editor-api-test' } + let(:host_url) { 'https://test-editor-api.raspberrypi.org' } + let(:sha) { SecureRandom.hex(20) } + let(:token) { 'dispatch-token' } + let(:env) do + { + 'HEROKU_APP_NAME' => heroku_app_name, + 'HEROKU_SLUG_COMMIT' => sha, + 'HOST_URL' => host_url, + 'LEARNER_EXPERIENCE_TESTS_DISPATCH_TOKEN' => token + } + end + + around { |example| ClimateControl.modify(env) { example.run } } + + before do + stub_request(:post, dispatch_url).to_return(status: 204) + end + + context 'when HEROKU_APP_NAME is editor-api-test' do + it 'dispatches the workflow with the expected payload and auth header' do + task.invoke + + expect(WebMock).to have_requested(:post, dispatch_url) + .with( + headers: { + 'Authorization' => "Bearer #{token}", + 'Accept' => 'application/vnd.github+json' + }, + body: { + event_type: 'learner-experience-test', + client_payload: { + repo: 'editor-api', + sha:, + ref: 'main', + commit_message: "https://github.com/RaspberryPiFoundation/editor-api/commit/#{sha}", + deploy_url: host_url, + triggered_at: /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ + } + } + ) + end + end + + context 'when HEROKU_APP_NAME is not editor-api-test' do + let(:heroku_app_name) { 'editor-api-staging' } + + it 'does not dispatch the workflow' do + task.invoke + + expect(WebMock).not_to have_requested(:post, dispatch_url) + end + end + + context 'when a required env var is missing' do + let(:env) { super().merge('HEROKU_SLUG_COMMIT' => nil) } + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(KeyError)) + end + end + + context 'when the request fails' do + before do + stub_request(:post, dispatch_url).to_return(status: 500) + end + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(RuntimeError)) + end + end + + context 'when an unexpected error is raised' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::ConnectionFailed, 'boom') # rubocop:disable RSpec/AnyInstance + end + + it 'does not raise' do + expect { task.invoke }.not_to raise_error + end + + it 'reports the error to Sentry' do + allow(Sentry).to receive(:capture_exception) + + task.invoke + + expect(Sentry).to have_received(:capture_exception).with(an_instance_of(Faraday::ConnectionFailed)) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 93941246a..528fd668d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -116,7 +116,7 @@ end config.before(:suite) do - Rails.application.load_tasks + Rails.application.load_tasks if Rake::Task.tasks.empty? db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first Rails.logger.debug { "Running tests in environment: #{Rails.env}" }