diff --git a/lib/travis/api/v3.rb b/lib/travis/api/v3.rb index 221df59b..8c8be745 100644 --- a/lib/travis/api/v3.rb +++ b/lib/travis/api/v3.rb @@ -34,6 +34,7 @@ module Travis ServerError = Error .create(status: 500) NotImplemented = ServerError .create('request not (yet) implemented', status: 501) RequestLimitReached = ClientError .create('request limit reached for resource', status: 429) + AlreadySyncing = ClientError .create('sync already in progress', status: 409) end end end diff --git a/lib/travis/api/v3/access_control/generic.rb b/lib/travis/api/v3/access_control/generic.rb index 0e31d723..c232f879 100644 --- a/lib/travis/api/v3/access_control/generic.rb +++ b/lib/travis/api/v3/access_control/generic.rb @@ -83,6 +83,10 @@ module Travis::API::V3 unrestricted_api? end + def user_writable?(user) + self.user == user + end + def repository_visible?(repository) return true if unrestricted_api? and not repository.private? private_repository_visible?(repository) diff --git a/lib/travis/api/v3/queries/user.rb b/lib/travis/api/v3/queries/user.rb index 47bd7f02..72db91a1 100644 --- a/lib/travis/api/v3/queries/user.rb +++ b/lib/travis/api/v3/queries/user.rb @@ -1,6 +1,7 @@ module Travis::API::V3 class Queries::User < Query - params :id, :login, :email, :github_id + setup_sidekiq(:user_sync, queue: :user_sync, class_name: "Travis::GithubSync::Workers::SyncUser") + params :id, :login, :email, :github_id, :is_syncing def find return Models::User.find_by_id(id) if id @@ -17,5 +18,12 @@ module Travis::API::V3 User.find_by_email(email) end end + + def sync(user) + raise AlreadySyncing if user.is_syncing? + perform_async(:user_sync, user.id) + user.update_column(:is_syncing, true) + user + end end end diff --git a/lib/travis/api/v3/query.rb b/lib/travis/api/v3/query.rb index e4ea4f09..97d803ba 100644 --- a/lib/travis/api/v3/query.rb +++ b/lib/travis/api/v3/query.rb @@ -1,6 +1,18 @@ module Travis::API::V3 class Query - @@sidekiq_cache = Tool::ThreadLocal.new + @@sidekiq_queue = {} + + def self.sidekiq_queue(identifier) + @@sidekiq_queue[identifier] ||= [ + "Travis::Sidekiq::#{identifier.to_s.camelcase}".freeze, + identifier.to_s.pluralize.freeze + ] + end + + def self.setup_sidekiq(identifier, queue: nil, class_name: nil) + sidekiq_queue(identifier)[0] = class_name if class_name + sidekiq_queue(identifier)[1] = queue if queue + end # generate from eval to avoid additional string allocations on every params access @@params_accessor = <<-RUBY @@ -90,11 +102,7 @@ module Travis::API::V3 end def perform_async(identifier, *args) - class_name, queue = @@sidekiq_cache[identifier] ||= [ - "Travis::Sidekiq::#{identifier.to_s.camelcase}".freeze, - identifier.to_s.pluralize.freeze - ] - + class_name, queue = Query.sidekiq_queue(identifier) ::Sidekiq::Client.push('queue'.freeze => queue, 'class'.freeze => class_name, 'args'.freeze => args) end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 37eb7699..d58d8970 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -118,6 +118,7 @@ module Travis::API::V3 route '/user' get :current get :find, '/{user.id}' + post :sync, '/{user.id}/sync' end end diff --git a/lib/travis/api/v3/services/user/sync.rb b/lib/travis/api/v3/services/user/sync.rb new file mode 100644 index 00000000..5ece301b --- /dev/null +++ b/lib/travis/api/v3/services/user/sync.rb @@ -0,0 +1,12 @@ +module Travis::API::V3 + class Services::User::Sync < Service + + def run! + raise LoginRequired unless access_control.logged_in? or access_control.full_access? + raise NotFound unless user = find(:user) + access_control.permissions(user).sync! + + query.sync(user) + end + end +end diff --git a/spec/v3/services/user/sync_spec.rb b/spec/v3/services/user/sync_spec.rb new file mode 100644 index 00000000..316d41f9 --- /dev/null +++ b/spec/v3/services/user/sync_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Travis::API::V3::Services::User::Sync do + let(:user) { Travis::API::V3::Models::User.find_by_login('svenfuchs') } + let(:user2) { Travis::API::V3::Models::User.create(login: 'carlad', is_syncing: true) } + let(:sidekiq_payload) { JSON.load(Sidekiq::Client.last['args'].last.to_json) } + let(:sidekiq_params) { Sidekiq::Client.last['args'].last.deep_symbolize_keys } + + before do + user.update_attribute(:is_syncing, false) + Travis::Features.stubs(:owner_active?).returns(true) + @original_sidekiq = Sidekiq::Client + Sidekiq.send(:remove_const, :Client) # to avoid a warning + Sidekiq::Client = [] + end + + after do + Sidekiq.send(:remove_const, :Client) # to avoid a warning + Sidekiq::Client = @original_sidekiq + end + + describe "not authenticated" do + before { post("/v3/user/#{user.id}/sync") } + example { expect(last_response.status).to be == 403 } + example { expect(JSON.load(body)).to be == { + "@type" => "error", + "error_type" => "login_required", + "error_message" => "login required" + }} + end + + describe "missing user, authenticated" do + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + before { post("/v3/user/9999999999/sync", {}, headers) } + + example { expect(last_response.status).to be == 404 } + example { expect(JSON.load(body)).to be == { + "@type" => "error", + "error_type" => "not_found", + "error_message" => "user not found (or insufficient access)", + "resource_type" => "user" + }} + end + + describe "existing user, matches current user " do + let(:params) {{}} + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + before { Travis::API::V3::Models::Permission.create(user: user) } + before { post("/v3/user/#{user.id}/sync", params, headers) } + + example { expect(last_response.status).to be == 200 } + example { expect(JSON.load(body).to_s).to include( + "@type", + "user", + "@href", + "@representation", + "sync", + "is_syncing", + "id", + "true") + } + + example { expect(sidekiq_payload).to be == user.id } + + example { expect(Sidekiq::Client.last['queue']).to be == :user_sync } + example { expect(Sidekiq::Client.last['class']).to be == 'Travis::GithubSync::Workers::SyncUser' } + end + + describe "existing user, current user does not have sync access " do + let(:params) {{}} + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + before { Travis::API::V3::Models::Permission.create(user: user) } + before { post("/v3/user/#{user2.id}/sync", params, headers) } + + example { expect(last_response.status).to be == 403 } + example { expect(JSON.load(body)).to be == { + "@type" => "error", + "error_type" => "insufficient_access", + "error_message" => "operation requires sync access to user", + "resource_type" => "user", + "permission" => "sync", + "user" => { + "@type" => "user", + "@href" => "/user/#{user2.id}", + "@representation"=> "minimal", + "id" => user2.id, + "login" => "carlad" + } + }} + end + + describe "existing user, authorized, user already syncing " do + let(:params) {{}} + let(:token) { Travis::Api::App::AccessToken.create(user: user2, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + before { Travis::API::V3::Models::Permission.create(user: user) } + before { post("/v3/user/#{user2.id}/sync", params, headers) } + + example { expect(last_response.status).to be == 409 } + example { expect(JSON.load(body)).to be == { + "@type" => "error", + "error_type" => "already_syncing", + "error_message" => "sync already in progress" + } + } + end +end