Skip to content

Commit 7b5948a

Browse files
[TBT-381] - Sync and Create Subscription
1 parent 0526c13 commit 7b5948a

File tree

6 files changed

+281
-106
lines changed

6 files changed

+281
-106
lines changed

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

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,35 @@
33
require 'travis/remote_vcs/user'
44
require 'travis/remote_vcs/repository'
55
require 'travis/api/v3/billing_client'
6+
require 'travis/services/assembla_user_service'
67
require_relative '../jwt_utils'
78

89
class Travis::Api::App
910
class Endpoint
11+
# Assembla integration endpoint for handling user authentication and organization setup
1012
class Assembla < Endpoint
1113
include Travis::Api::App::JWTUtils
14+
15+
REQUIRED_JWT_FIELDS = %w[name email login space_id id access_token refresh_token].freeze
16+
CLUSTER_HEADER = 'HTTP_X_ASSEMBLA_CLUSTER'.freeze
17+
18+
1219
set prefix: '/assembla'
1320
set :check_auth, false
1421

1522
before do
16-
halt 403, { error: 'Deep integration not enabled' } unless deep_integration_enabled?
17-
halt 403, { error: 'Invalid ASM cluster' } unless valid_asm_cluster?
18-
begin
19-
@jwt_payload = verify_jwt(request, Travis.config.assembla_jwt_secret)
20-
rescue JWTUtils::UnauthorizedError => e
21-
halt 401, { error: e.message }.to_json
22-
end
23+
validate_request!
2324
end
2425

25-
# POST /assembla/login
26-
# Accepts a JWT, finds or creates a user, and signs them in
2726
post '/login' do
28-
user = find_or_create_user(@jwt_payload)
29-
sync_user(user.id)
30-
create_org_subscription(user.id, @jwt_payload[:space_id])
27+
service = Travis::Services::AssemblaUserService.new(@jwt_payload)
28+
29+
user = service.find_or_create_user
30+
org = service.find_or_create_organization(user)
31+
service.create_org_subscription(user, org.id)
32+
3133

32-
{
34+
response = {
3335
user_id: user.id,
3436
login: user.login,
3537
token: user.token,
@@ -39,48 +41,28 @@ class Assembla < Endpoint
3941

4042
private
4143

42-
def deep_integration_enabled?
43-
Travis.config.deep_integration_enabled
44-
end
45-
46-
def valid_asm_cluster?
47-
allowed = Array(Travis.config.assembla_clusters.split(','))
48-
cluster = request.env['HTTP_X_ASSEMBLA_CLUSTER']
49-
allowed.include?(cluster)
44+
def validate_request!
45+
halt 403, { error: 'Deep integration not enabled' } unless deep_integration_enabled?
46+
halt 403, { error: 'Invalid ASM cluster' } unless valid_asm_cluster?
47+
@jwt_payload = verify_jwt(request)
48+
check_required_fields
5049
end
5150

52-
# Finds or creates a user based on the payload
53-
def find_or_create_user(payload)
54-
required_fields = %w[name email login space_id]
55-
missing = required_fields.select { |f| payload[f].nil? || payload[f].to_s.strip.empty? }
51+
def check_required_fields
52+
missing = REQUIRED_JWT_FIELDS.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? }
5653
unless missing.empty?
5754
halt 400, { error: 'Missing required fields', missing: missing }.to_json
5855
end
59-
attrs = {
60-
name: payload['name'],
61-
email: payload['email'],
62-
login: payload['login'],
63-
org_id: payload['space_id'],
64-
vcs_type: 'AssemblaUser'
65-
}
66-
::User.find_or_create_by!(attrs)
6756
end
6857

69-
def sync_user(user_id)
70-
Travis::RemoteVCS::User.new.sync(user_id: user_id)
71-
rescue => e
72-
halt 500, { error: 'User sync failed', details: e.message }.to_json
58+
def deep_integration_enabled?
59+
Travis.config.deep_integration_enabled
7360
end
7461

75-
def create_org_subscription(user_id, space_id)
76-
plan = 'beta_plan'
77-
client = Travis::API::V3::BillingClient.new(user_id)
78-
client.create_v2_subscription({
79-
'plan' => plan,
80-
'organization_id' => space_id,
81-
})
82-
rescue => e
83-
halt 500, { error: 'Subscription creation failed', details: e.message }.to_json
62+
def valid_asm_cluster?
63+
allowed = Array(Travis.config.assembla_clusters.to_s.split(','))
64+
cluster = request.env[CLUSTER_HEADER]
65+
!cluster.nil? && allowed.include?(cluster)
8466
end
8567
end
8668
end

lib/travis/api/app/jwt_utils.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ def extract_jwt_token(request)
44
request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
55
end
66

7-
def verify_jwt(request, secret)
7+
def verify_jwt(request)
8+
secret = Travis.config.assembla_jwt_secret
89
token = extract_jwt_token(request)
9-
raise UnauthorizedError, 'Missing JWT' unless token
10+
11+
halt 401, { error: "Missing JWT" }.to_json unless token
12+
1013
begin
1114
decoded, = JWT.decode(token, secret, true, { algorithm: 'HS256' })
1215
decoded
1316
rescue JWT::DecodeError => e
14-
raise UnauthorizedError, "Invalid JWT: #{e.message}"
17+
halt 401, { error: "Invalid JWT: #{e.message}" }.to_json
1518
end
1619
end
17-
18-
class UnauthorizedError < StandardError; end
1920
end
2021
end

lib/travis/model/user.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ class User < Travis::Model
2222
after_create :create_the_tokens
2323
before_save :track_previous_changes
2424

25-
serialize :github_scopes
25+
alias_attribute :vcs_oauth_token, :github_oauth_token
2626

27+
serialize :github_scopes
28+
serialize :vcs_oauth_token, EncryptedColumn.new
2729
serialize :github_oauth_token, Travis::Model::EncryptedColumn.new
2830

2931
before_save do
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module Travis
2+
module Services
3+
class AssemblaUserService
4+
class SyncError < StandardError; end
5+
6+
def initialize(payload)
7+
@payload = payload
8+
end
9+
10+
def find_or_create_user
11+
attrs = {
12+
vcs_id: @payload['id'],
13+
email: @payload['email'],
14+
login: @payload['login'],
15+
vcs_type: 'AssemblaUser'
16+
}
17+
18+
user = ::User.find_or_create_by!(attrs)
19+
user.update(vcs_oauth_token: @payload['refresh_token'])
20+
sync_user(user.id)
21+
user
22+
end
23+
24+
def find_or_create_organization(user)
25+
attrs = {
26+
vcs_id: @payload['space_id'],
27+
vcs_type: 'AssemblaOrganization'
28+
}
29+
user.organizations.find_or_create_by(attrs)
30+
end
31+
32+
def create_org_subscription(user, organization_id)
33+
client = Travis::API::V3::BillingClient.new(user.id)
34+
client.create_v2_subscription(subscription_params(user, organization_id))
35+
rescue => e
36+
{ error: true, details: e.message }
37+
end
38+
39+
private
40+
41+
def sync_user(user_id)
42+
Travis::RemoteVCS::User.new.sync(user_id: user_id)
43+
rescue => e
44+
raise SyncError, "Failed to sync user: #{e.message}"
45+
end
46+
47+
def subscription_params(user, organization_id)
48+
{
49+
'plan' => 'beta_plan',
50+
'organization_id' => organization_id,
51+
'billing_info' => billing_info(user),
52+
'credit_card_info' => { 'token' => nil }
53+
}
54+
end
55+
56+
def billing_info(user)
57+
{
58+
'address' => "System-generated for user #{user.login} (#{user.id})",
59+
'city' => "AutoCity-#{user.id}",
60+
'country' => 'Poland',
61+
'first_name' => user.name&.split&.first,
62+
'last_name' => user.name&.split&.last,
63+
'zip_code' => "000#{user.id}",
64+
'billing_email' => user.email
65+
}
66+
end
67+
end
68+
end
69+
end
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe Travis::Services::AssemblaUserService do
4+
let(:payload) do
5+
{
6+
'id' => '12345',
7+
'email' => '[email protected]',
8+
'login' => 'testuser',
9+
'refresh_token' => 'refresh123',
10+
'space_id' => '67890'
11+
}
12+
end
13+
14+
let(:service) { described_class.new(payload) }
15+
let(:user) { double('User', id: 1, login: 'testuser', email: '[email protected]', name: 'Test User') }
16+
let(:organization) { double('Organization', id: 2) }
17+
18+
describe '#initialize' do
19+
it 'stores the payload' do
20+
expect(service.instance_variable_get(:@payload)).to eq(payload)
21+
end
22+
end
23+
24+
describe '#find_or_create_user' do
25+
let(:expected_attrs) do
26+
{
27+
vcs_id: '12345',
28+
29+
login: 'testuser',
30+
vcs_type: 'AssemblaUser'
31+
}
32+
end
33+
34+
before do
35+
allow(::User).to receive(:find_or_create_by!).with(expected_attrs).and_return(user)
36+
allow(user).to receive(:update)
37+
allow(Travis::RemoteVCS::User).to receive(:new).and_return(double(sync: true))
38+
end
39+
40+
it 'finds or creates a user with correct attributes' do
41+
expect(::User).to receive(:find_or_create_by!).with(expected_attrs)
42+
service.find_or_create_user
43+
end
44+
45+
it 'returns the user' do
46+
result = service.find_or_create_user
47+
expect(result).to eq(user)
48+
end
49+
50+
context 'when sync fails' do
51+
it 'raises SyncError' do
52+
allow(Travis::RemoteVCS::User).to receive(:new).and_raise(StandardError.new('Sync failed'))
53+
54+
expect { service.find_or_create_user }.to raise_error(
55+
Travis::Services::AssemblaUserService::SyncError,
56+
'Failed to sync user: Sync failed'
57+
)
58+
end
59+
end
60+
end
61+
62+
describe '#find_or_create_organization' do
63+
let(:organizations_relation) { double('organizations') }
64+
let(:expected_attrs) do
65+
{
66+
vcs_id: '67890',
67+
vcs_type: 'AssemblaOrganization'
68+
}
69+
end
70+
71+
before do
72+
allow(user).to receive(:organizations).and_return(organizations_relation)
73+
end
74+
75+
it 'finds or creates organization with correct attributes' do
76+
expect(organizations_relation).to receive(:find_or_create_by).with(expected_attrs).and_return(organization)
77+
78+
result = service.find_or_create_organization(user)
79+
expect(result).to eq(organization)
80+
end
81+
end
82+
83+
describe '#create_org_subscription' do
84+
let(:billing_client) { double('BillingClient') }
85+
let(:expected_subscription_params) do
86+
{
87+
'plan' => 'beta_plan',
88+
'organization_id' => 2,
89+
'billing_info' => {
90+
'address' => 'System-generated for user testuser (1)',
91+
'city' => 'AutoCity-1',
92+
'country' => 'Poland',
93+
'first_name' => 'Test',
94+
'last_name' => 'User',
95+
'zip_code' => '0001',
96+
'billing_email' => '[email protected]'
97+
},
98+
'credit_card_info' => { 'token' => nil }
99+
}
100+
end
101+
102+
before do
103+
allow(Travis::API::V3::BillingClient).to receive(:new).with(1).and_return(billing_client)
104+
end
105+
106+
it 'creates a billing client with user id' do
107+
expect(Travis::API::V3::BillingClient).to receive(:new).with(1)
108+
allow(billing_client).to receive(:create_v2_subscription)
109+
110+
service.create_org_subscription(user, 2)
111+
end
112+
113+
it 'calls create_v2_subscription with correct params' do
114+
expect(billing_client).to receive(:create_v2_subscription).with(expected_subscription_params)
115+
116+
service.create_org_subscription(user, 2)
117+
end
118+
119+
context 'when billing client raises an error' do
120+
let(:error) { StandardError.new('Billing error') }
121+
122+
it 'returns error hash' do
123+
allow(billing_client).to receive(:create_v2_subscription).and_raise(error)
124+
125+
result = service.create_org_subscription(user, 2)
126+
expect(result).to eq({ error: true, details: 'Billing error' })
127+
end
128+
end
129+
130+
context 'when user has no name' do
131+
let(:user_without_name) { double('User', id: 1, login: 'testuser', email: '[email protected]', name: nil) }
132+
133+
it 'handles nil name gracefully' do
134+
expected_params = expected_subscription_params.dup
135+
expected_params['billing_info']['first_name'] = nil
136+
expected_params['billing_info']['last_name'] = nil
137+
138+
expect(billing_client).to receive(:create_v2_subscription).with(expected_params)
139+
140+
service.create_org_subscription(user_without_name, 2)
141+
end
142+
end
143+
end
144+
end

0 commit comments

Comments
 (0)