-
Notifications
You must be signed in to change notification settings - Fork 161
Assembla Deep Integration: Travis API Endpoint #1380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
447f8e3
1934be6
e461de2
0526c13
7b5948a
ab5b727
a1bf5c3
1a2c602
5f79e3b
fb82a6a
daaafb7
db9cadb
bf4eb60
65a72ca
e4227ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
require 'travis/api/app' | ||
require 'jwt' | ||
require 'travis/remote_vcs/user' | ||
require 'travis/remote_vcs/repository' | ||
require 'travis/api/v3/billing_client' | ||
require 'travis/services/assembla_user_service' | ||
require_relative '../jwt_utils' | ||
|
||
class Travis::Api::App | ||
class Endpoint | ||
# Assembla integration endpoint for handling user authentication and organization setup | ||
class Assembla < Endpoint | ||
include Travis::Api::App::JWTUtils | ||
|
||
REQUIRED_JWT_FIELDS = %w[name email login space_id id refresh_token].freeze | ||
CLUSTER_HEADER = 'HTTP_X_ASSEMBLA_CLUSTER'.freeze | ||
|
||
set prefix: '/assembla' | ||
set :check_auth, false | ||
|
||
before do | ||
validate_request! | ||
end | ||
|
||
post '/login' do | ||
service = Travis::Services::AssemblaUserService.new(@jwt_payload) | ||
|
||
user = service.find_or_create_user | ||
org = service.find_or_create_organization(user) | ||
service.create_org_subscription(user, org.id) | ||
|
||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
user_id: user.id, | ||
login: user.login, | ||
token: user.token | ||
} | ||
end | ||
|
||
private | ||
|
||
def validate_request! | ||
halt 403, { error: 'Deep integration not enabled' } unless deep_integration_enabled? | ||
halt 403, { error: 'Invalid ASM cluster' } unless valid_asm_cluster? | ||
@jwt_payload = verify_jwt(request) | ||
check_required_fields | ||
end | ||
|
||
def check_required_fields | ||
missing = REQUIRED_JWT_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 } | ||
Comment on lines
+43
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe create a config and add all error messages to that so it is easy to see all errors this integration can generate and then use that config where you are raising the error. |
||
end | ||
end | ||
|
||
def deep_integration_enabled? | ||
Travis.config.deep_integration_enabled | ||
end | ||
|
||
def valid_asm_cluster? | ||
allowed = Array(Travis.config.assembla_clusters.to_s.split(',')) | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cluster = request.env[CLUSTER_HEADER] | ||
allowed.include?(cluster) | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
end | ||
end | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
class Travis::Api::App | ||
module JWTUtils | ||
def extract_jwt_token(request) | ||
request.env['HTTP_AUTHORIZATION']&.split&.last | ||
end | ||
|
||
def verify_jwt(request) | ||
token = extract_jwt_token(request) | ||
|
||
halt 401, { error: "Missing JWT" } unless token | ||
|
||
begin | ||
decoded, = JWT.decode(token, Travis.config.assembla_jwt_secret, true, algorithm: 'HS256' ) | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
decoded | ||
rescue JWT::DecodeError => e | ||
halt 401, { error: "Invalid JWT: #{e.message}" } | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
module Travis | ||
module Services | ||
class AssemblaUserService | ||
class SyncError < StandardError; end | ||
|
||
BILLING_COUNTRY = 'Poland' | ||
BILLING_ADDRESS = "System-generated for user %{login} (%{id})" | ||
BILLING_CITY = "AutoCity-%{id}" | ||
BILLING_ZIP = "000%{id}" | ||
|
||
def initialize(payload) | ||
@payload = payload | ||
end | ||
|
||
def find_or_create_user | ||
user = ::User.find_or_initialize_by( | ||
name: @payload['name'], | ||
vcs_id: @payload['id'], | ||
email: @payload['email'], | ||
login: @payload['login'], | ||
vcs_type: 'AssemblaUser' | ||
) | ||
user.vcs_oauth_token = @payload['refresh_token'] | ||
user.save! | ||
sync_user(user.id) | ||
user | ||
end | ||
|
||
def find_or_create_organization(user) | ||
user.organizations.find_or_create_by!( | ||
vcs_id: @payload['space_id'], | ||
vcs_type: 'AssemblaOrganization' | ||
) | ||
end | ||
|
||
def create_org_subscription(user, organization_id) | ||
billing_client = Travis::API::V3::BillingClient.new(user.id) | ||
billing_client.create_v2_subscription(subscription_params(user, organization_id)) | ||
rescue => e | ||
{ error: true, details: e.message } | ||
end | ||
|
||
private | ||
|
||
def sync_user(user_id) | ||
Travis::RemoteVCS::User.new.sync(user_id: user_id) | ||
rescue => e | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we rescue only specific errors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It think we should know what went wrong while sycning, filtering can overlook other services related errors. |
||
raise SyncError, "Failed to sync user: #{e.message}" | ||
end | ||
|
||
def subscription_params(user, organization_id) | ||
{ | ||
'plan' => Travis.config.beta_plan_name, | ||
'organization_id' => organization_id, | ||
'billing_info' => billing_info(user), | ||
'credit_card_info' => { 'token' => nil } | ||
} | ||
end | ||
|
||
def billing_info(user) | ||
{ | ||
'address' => BILLING_ADDRESS % { login: user.login, id: user.id }, | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'city' => BILLING_CITY % { id: user.id }, | ||
'country' => BILLING_COUNTRY, | ||
'first_name' => user.name&.split&.first, | ||
'last_name' => user.name&.split&.last, | ||
'zip_code' => BILLING_ZIP % { id: user.id }, | ||
'billing_email' => user.email | ||
} | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
require 'spec_helper' | ||
require 'factory_bot' | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
RSpec.describe Travis::Services::AssemblaUserService do | ||
let(:payload) do | ||
{ | ||
'id' => '12345', | ||
'name' => 'Test User', | ||
'email' => '[email protected]', | ||
'login' => 'testuser', | ||
'refresh_token' => 'refresh123', | ||
'space_id' => '67890' | ||
} | ||
end | ||
|
||
let(:service) { described_class.new(payload) } | ||
let(:user) { FactoryBot.create(:user, vcs_id: payload['id'], email: payload['email'], login: payload['login'], name: payload['name']) } | ||
let(:organization) { FactoryBot.create(:org, vcs_id: payload['space_id'], vcs_type: 'AssemblaOrganization') } | ||
|
||
describe '#find_or_create_user' do | ||
let(:expected_attrs) do | ||
{ | ||
vcs_id: payload['id'], | ||
email: payload['email'], | ||
name: payload['name'], | ||
login: payload['login'], | ||
vcs_type: 'AssemblaUser' | ||
} | ||
end | ||
|
||
before do | ||
allow(Travis::RemoteVCS::User).to receive(:new).and_return(double(sync: true)) | ||
end | ||
|
||
it 'finds or creates a user with correct attributes' do | ||
service_user = service.find_or_create_user | ||
expect(service_user.login).to eq(expected_attrs[:login]) | ||
expect(service_user.email).to eq(expected_attrs[:email]) | ||
expect(service_user.name).to eq(expected_attrs[:name]) | ||
expect(service_user.vcs_id).to eq(expected_attrs[:vcs_id]) | ||
end | ||
|
||
context 'when sync fails' do | ||
it 'raises SyncError' do | ||
allow(Travis::RemoteVCS::User).to receive(:new).and_raise(StandardError.new('Sync failed')) | ||
|
||
expect { service.find_or_create_user }.to raise_error( | ||
Travis::Services::AssemblaUserService::SyncError, | ||
'Failed to sync user: Sync failed' | ||
) | ||
end | ||
end | ||
end | ||
|
||
describe '#find_or_create_organization' do | ||
let(:expected_attrs) do | ||
{ | ||
vcs_id: payload['space_id'], | ||
vcs_type: 'AssemblaOrganization' | ||
} | ||
end | ||
|
||
it 'finds or creates organization with correct attributes' do | ||
service_org = service.find_or_create_organization(user) | ||
|
||
expect(service_org.vcs_type).to eq(expected_attrs[:vcs_type]) | ||
expect(service_org.vcs_id).to eq(expected_attrs[:vcs_id]) | ||
end | ||
end | ||
|
||
describe '#create_org_subscription' do | ||
let(:billing_client) { double('BillingClient') } | ||
|
||
before do | ||
allow(Travis::API::V3::BillingClient).to receive(:new).with(user.id).and_return(billing_client) | ||
end | ||
|
||
context 'when billing client raises an error' do | ||
let(:error) { StandardError.new('Billing error') } | ||
|
||
it 'returns error hash' do | ||
allow(billing_client).to receive(:create_v2_subscription).and_raise(error) | ||
|
||
result = service.create_org_subscription(user, organization.id) | ||
expect(result[:error]).to be_truthy | ||
expect(result[:details]).to be_present | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
end | ||
end | ||
mshahzaib-travis marked this conversation as resolved.
Show resolved
Hide resolved
|
Uh oh!
There was an error while loading. Please reload this page.