Skip to content

Commit ce7369e

Browse files
[TBT-392] Soft delete orgs and repo (#1399)
* [TBT-392] Soft delete orgs and repo * removed pry * Added logging * Refactored code * Fixed specs * Fixed organization spec * Stub spec URL * Restore Repo and orgs * Stub request * Stub request * Used restrict instead of destroy * revert admin ID * fixed specs * use hash string * Added default scope deleted_at * fixed specs --------- Co-authored-by: Dominik <dominik.alberski@gmail.com>
1 parent b9b24dc commit ce7369e

File tree

11 files changed

+278
-1
lines changed

11 files changed

+278
-1
lines changed

lib/travis/api/app/endpoint/assembla.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
require 'travis/remote_vcs/repository'
55
require 'travis/api/v3/billing_client'
66
require 'travis/services/assembla_user_service'
7+
require 'travis/services/assembla_notify_service'
8+
require 'travis/remote_vcs/client'
79
require_relative '../jwt_utils'
810

911
class Travis::Api::App
@@ -13,6 +15,7 @@ class Assembla < Endpoint
1315
include Travis::Api::App::JWTUtils
1416

1517
REQUIRED_JWT_FIELDS = %w[name email login space_id repository_id id refresh_token].freeze
18+
REQUIRED_NOTIFY_FIELDS = %w[action object id].freeze
1619
CLUSTER_HEADER = 'HTTP_X_ASSEMBLA_CLUSTER'.freeze
1720

1821
set prefix: '/assembla'
@@ -39,6 +42,19 @@ class Assembla < Endpoint
3942
}
4043
end
4144

45+
post '/notify' do
46+
service = Travis::Services::AssemblaNotifyService.new(@jwt_payload)
47+
if service.run
48+
{
49+
status: 200,
50+
body: { message: 'Assembla notification processed successfully' }
51+
}
52+
else
53+
Travis.logger.error("Failed to process Assembla notification")
54+
halt 500, { error: 'Failed to process notification' }
55+
end
56+
end
57+
4258
private
4359

4460
def validate_request!
@@ -49,7 +65,8 @@ def validate_request!
4965
end
5066

5167
def check_required_fields
52-
missing = REQUIRED_JWT_FIELDS.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? }
68+
required_fields = request.path_info.end_with?('/notify') ? REQUIRED_NOTIFY_FIELDS : REQUIRED_JWT_FIELDS
69+
missing = required_fields.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? }
5370
unless missing.empty?
5471
halt 400, { error: 'Missing required fields', missing: missing }
5572
end

lib/travis/api/v3/models/organization.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module Travis::API::V3
44
class Models::Organization < Model
5+
default_scope { where(deleted_at: nil) }
56
has_many :memberships
67
has_many :users, through: :memberships
78
has_one :beta_migration_request

lib/travis/api/v3/models/repository.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module Travis::API::V3
44
class Models::Repository < Model
5+
default_scope { where(deleted_at: nil) }
56
has_many :commits, dependent: :delete_all
67
has_many :requests, dependent: :delete_all
78
has_many :branches, -> { order('branches.id DESC'.freeze) }, dependent: :delete_all

lib/travis/model/organization.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
class Organization < Travis::Model
55
self.table_name = 'organizations'
6+
default_scope { where(deleted_at: nil) }
7+
68
has_many :memberships
79
has_many :users, :through => :memberships
810
has_many :repositories, :as => :owner

lib/travis/model/repository.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# hooks on Github.
1313
class Repository < Travis::Model
1414
self.table_name = 'repositories'
15+
default_scope { where(deleted_at: nil) }
1516
include Travis::ScopeAccess
1617

1718
require 'travis/model/repository/status_image'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
require 'travis/remote_vcs/client'
4+
5+
module Travis
6+
class RemoteVCS
7+
class Organization < Client
8+
def destroy(org_id:)
9+
request(:delete, __method__, false) do |req|
10+
req.url "organizations/#{org_id}"
11+
end
12+
rescue ResponseError => e
13+
Travis.logger.error("Failed to destroy organization: #{e.message}")
14+
false
15+
end
16+
17+
def restore(org_id:)
18+
request(:post, __method__, false) do |req|
19+
req.url "organizations/#{org_id}/restore"
20+
end
21+
rescue ResponseError => e
22+
Travis.logger.error("Failed to restore organization: #{e.message}")
23+
false
24+
end
25+
end
26+
end
27+
end

lib/travis/remote_vcs/repository.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,25 @@ def set_perforce_ticket(repository_id:, user_id:)
7373
rescue ResponseError
7474
{}
7575
end
76+
77+
def destroy(repository_id:, vcs_type:)
78+
request(:delete, __method__, false) do |req|
79+
req.url "repos/#{repository_id}"
80+
req.params['vcs_type'] = vcs_type
81+
end
82+
rescue ResponseError => e
83+
Travis.logger.error("Failed to destroy repository: #{e.message}")
84+
false
85+
end
86+
87+
def restore(repository_id:)
88+
request(:post, __method__, false) do |req|
89+
req.url "repos/#{repository_id}/restore"
90+
end
91+
rescue ResponseError => e
92+
Travis.logger.error("Failed to restore repository: #{e.message}")
93+
false
94+
end
7695
end
7796
end
7897
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# frozen_string_literal: true
2+
3+
require 'travis/remote_vcs/repository'
4+
require 'travis/remote_vcs/organization'
5+
6+
module Travis
7+
module Services
8+
class AssemblaNotifyService
9+
VALID_ACTIONS = %w[restrict restore].freeze
10+
VALID_OBJECTS = %w[space tool].freeze
11+
12+
def initialize(payload)
13+
@action = payload['action']
14+
@object = payload['object']
15+
@object_id = payload['id']
16+
end
17+
18+
def run
19+
return false unless validate
20+
21+
case @object
22+
when 'tool'
23+
handle_tool_action
24+
when 'space'
25+
handle_space_action
26+
else
27+
false
28+
end
29+
end
30+
31+
private
32+
33+
def validate
34+
unless VALID_ACTIONS.include?(@action)
35+
Travis.logger.error("Invalid action: #{@action}. Allowed actions: #{VALID_ACTIONS.join(', ')}")
36+
return false
37+
end
38+
39+
unless VALID_OBJECTS.include?(@object)
40+
Travis.logger.error("Invalid object type: #{@object}. Allowed objects: #{VALID_OBJECTS.join(', ')}")
41+
return false
42+
end
43+
44+
true
45+
end
46+
47+
def handle_tool_action
48+
vcs_repository = Travis::RemoteVCS::Repository.new
49+
case @action
50+
when 'restrict'
51+
vcs_repository.destroy(repository_id: @object_id, vcs_type: 'AssemblaRepository')
52+
when 'restore'
53+
vcs_repository.restore(repository_id: @object_id)
54+
end
55+
end
56+
57+
def handle_space_action
58+
vcs_organization = Travis::RemoteVCS::Organization.new
59+
case @action
60+
when 'restrict'
61+
vcs_organization.destroy(org_id: @object_id)
62+
when 'restore'
63+
vcs_organization.restore(org_id: @object_id)
64+
end
65+
end
66+
end
67+
end
68+
end
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
require 'spec_helper'
2+
require 'travis/services/assembla_notify_service'
3+
4+
RSpec.describe Travis::Services::AssemblaNotifyService do
5+
let(:payload) { { 'action' => 'restrict', 'object' => 'tool', 'id' => '12345' } }
6+
let(:service) { described_class.new(payload) }
7+
let(:vcs_repository) { instance_double(Travis::RemoteVCS::Repository) }
8+
let(:vcs_organization) { instance_double(Travis::RemoteVCS::Organization) }
9+
10+
before do
11+
allow(Travis::RemoteVCS::Repository).to receive(:new).and_return(vcs_repository)
12+
allow(vcs_repository).to receive(:destroy).with(any_args)
13+
allow(vcs_repository).to receive(:restore)
14+
allow(Travis::RemoteVCS::Organization).to receive(:new).and_return(vcs_organization)
15+
allow(vcs_organization).to receive(:destroy)
16+
allow(vcs_organization).to receive(:restore)
17+
allow(Travis.logger).to receive(:error)
18+
end
19+
20+
describe '#run' do
21+
context 'with a valid payload for tool restriction' do
22+
it 'calls destroy on the vcs_repository' do
23+
expect(vcs_repository).to receive(:destroy).with(repository_id: '12345', vcs_type: 'AssemblaRepository')
24+
service.run
25+
end
26+
end
27+
28+
context 'with a valid payload for tool restoration' do
29+
let(:payload) { { 'action' => 'restore', 'object' => 'tool', 'id' => '12345' } }
30+
it 'calls restore on the vcs_repository' do
31+
expect(vcs_repository).to receive(:restore).with(repository_id: '12345')
32+
service.run
33+
end
34+
end
35+
36+
context 'with a valid payload for space restriction' do
37+
let(:payload) { { 'action' => 'restrict', 'object' => 'space', 'id' => '67890' } }
38+
39+
it 'calls destroy on the vcs_organization' do
40+
expect(vcs_organization).to receive(:destroy).with(org_id: '67890')
41+
service.run
42+
end
43+
end
44+
45+
context 'with a valid payload for space restoration' do
46+
let(:payload) { { 'action' => 'restore', 'object' => 'space', 'id' => '67890' } }
47+
48+
it 'calls restore on the vcs_organization' do
49+
expect(vcs_organization).to receive(:restore).with(org_id: '67890')
50+
service.run
51+
end
52+
end
53+
54+
context 'with an invalid object type' do
55+
let(:payload) { { 'action' => 'restrict', 'object' => 'repository', 'id' => '12345' } }
56+
57+
it 'returns false and logs an error' do
58+
expect(service.run).to be_falsey
59+
expect(Travis.logger).to have_received(:error).with("Invalid object type: repository. Allowed objects: space, tool")
60+
end
61+
end
62+
63+
context 'with an invalid action type' do
64+
let(:payload) { { 'action' => 'modify', 'object' => 'tool', 'id' => '12345' } }
65+
66+
it 'returns false and logs an error' do
67+
expect(service.run).to be_falsey
68+
expect(Travis.logger).to have_received(:error).with("Invalid action: modify. Allowed actions: restrict, restore")
69+
end
70+
end
71+
72+
context 'with an unsupported object type for an action' do
73+
before do
74+
stub_const("Travis::Services::AssemblaNotifyService::VALID_OBJECTS", %w[space tool unsupported])
75+
end
76+
let(:payload) { { 'action' => 'restrict', 'object' => 'unsupported', 'id' => '12345' } }
77+
78+
it 'returns false without logging an error for the action' do
79+
expect(service.run).to be_falsey
80+
expect(vcs_repository).not_to receive(:destroy)
81+
expect(vcs_repository).not_to receive(:restore)
82+
expect(vcs_organization).not_to receive(:destroy)
83+
expect(vcs_organization).not_to receive(:restore)
84+
end
85+
end
86+
end
87+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'spec_helper'
2+
require 'travis/remote_vcs/organization'
3+
4+
RSpec.describe Travis::RemoteVCS::Organization do
5+
let(:client) { described_class.new }
6+
let(:org_id) { '12345' }
7+
let(:subject) { client.destroy(org_id: org_id) }
8+
9+
describe '#destroy' do
10+
it 'sends a delete request to the correct URL' do
11+
request = double('request')
12+
expect(request).to receive(:url).with("organizations/#{org_id}")
13+
expect(client).to receive(:request).with(:delete, :destroy, false).and_yield(request)
14+
subject
15+
end
16+
17+
context 'when request is successful' do
18+
before { allow(client).to receive(:request).and_return(true) }
19+
it { is_expected.to be true }
20+
end
21+
22+
context 'when the request fails' do
23+
before { allow(client).to receive(:request).and_return(false) }
24+
it { is_expected.to be false }
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)