diff options
Diffstat (limited to 'spec')
6 files changed, 454 insertions, 25 deletions
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb index f1dc170dea9..525690fa6b7 100644 --- a/spec/models/jira_connect_installation_spec.rb +++ b/spec/models/jira_connect_installation_spec.rb @@ -124,6 +124,20 @@ RSpec.describe JiraConnectInstallation, feature_category: :integrations do end end + describe 'audience_uninstalled_event_url' do + let(:installation) { build(:jira_connect_installation) } + + subject(:audience) { installation.audience_uninstalled_event_url } + + it { is_expected.to eq(nil) } + + context 'when proxy installation' do + let(:installation) { build(:jira_connect_installation, instance_url: 'https://example.com') } + + it { is_expected.to eq('https://example.com/-/jira_connect/events/uninstalled') } + end + end + describe 'proxy?' do let(:installation) { build(:jira_connect_installation) } diff --git a/spec/requests/jira_connect/installations_controller_spec.rb b/spec/requests/jira_connect/installations_controller_spec.rb index 42abc1e4542..67544bbca2e 100644 --- a/spec/requests/jira_connect/installations_controller_spec.rb +++ b/spec/requests/jira_connect/installations_controller_spec.rb @@ -47,16 +47,18 @@ RSpec.describe JiraConnect::InstallationsController, feature_category: :integrat end describe 'PUT /-/jira_connect/installations' do - before do + subject(:do_request) do put '/-/jira_connect/installations', params: { jwt: jwt, installation: { instance_url: update_instance_url } } end - let(:update_instance_url) { 'https://example.com' } + let(:update_instance_url) { nil } context 'without JWT' do let(:jwt) { nil } it 'returns 403' do + do_request + expect(response).to have_gitlab_http_status(:forbidden) end end @@ -66,28 +68,69 @@ RSpec.describe JiraConnect::InstallationsController, feature_category: :integrat let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) } it 'returns 200' do + do_request + expect(response).to have_gitlab_http_status(:ok) end - it 'updates the instance_url' do - expect(json_response).to eq({ - 'gitlab_com' => false, - 'instance_url' => 'https://example.com' - }) - end + context 'with instance_url param' do + let(:update_instance_url) { 'https://example.com' } - context 'invalid URL' do - let(:update_instance_url) { 'invalid url' } + context 'instance response with success' do + before do + stub_request(:post, 'https://example.com/-/jira_connect/events/installed') + end - it 'returns 422 and errors', :aggregate_failures do - expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response).to eq({ - 'errors' => { - 'instance_url' => [ - 'is blocked: Only allowed schemes are http, https' - ] - } - }) + it 'updates the instance_url' do + do_request + + expect(json_response).to eq({ + 'gitlab_com' => false, + 'instance_url' => 'https://example.com' + }) + end + + it 'sends an installed event to the self-managed instance' do + do_request + + expect(WebMock).to have_requested(:post, 'https://example.com/-/jira_connect/events/installed') + end + end + + context 'instance response with error' do + before do + stub_request(:post, 'https://example.com/-/jira_connect/events/installed').to_return(status: 422) + end + + it 'returns 422 and errors', :aggregate_failures do + do_request + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to eq({ + 'errors' => { + 'instance_url' => [ + 'Could not be installed on the instance. Error response code 422' + ] + } + }) + end + end + + context 'invalid URL' do + let(:update_instance_url) { 'invalid url' } + + it 'returns 422 and errors', :aggregate_failures do + do_request + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to eq({ + 'errors' => { + 'instance_url' => [ + 'is blocked: Only allowed schemes are http, https' + ] + } + }) + end end end end diff --git a/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb b/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb index 5dbbc7fb0a2..bb96e327307 100644 --- a/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb +++ b/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb @@ -19,27 +19,40 @@ RSpec.describe JiraConnect::CreateAsymmetricJwtService, feature_category: :integ let(:public_key_id) { Atlassian::Jwt.decode(jwt_token, nil, false, algorithm: 'RS256').last['kid'] } let(:public_key_cdn) { 'https://gitlab.com/-/jira_connect/public_keys/' } + let(:event_url) { 'https://gitlab.test/-/jira_connect/events/installed' } let(:jwt_verification_claims) do { aud: 'https://gitlab.test/-/jira_connect', iss: jira_connect_installation.client_key, - qsh: Atlassian::Jwt.create_query_string_hash('https://gitlab.test/-/jira_connect/events/installed', 'POST', 'https://gitlab.test/-/jira_connect') + qsh: Atlassian::Jwt.create_query_string_hash(event_url, 'POST', 'https://gitlab.test/-/jira_connect') } end subject(:jwt_token) { service.execute } + shared_examples 'produces a valid JWT' do + it 'produces a valid JWT' do + public_key = OpenSSL::PKey.read(JiraConnect::PublicKey.find(public_key_id).key) + options = jwt_verification_claims.except(:qsh).merge({ verify_aud: true, verify_iss: true, + algorithm: 'RS256' }) + + decoded_token = Atlassian::Jwt.decode(jwt_token, public_key, true, options).first + + expect(decoded_token).to eq(jwt_verification_claims.stringify_keys) + end + end + it 'stores the public key' do expect { JiraConnect::PublicKey.find(public_key_id) }.not_to raise_error end - it 'is produces a valid JWT' do - public_key = OpenSSL::PKey.read(JiraConnect::PublicKey.find(public_key_id).key) - options = jwt_verification_claims.except(:qsh).merge({ verify_aud: true, verify_iss: true, algorithm: 'RS256' }) + it_behaves_like 'produces a valid JWT' - decoded_token = Atlassian::Jwt.decode(jwt_token, public_key, true, options).first + context 'with uninstalled event option' do + let(:service) { described_class.new(jira_connect_installation, event: :uninstalled) } + let(:event_url) { 'https://gitlab.test/-/jira_connect/events/uninstalled' } - expect(decoded_token).to eq(jwt_verification_claims.stringify_keys) + it_behaves_like 'produces a valid JWT' end end end diff --git a/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb new file mode 100644 index 00000000000..c621388a734 --- /dev/null +++ b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnectInstallations::ProxyLifecycleEventService, feature_category: :integrations do + describe '.execute' do + let(:installation) { create(:jira_connect_installation) } + + it 'creates an instance and calls execute' do + expect_next_instance_of(described_class, installation, 'installed', 'https://test.gitlab.com') do |update_service| + expect(update_service).to receive(:execute) + end + + described_class.execute(installation, 'installed', 'https://test.gitlab.com') + end + end + + describe '.new' do + let_it_be(:installation) { create(:jira_connect_installation, instance_url: nil) } + + let(:event) { :installed } + + subject(:service) { described_class.new(installation, event, 'https://test.gitlab.com') } + + it 'creates an internal duplicate of the installation and sets the instance_url' do + expect(service.instance_variable_get(:@installation).instance_url).to eq('https://test.gitlab.com') + end + + context 'with unknown event' do + let(:event) { 'test' } + + it 'raises an error' do + expect { service }.to raise_error(ArgumentError, 'Unknown event \'test\'') + end + end + end + + describe '#execute' do + let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://old_instance_url.example.com') } + + let(:service) { described_class.new(installation, evnet_type, 'https://gitlab.example.com') } + let(:service_instance_installation) { service.instance_variable_get(:@installation) } + + before do + allow_next_instance_of(JiraConnect::CreateAsymmetricJwtService) do |create_asymmetric_jwt_service| + allow(create_asymmetric_jwt_service).to receive(:execute).and_return('123456') + end + + stub_request(:post, hook_url) + end + + subject(:execute_service) { service.execute } + + shared_examples 'sends the event hook' do + it 'returns a ServiceResponse' do + expect(execute_service).to be_kind_of(ServiceResponse) + expect(execute_service[:status]).to eq(:success) + end + + it 'sends an installed event to the instance' do + execute_service + + expect(WebMock).to have_requested(:post, hook_url).with(body: expected_request_body) + end + + it 'creates the JWT token with the event and installation' do + expect_next_instance_of( + JiraConnect::CreateAsymmetricJwtService, + service_instance_installation, + event: evnet_type + ) do |create_asymmetric_jwt_service| + expect(create_asymmetric_jwt_service).to receive(:execute).and_return('123456') + end + + expect(execute_service[:status]).to eq(:success) + end + + context 'and the instance responds with an error' do + before do + stub_request(:post, hook_url).to_return( + status: 422, + body: 'Error message', + headers: {} + ) + end + + it 'returns an error ServiceResponse', :aggregate_failures do + expect(execute_service).to be_kind_of(ServiceResponse) + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:message]).to eq( { type: :response_error, code: 422 } ) + end + + it 'logs the error response' do + expect(Gitlab::IntegrationsLogger).to receive(:info).with( + integration: 'JiraConnect', + message: 'Proxy lifecycle event received error response', + event_type: evnet_type, + status_code: 422, + body: 'Error message' + ) + + execute_service + end + end + + context 'and the request raises an error' do + before do + allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message') + end + + it 'returns an error ServiceResponse', :aggregate_failures do + expect(execute_service).to be_kind_of(ServiceResponse) + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:message]).to eq( + { + type: :network_error, + message: 'Connection refused - error message' + } + ) + end + end + end + + context 'when installed event' do + let(:evnet_type) { :installed } + let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/installed' } + let(:expected_request_body) do + { + clientKey: installation.client_key, + sharedSecret: installation.shared_secret, + baseUrl: installation.base_url, + jwt: '123456', + eventType: 'installed' + } + end + + it_behaves_like 'sends the event hook' + end + + context 'when uninstalled event' do + let(:evnet_type) { :uninstalled } + let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/uninstalled' } + let(:expected_request_body) do + { + clientKey: installation.client_key, + jwt: '123456', + eventType: 'uninstalled' + } + end + + it_behaves_like 'sends the event hook' + end + end +end diff --git a/spec/services/jira_connect_installations/update_service_spec.rb b/spec/services/jira_connect_installations/update_service_spec.rb new file mode 100644 index 00000000000..000c854b495 --- /dev/null +++ b/spec/services/jira_connect_installations/update_service_spec.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integrations do + describe '.execute' do + it 'creates an instance and calls execute' do + expect_next_instance_of(described_class, 'param1', 'param2') do |update_service| + expect(update_service).to receive(:execute) + end + + described_class.execute('param1', 'param2') + end + end + + describe '#execute' do + let_it_be_with_reload(:installation) { create(:jira_connect_installation) } + let(:update_params) { { client_key: 'new_client_key' } } + + subject(:execute_service) { described_class.new(installation, update_params).execute } + + it 'returns a ServiceResponse' do + expect(execute_service).to be_kind_of(ServiceResponse) + expect(execute_service[:status]).to eq(:success) + end + + it 'updates the installation' do + expect { execute_service }.to change { installation.client_key }.to('new_client_key') + end + + it 'returns a successful result' do + expect(execute_service.success?).to eq(true) + end + + context 'and model validation fails' do + let(:update_params) { { instance_url: 'invalid' } } + + it 'returns an error result' do + expect(execute_service.error?).to eq(true) + expect(execute_service.message).to eq(installation.errors) + end + end + + context 'and instance_url is updated' do + let(:update_params) { { instance_url: 'https://gitlab.example.com' } } + + it 'sends an installed event to the instance and updates instance_url' do + expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, +'https://gitlab.example.com') do |proxy_lifecycle_events_service| + expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success)) + end + + expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async) + + execute_service + + expect(installation.instance_url).to eq(update_params[:instance_url]) + end + + context 'and the instance installation cannot be created' do + before do + allow_next_instance_of( + JiraConnectInstallations::ProxyLifecycleEventService, + installation, + :installed, + 'https://gitlab.example.com' + ) do |proxy_lifecycle_events_service| + allow(proxy_lifecycle_events_service).to receive(:execute).and_return( + ServiceResponse.error( + message: { + type: :response_error, + code: '422' + } + ) + ) + end + end + + it 'does not change instance_url' do + expect { execute_service }.not_to change { installation.instance_url } + end + + it 'returns an error message' do + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:message]).to eq( + { + instance_url: ["Could not be installed on the instance. Error response code 422"] + } + ) + end + + context 'and the installation had a previous instance_url' do + let(:installation) { build(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') } + + it 'does not send the uninstalled hook to the previous instance_url' do + expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async) + + execute_service + end + end + + context 'when failure because of a network error' do + before do + allow_next_instance_of( + JiraConnectInstallations::ProxyLifecycleEventService, + installation, + :installed, + 'https://gitlab.example.com' + ) do |proxy_lifecycle_events_service| + allow(proxy_lifecycle_events_service).to receive(:execute).and_return( + ServiceResponse.error( + message: { + type: :network_error, + message: 'Connection refused - error message' + } + ) + ) + end + end + + it 'returns an error message' do + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:message]).to eq( + { + instance_url: ["Could not be installed on the instance. Network error"] + } + ) + end + end + end + + context 'and the installation had a previous instance_url' do + let_it_be_with_reload(:installation) { create(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') } + + before do + stub_request(:post, 'https://other_gitlab.example.com/-/jira_connect/events/uninstalled') + end + + it 'starts an async worker to send an uninstalled event to the previous instance' do + expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com') + + expect_next_instance_of( + JiraConnectInstallations::ProxyLifecycleEventService, + installation, :installed, + 'https://gitlab.example.com' + ) do |proxy_lifecycle_events_service| + expect(proxy_lifecycle_events_service).to receive(:execute) + .and_return(ServiceResponse.new(status: :success)) + end + + execute_service + + expect(installation.instance_url).to eq(update_params[:instance_url]) + end + + context 'and the new instance_url is empty' do + let(:update_params) { { instance_url: nil } } + + it 'starts an async worker to send an uninstalled event to the previous instance' do + expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com') + + execute_service + + expect(installation.instance_url).to eq(nil) + end + + it 'does not send an installed event' do + expect(JiraConnectInstallations::ProxyLifecycleEventService).not_to receive(:new) + + execute_service + end + end + end + end + end +end diff --git a/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb b/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb new file mode 100644 index 00000000000..d8ca8dee54d --- /dev/null +++ b/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnect::SendUninstalledHookWorker, feature_category: :integrations do + describe '#perform' do + let_it_be(:jira_connect_installation) { create(:jira_connect_installation) } + let(:instance_url) { 'http://example.com' } + let(:attempts) { 3 } + let(:service_response) { ServiceResponse.new(status: :success) } + let(:job_args) { [jira_connect_installation.id, instance_url] } + + before do + allow(JiraConnectInstallations::ProxyLifecycleEventService).to receive(:execute).and_return(service_response) + end + + include_examples 'an idempotent worker' do + it 'calls the ProxyLifecycleEventService service' do + expect(JiraConnectInstallations::ProxyLifecycleEventService).to receive(:execute).with( + jira_connect_installation, + :uninstalled, + instance_url + ).twice + + subject + end + end + end +end |