summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/models/jira_connect_installation_spec.rb14
-rw-r--r--spec/requests/jira_connect/installations_controller_spec.rb81
-rw-r--r--spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb25
-rw-r--r--spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb154
-rw-r--r--spec/services/jira_connect_installations/update_service_spec.rb176
-rw-r--r--spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb29
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