Files
codeocean/spec/lib/runner/strategy/poseidon_spec.rb
2021-11-01 17:12:53 +01:00

345 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe Runner::Strategy::Poseidon do
let(:runner_id) { FactoryBot.attributes_for(:runner)[:runner_id] }
let(:execution_environment) { FactoryBot.create :ruby }
let(:poseidon) { described_class.new(runner_id, execution_environment) }
let(:error_message) { 'test error message' }
let(:response_body) { nil }
# All requests handle a BadRequest (400) response the same way.
shared_examples 'BadRequest (400) error handling' do
context 'when Poseidon returns BadRequest (400)' do
let(:response_body) { {message: error_message}.to_json }
let(:response_status) { 400 }
it 'raises an error' do
allow(Runner).to receive(:destroy).with(runner_id)
expect { action.call }.to raise_error(Runner::Error::BadRequest, /#{error_message}/)
end
end
end
# Only #copy_files and #execute_command destroy the runner locally in case
# of a BadRequest (400) response.
shared_examples 'BadRequest (400) destroys local runner' do
context 'when Poseidon returns BadRequest (400)' do
let(:response_body) { {message: error_message}.to_json }
let(:response_status) { 400 }
it 'destroys the runner locally' do
expect(Runner).to receive(:destroy).with(runner_id)
expect { action.call }.to raise_error(Runner::Error::BadRequest)
end
end
end
# All requests handle a Unauthorized (401) response the same way.
shared_examples 'Unauthorized (401) error handling' do
context 'when Poseidon returns Unauthorized (401)' do
let(:response_status) { 401 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::Unauthorized)
end
end
end
# All requests except creation handle a NotFound (404) response the same way.
shared_examples 'NotFound (404) error handling' do
context 'when Poseidon returns NotFound (404)' do
let(:response_status) { 404 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::RunnerNotFound)
end
end
end
# All requests handle an InternalServerError (500) response the same way.
shared_examples 'InternalServerError (500) error handling' do
context 'when Poseidon returns InternalServerError (500)' do
shared_examples 'InternalServerError (500) with error code' do |error_code, error_class|
let(:response_status) { 500 }
let(:response_body) { {message: error_message, errorCode: error_code}.to_json }
it 'raises an error' do
expect { action.call }.to raise_error(error_class) do |error|
expect(error.message).to match(/#{error_message}/)
expect(error.message).to match(/#{error_code}/)
end
end
end
context 'when error code is nomad overload' do
include_examples(
'InternalServerError (500) with error code',
described_class.error_nomad_overload, Runner::Error::NotAvailable
)
end
context 'when error code is not nomad overload' do
include_examples(
'InternalServerError (500) with error code',
described_class.error_unknown, Runner::Error::InternalServerError
)
end
end
end
# All requests handle an unknown response status the same way.
shared_examples 'unknown response status error handling' do
context 'when Poseidon returns an unknown response status' do
let(:response_status) { 1337 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse, /#{response_status}/)
end
end
end
# All requests handle a Faraday error the same way.
shared_examples 'Faraday error handling' do
context 'when Faraday throws an error' do
# The response status is not needed in this context but the describes block this context is embedded
# into expect this variable to be set in order to properly stub requests to the runner management.
let(:response_status) { -1 }
it 'raises an error' do
%i[post patch delete].each {|message| allow(Faraday).to receive(message).and_raise(Faraday::TimeoutError) }
expect { action.call }.to raise_error(Runner::Error::FaradayError)
end
end
end
describe '::sync_environment' do
let(:action) { -> { described_class.sync_environment(execution_environment) } }
let(:execution_environment) { FactoryBot.create(:ruby) }
it 'makes the correct request to Poseidon' do
allow(Faraday).to receive(:put).and_return(Faraday::Response.new(status: 201))
action.call
expect(Faraday).to have_received(:put) do |url, body, headers|
expect(url).to match(%r{execution-environments/#{execution_environment.id}\z})
expect(body).to eq(execution_environment.to_json)
expect(headers).to include({'Content-Type' => 'application/json'})
end
end
shared_examples 'returns true when the api request was successful' do |status|
it "returns true on status #{status}" do
allow(Faraday).to receive(:put).and_return(Faraday::Response.new(status: status))
expect(action.call).to be_truthy
end
end
shared_examples 'returns false when the api request failed' do |status|
it "returns false on status #{status}" do
allow(Faraday).to receive(:put).and_return(Faraday::Response.new(status: status))
expect(action.call).to be_falsey
end
end
[201, 204].each do |status|
include_examples 'returns true when the api request was successful', status
end
[400, 500].each do |status|
include_examples 'returns false when the api request failed', status
end
it 'returns false if Faraday raises an error' do
allow(Faraday).to receive(:put).and_raise(Faraday::TimeoutError)
expect(action.call).to be_falsey
end
end
describe '::request_from_management' do
let(:action) { -> { described_class.request_from_management(execution_environment) } }
let!(:request_runner_stub) do
WebMock
.stub_request(:post, "#{described_class.config[:url]}/runners")
.with(
body: {
executionEnvironmentId: execution_environment.id,
inactivityTimeout: described_class.config[:unused_runner_expiration_time].seconds,
},
headers: {'Content-Type' => 'application/json'}
)
.to_return(body: response_body, status: response_status)
end
context 'when Poseidon returns Ok (200) with an id' do
let(:response_body) { {runnerId: runner_id}.to_json }
let(:response_status) { 200 }
it 'successfully requests Poseidon' do
action.call
expect(request_runner_stub).to have_been_requested.once
end
it 'returns the received runner id' do
id = action.call
expect(id).to eq(runner_id)
end
end
context 'when Poseidon returns Ok (200) without an id' do
let(:response_body) { {}.to_json }
let(:response_status) { 200 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse)
end
end
context 'when Poseidon returns Ok (200) with invalid JSON' do
let(:response_body) { '{hello}' }
let(:response_status) { 200 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse)
end
end
include_examples 'BadRequest (400) error handling'
include_examples 'Unauthorized (401) error handling'
context 'when Poseidon returns NotFound (404)' do
let(:response_status) { 404 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::EnvironmentNotFound)
end
end
include_examples 'InternalServerError (500) error handling'
include_examples 'unknown response status error handling'
include_examples 'Faraday error handling'
end
describe '#execute_command' do
let(:command) { 'ls' }
let(:action) { -> { poseidon.send(:execute_command, command) } }
let(:websocket_url) { 'ws://ws.example.com/path/to/websocket' }
let!(:execute_command_stub) do
WebMock
.stub_request(:post, "#{described_class.config[:url]}/runners/#{runner_id}/execute")
.with(
body: {command: command, timeLimit: execution_environment.permitted_execution_time},
headers: {'Content-Type' => 'application/json'}
)
.to_return(body: response_body, status: response_status)
end
context 'when Poseidon returns Ok (200) with a websocket url' do
let(:response_status) { 200 }
let(:response_body) { {websocketUrl: websocket_url}.to_json }
it 'schedules an execution in Poseidon' do
action.call
expect(execute_command_stub).to have_been_requested.once
end
it 'returns the url' do
url = action.call
expect(url).to eq(websocket_url)
end
end
context 'when Poseidon returns Ok (200) without a websocket url' do
let(:response_body) { {}.to_json }
let(:response_status) { 200 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse)
end
end
context 'when Poseidon returns Ok (200) with invalid JSON' do
let(:response_body) { '{hello}' }
let(:response_status) { 200 }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse)
end
end
include_examples 'BadRequest (400) error handling'
include_examples 'BadRequest (400) destroys local runner'
include_examples 'Unauthorized (401) error handling'
include_examples 'NotFound (404) error handling'
include_examples 'InternalServerError (500) error handling'
include_examples 'unknown response status error handling'
include_examples 'Faraday error handling'
end
describe '#destroy_at_management' do
let(:action) { -> { poseidon.destroy_at_management } }
let!(:destroy_stub) do
WebMock
.stub_request(:delete, "#{described_class.config[:url]}/runners/#{runner_id}")
.to_return(body: response_body, status: response_status)
end
context 'when Poseidon returns NoContent (204)' do
let(:response_status) { 204 }
it 'deletes the runner from Poseidon' do
action.call
expect(destroy_stub).to have_been_requested.once
end
end
include_examples 'Unauthorized (401) error handling'
include_examples 'NotFound (404) error handling'
include_examples 'InternalServerError (500) error handling'
include_examples 'unknown response status error handling'
include_examples 'Faraday error handling'
end
describe '#copy_files' do
let(:file_content) { 'print("Hello World!")' }
let(:file) { FactoryBot.build(:file, content: file_content) }
let(:action) { -> { poseidon.copy_files([file]) } }
let(:encoded_file_content) { Base64.strict_encode64(file.content) }
let!(:copy_files_stub) do
WebMock
.stub_request(:patch, "#{described_class.config[:url]}/runners/#{runner_id}/files")
.with(
body: {copy: [{path: file.filepath, content: encoded_file_content}]},
headers: {'Content-Type' => 'application/json'}
)
.to_return(body: response_body, status: response_status)
end
context 'when Poseidon returns NoContent (204)' do
let(:response_status) { 204 }
it 'sends the files to Poseidon' do
action.call
expect(copy_files_stub).to have_been_requested.once
end
end
include_examples 'BadRequest (400) error handling'
include_examples 'BadRequest (400) destroys local runner'
include_examples 'Unauthorized (401) error handling'
include_examples 'NotFound (404) error handling'
include_examples 'InternalServerError (500) error handling'
include_examples 'unknown response status error handling'
include_examples 'Faraday error handling'
end
describe '#attach_to_execution' do
# TODO: add tests here
let(:command) { 'ls' }
let(:event_loop) { Runner::EventLoop.new }
let(:action) { -> { poseidon.attach_to_execution(command, event_loop) } }
let(:websocket_url) { 'ws://ws.example.com/path/to/websocket' }
end
end