diff --git a/lib/travis/api/app/endpoint/assembla.rb b/lib/travis/api/app/endpoint/assembla.rb index bee33b80a..c2e91ce72 100644 --- a/lib/travis/api/app/endpoint/assembla.rb +++ b/lib/travis/api/app/endpoint/assembla.rb @@ -4,6 +4,8 @@ require 'travis/remote_vcs/repository' require 'travis/api/v3/billing_client' require 'travis/services/assembla_user_service' +require 'travis/services/assembla_notify_service' +require 'travis/remote_vcs/client' require_relative '../jwt_utils' class Travis::Api::App @@ -13,6 +15,7 @@ class Assembla < Endpoint include Travis::Api::App::JWTUtils REQUIRED_JWT_FIELDS = %w[name email login space_id repository_id id refresh_token].freeze + REQUIRED_NOTIFY_FIELDS = %w[action object id].freeze CLUSTER_HEADER = 'HTTP_X_ASSEMBLA_CLUSTER'.freeze set prefix: '/assembla' @@ -39,6 +42,19 @@ class Assembla < Endpoint } end + post '/notify' do + service = Travis::Services::AssemblaNotifyService.new(@jwt_payload) + if service.run + { + status: 200, + body: { message: 'Assembla notification processed successfully' } + } + else + Travis.logger.error("Failed to process Assembla notification") + halt 500, { error: 'Failed to process notification' } + end + end + private def validate_request! @@ -49,7 +65,8 @@ def validate_request! end def check_required_fields - missing = REQUIRED_JWT_FIELDS.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? } + required_fields = request.path_info.end_with?('/notify') ? REQUIRED_NOTIFY_FIELDS : REQUIRED_JWT_FIELDS + missing = required_fields.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? } unless missing.empty? halt 400, { error: 'Missing required fields', missing: missing } end diff --git a/lib/travis/remote_vcs/organization.rb b/lib/travis/remote_vcs/organization.rb new file mode 100644 index 000000000..3099608a5 --- /dev/null +++ b/lib/travis/remote_vcs/organization.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'travis/remote_vcs/client' + +module Travis + class RemoteVCS + class Organization < Client + def destroy(org_id:) + request(:delete, __method__, false) do |req| + req.url "organizations/#{org_id}" + end + rescue ResponseError => e + Travis.logger.error("Failed to destroy organization: #{e.message}") + false + end + end + end +end diff --git a/lib/travis/remote_vcs/repository.rb b/lib/travis/remote_vcs/repository.rb index 4d36680e8..b62c9b868 100644 --- a/lib/travis/remote_vcs/repository.rb +++ b/lib/travis/remote_vcs/repository.rb @@ -73,6 +73,15 @@ def set_perforce_ticket(repository_id:, user_id:) rescue ResponseError {} end + + def destroy(repository_id:) + request(:delete, __method__, false) do |req| + req.url "repos/#{repository_id}" + end + rescue ResponseError => e + Travis.logger.error("Failed to destroy repository: #{e.message}") + false + end end end end diff --git a/lib/travis/services/assembla_notify_service.rb b/lib/travis/services/assembla_notify_service.rb new file mode 100644 index 000000000..e0811c9d5 --- /dev/null +++ b/lib/travis/services/assembla_notify_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'travis/remote_vcs/repository' +require 'travis/remote_vcs/organization' + +module Travis + module Services + class AssemblaNotifyService + VALID_ACTIONS = %w[destroy].freeze + VALID_OBJECTS = %w[space tool].freeze + + def initialize(payload) + @action = payload[:action] + @object = payload[:object] + @object_id = payload[:id] + end + + def run + validate + case @object + when 'tool' + handle_tool_destruction + when 'space' + handle_space_destruction + else + { status: 400, body: { error: 'Unsupported object type for destruction' } } + end + end + + private + + def validate + unless VALID_ACTIONS.include?(@action) + return { status: 400, body: { error: 'Invalid action', allowed_actions: VALID_ACTIONS } } + end + + unless VALID_OBJECTS.include?(@object) + return { status: 400, body: { error: 'Invalid object type', allowed_objects: VALID_OBJECTS } } + end + end + + def handle_tool_destruction + vcs_repository = Travis::RemoteVCS::Repository.new + vcs_repository.destroy(repository_id: @object_id) + end + + def handle_space_destruction + vcs_organization = Travis::RemoteVCS::Organization.new + vcs_organization.destroy(org_id: @object_id) + end + end + end +end diff --git a/spec/lib/services/assembla_notify_service_spec.rb b/spec/lib/services/assembla_notify_service_spec.rb new file mode 100644 index 000000000..ba0ed3d33 --- /dev/null +++ b/spec/lib/services/assembla_notify_service_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' +require 'travis/services/assembla_notify_service' + +RSpec.describe Travis::Services::AssemblaNotifyService do + let(:payload) { { action: 'destroy', object: 'tool', id: '12345' } } + let(:service) { described_class.new(payload) } + let(:vcs_repository) { instance_double(Travis::RemoteVCS::Repository) } + let(:vcs_organization) { instance_double(Travis::RemoteVCS::Organization) } + + before do + allow(Travis::RemoteVCS::Repository).to receive(:new).and_return(vcs_repository) + allow(vcs_repository).to receive(:destroy) + allow(Travis::RemoteVCS::Organization).to receive(:new).and_return(vcs_organization) + allow(vcs_organization).to receive(:destroy) + allow(Travis.logger).to receive(:error) + end + + describe '#run' do + context 'with a valid payload for tool destruction' do + it 'calls handle_tool_destruction' do + expect(service).to receive(:handle_tool_destruction) + service.run + end + end + + context 'with a valid payload for space destruction' do + let(:payload) { { action: 'destroy', object: 'space', id: '67890' } } + + it 'calls handle_space_destruction' do + expect(service).to receive(:handle_space_destruction) + service.run + end + end + + context 'with an invalid object type' do + let(:payload) { { action: 'destroy', object: 'repository', id: '12345' } } + + it 'returns an error' do + result = service.run + expect(result[:status]).to eq(400) + end + end + + context 'with an unsupported object type for destruction' do + before do + stub_const("Travis::Services::AssemblaNotifyService::VALID_OBJECTS", %w[space tool unsupported]) + end + let(:payload) { { action: 'destroy', object: 'unsupported', id: '12345' } } + + it 'returns an error' do + result = service.run + expect(result[:status]).to eq(400) + end + end + end + + describe '#handle_tool_destruction' do + it 'destroys the repository using RemoteVCS' do + expect(vcs_repository).to receive(:destroy).with(repository_id: '12345') + service.send(:handle_tool_destruction) + end + end + + describe '#handle_space_destruction' do + let(:payload) { { action: 'destroy', object: 'space', id: '67890' } } + + it 'destroys the organization using RemoteVCS' do + expect(vcs_organization).to receive(:destroy).with(org_id: '67890') + service.send(:handle_space_destruction) + end + end +end diff --git a/spec/travis/remote_vcs/organization_spec.rb b/spec/travis/remote_vcs/organization_spec.rb new file mode 100644 index 000000000..f65a57598 --- /dev/null +++ b/spec/travis/remote_vcs/organization_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'travis/remote_vcs/organization' + +RSpec.describe Travis::RemoteVCS::Organization do + let(:client) { described_class.new } + let(:org_id) { '12345' } + let(:subject) { client.destroy(org_id: org_id) } + + describe '#destroy' do + it 'sends a delete request to the correct URL' do + request = double('request') + expect(request).to receive(:url).with("organizations/#{org_id}") + expect(client).to receive(:request).with(:delete, :destroy, false).and_yield(request) + subject + end + + context 'when request is successful' do + before { allow(client).to receive(:request).and_return(true) } + it { is_expected.to be true } + end + + context 'when the request fails' do + before { allow(client).to receive(:request).and_return(false) } + it { is_expected.to be false } + end + end +end diff --git a/spec/travis/remote_vcs/repository_spec.rb b/spec/travis/remote_vcs/repository_spec.rb index 0c0dba642..72a570b91 100644 --- a/spec/travis/remote_vcs/repository_spec.rb +++ b/spec/travis/remote_vcs/repository_spec.rb @@ -79,4 +79,31 @@ expect(request).to have_been_made end end + + describe '#destroy' do + subject { repository.destroy(repository_id: id) } + + context 'when the request is successful' do + let!(:request) do + stub_request(:delete, /repos\/#{id}/) + .to_return(status: 204) + end + + it 'performs a proper request' do + subject + expect(request).to have_been_made + end + end + + context 'when the request fails' do + let!(:request) do + stub_request(:delete, /repos\/#{id}/) + .to_return(status: 500) + end + + it 'returns false' do + expect(subject).to be false + end + end + end end