v3: full request create implementation and specs

This commit is contained in:
Konstantin Haase 2015-03-04 18:35:25 +01:00
parent 714f365e15
commit 7c6dc9a54c
16 changed files with 352 additions and 55 deletions

View File

@ -30,6 +30,7 @@ module Travis
WrongCredentials = ClientError .create('access denied', status: 403)
LoginRequired = ClientError .create('login required', status: 403)
InsufficientAccess = ClientError .create(status: 403)
PushAccessRequired = InsufficientAccess .create('push access required')
WrongParams = ClientError .create('wrong parameters')
ServerError = Error .create(status: 500)
NotImplemented = ServerError .create('request not (yet) implemented', status: 501)

View File

@ -16,8 +16,6 @@ module Travis::API::V3
full_access? or !!user
end
protected
def full_access?
config.full_access
end

View File

@ -11,6 +11,10 @@ module Travis::API::V3
full_access? or dispatch(object)
end
def writable?(object)
full_access? or dispatch(object)
end
def user
end
@ -18,6 +22,10 @@ module Travis::API::V3
false
end
def full_access?
false
end
protected
def build_visible?(build)
@ -41,10 +49,6 @@ module Travis::API::V3
false
end
def full_access?
false
end
def public_api?
!Travis.config.private_api
end

View File

@ -17,6 +17,10 @@ module Travis::API::V3
protected
def repository_writable?(repository)
permission?(:push, repository)
end
def private_repository_visible?(repository)
permission?(:pull, repository)
end

View File

@ -0,0 +1,18 @@
module Travis::API::V3
class Queries::Request < Query
params :message, :branch, :config, prefix: :request
def schedule(repository, user)
raise ServerError, 'repository does not have a github_id'.freeze unless repository.github_id
raise WrongParams, 'missing user'.freeze unless user and user.id
perform_async(:build_request, type: 'api'.freeze, credentials: {}, payload: {
repository: { id: repository.github_id },
user: { id: user.id },
message: message,
branch: branch || repository.default_branch_name,
config: config || {}
})
end
end
end

View File

@ -1,14 +1,6 @@
module Travis::API::V3
class Queries::Requests < Query
def schedule_for(repository)
perform_async(:build_request, type: 'api'.freeze, payload: payload, credentials: {})
end
def find(repository)
end
def payload
raise NotImplementedError
end
end
end

View File

@ -0,0 +1,10 @@
module Travis::API::V3
class Queries::User < Query
params :id
def find
return Models::User.find_by_id(id) if id
raise WrongParams, 'missing user.id'.freeze
end
end
end

View File

@ -10,12 +10,12 @@ module Travis::API::V3
return @%<name>s if defined? @%<name>s
return @%<name>s = @params['%<prefix>s.%<name>s'.freeze] if @params.include? '%<prefix>s.%<name>s'.freeze
return @%<name>s = @params['%<prefix>s'.freeze]['%<name>s'.freeze] if @params.include? '%<prefix>s'.freeze and @params['%<prefix>s'.freeze].is_a? Hash
return @%<name>s = @params['%<name>s'.freeze] if @params['@type'.freeze].nil? or @params['@type'.freeze] == '%<prefix>s'.freeze
return @%<name>s = @params['%<name>s'.freeze] if (@params['@type'.freeze] || @main_type) == '%<prefix>s'.freeze
@%<name>s = nil
end
def %<name>s!
%<name>s or raise WrongParams, 'missing %<prefix>s.%<name>s'.freeze, missing_field: '%<prefix>s.%<name>s'.freeze
%<name>s or raise WrongParams, 'missing %<prefix>s.%<name>s'.freeze
end
RUBY
@ -24,13 +24,14 @@ module Travis::API::V3
list.each { |e| class_eval(@@params_accessor % { name: e, prefix: prefix }) }
end
attr_reader :params
attr_reader :params, :main_type
def initialize(params)
def initialize(params, main_type)
@params = params
@main_type = main_type.to_s
end
def perform_async(worker, *args)
def perform_async(identifier, *args)
class_name, queue, client = @@sidekiq_cache[identifier] ||= [
"Travis::Sidekiq::#{identifier.to_s.camelcase}".freeze,
identifier.to_s.pluralize.freeze

View File

@ -1,5 +1,8 @@
module Travis::API::V3
module Renderer
PRIMITIVE = [String, Symbol, Numeric, true, false, nil]
private_constant :PRIMITIVE
EXPANDER_CACHE = Tool::ThreadLocal.new
private_constant :EXPANDER_CACHE
@ -31,6 +34,21 @@ module Travis::API::V3
expander.call(args)
end
def render_model(model, type: model.class.name[/[^:]+$/].to_sym, mode: :minimal, **options)
Renderer[type].render(model, mode, **options)
end
def render_value(value, **options)
case value
when Hash then value.map { |k, v| [k, render_value(v)] }.to_h
when Array then value.map { |v | render_value(v) }
when *PRIMITIVE then value
when Time then value.strftime('%Y-%m-%dT%H:%M:%SZ')
when Model then render_model(value, **options)
else raise ArgumentError, 'cannot render %p (%p)' % [value.class, value]
end
end
private
def generate_expander(route, key_mapping)

View File

@ -1,5 +1,5 @@
module Travis::API::V3
module Renderer::Error
module Renderer::Accepted
extend self
def render(type, **)

View File

@ -7,7 +7,7 @@ module Travis::API::V3
:@type => 'error'.freeze,
:error_type => error.type,
:error_message => error.message,
**error.payload
**Renderer.render_value(error.payload)
}
end
end

View File

@ -1,8 +1,5 @@
module Travis::API::V3
class Renderer::ModelRenderer
PRIMITIVE = [String, Symbol, Numeric, true, false, nil]
private_constant :PRIMITIVE
def self.type(type = nil)
@type = type if type
@type = name[/[^:]+$/].underscore.to_sym unless defined? @type # allows setting type to nil
@ -43,23 +40,8 @@ module Travis::API::V3
result[:@href] = href if href
fields = self.class.representations.fetch(representation)
fields.each { |field| result[field] = render_value(send(field)) }
fields.each { |field| result[field] = Renderer.render_value(send(field), script_name: script_name) }
result
end
def render_model(model, type: model.class.name[/[^:]+$/].to_sym, mode: :minimal, **options)
Renderer[type].render(model, mode, script_name: script_name, **options)
end
def render_value(value)
case value
when Hash then value.map { |k, v| [k, render_value(v)] }.to_h
when Array then value.map { |v | render_value(v) }
when *PRIMITIVE then value
when Time then value.strftime('%Y-%m-%dT%H:%M:%SZ')
when Model then render_model(value)
else raise ArgumentError, 'cannot render %p (%p)' % [value.class, value]
end
end
end
end

View File

@ -16,7 +16,7 @@ module Travis::API::V3
end
def query(type = self.class.result_type)
@queries[type] ||= Queries[type].new(params)
@queries[type] ||= Queries[type].new(params, self.class.result_type)
end
def find(type = self.class.result_type, *args)
@ -41,6 +41,12 @@ module Travis::API::V3
result
end
def params_for?(prefix)
return true if params['@type'.freeze] == prefix
return true if params[prefix].is_a? Hash
params.keys.any? { |key| key.start_with? "#{prefix}." }
end
def accepted(type = self.class.result_type)
Result.new(:accepted, type, status: 202)
end

View File

@ -1,9 +1,17 @@
module Travis::API::V3
class Services::Requests::Create < Service
result_type :request
def run
not_implemented
query.schedule_for(find(:repository))
accepted
raise LoginRequired unless access_control.logged_in? or access_control.full_access?
raise NotFound unless repository = find(:repository)
raise PushAccessRequired, repository: repository unless access_control.writable?(repository)
user = find(:user) if access_control.full_access? and params_for? 'user'.freeze
user ||= access_control.user
query.schedule(repository, user)
accepted(:request)
end
end
end

View File

@ -88,7 +88,7 @@ describe Travis::API::V3::Services::Repository::Find do
before { Permission.create(repository: repo, user: repo.owner, pull: true) }
before { repo.update_attribute(:private, true) }
before { get("/v3/repo/#{repo.id}", {}, headers) }
before { repo.update_attribute(:private, false) }
after { repo.update_attribute(:private, false) }
example { expect(last_response).to be_ok }
example { expect(JSON.load(body)).to be == {
"@type" => "repository",

View File

@ -0,0 +1,255 @@
require 'spec_helper'
describe Travis::API::V3::Services::Requests::Create do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
let(:sidekiq_payload) { Sidekiq::Client.last['args'].last[:payload] }
before do
@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/repo/#{repo.id}/requests") }
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 repository, authenticated" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/9999999999/requests", {}, headers) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, no push access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/#{repo.id}/requests", {}, headers) }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "push_access_required",
"error_message" => "push access required",
"repository" => {
"@type" => "repository",
"@href" => "/repo/#{repo.id}",
"id" => repo.id,
"slug" => "svenfuchs/minimal"}
}}
end
describe "private repository, no access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { repo.update_attribute(:private, true) }
before { post("/v3/repo/#{repo.id}/requests", {}, headers) }
after { repo.update_attribute(:private, false) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, push access" do
let(:params) {{}}
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { Travis::API::V3::Models::Permission.create(repository: repo, user: repo.owner, push: true) }
before { post("/v3/repo/#{repo.id}/requests", params, headers) }
example { expect(last_response.status).to be == 202 }
example { expect(JSON.load(body)).to be == {
"@type" => "pending",
"resource_type" => "request"
}}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
example { expect(Sidekiq::Client.last['queue']).to be == 'build_requests' }
example { expect(Sidekiq::Client.last['class']).to be == 'Travis::Sidekiq::BuildRequest' }
describe "setting id has no effect" do
let(:params) {{ id: 42 }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
end
describe "setting repository has no effect" do
let(:params) {{ repository: { id: 42 } }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
end
describe "setting user has no effect" do
let(:params) {{ user: { id: 42 } }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
end
describe "overriding config" do
let(:params) {{ config: { script: 'true' } }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: { 'script' => 'true' }
}}
end
describe "overriding message" do
let(:params) {{ message: 'example' }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: 'example',
branch: 'master',
config: {}
}}
end
describe "overriding branch" do
let(:params) {{ branch: 'example' }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'example',
config: {}
}}
end
describe "overriding branch (in request)" do
let(:params) {{ request: { branch: 'example' } }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'example',
config: {}
}}
end
describe "overriding branch (with request prefix)" do
let(:params) {{ "request.branch" => 'example' }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'example',
config: {}
}}
end
describe "overriding branch (with request type)" do
let(:params) {{ "@type" => "request", "branch" => 'example' }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'example',
config: {}
}}
end
describe "overriding branch (with wrong type)" do
let(:params) {{ "@type" => "repository", "branch" => 'example' }}
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
end
end
describe "existing repository, application with full access" do
let(:app_name) { 'travis-example' }
let(:app_secret) { '12345678' }
let(:sign_opts) { "a=#{app_name}" }
let(:signature) { OpenSSL::HMAC.hexdigest('sha256', app_secret, sign_opts) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "signature #{sign_opts}:#{signature}" }}
before { Travis.config.applications = { app_name => { full_access: true, secret: app_secret }}}
before { post("/v3/repo/#{repo.id}/requests", params, headers) }
describe 'without setting user' do
let(:params) {{}}
example { expect(last_response.status).to be == 400 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "wrong_params",
"error_message" => "missing user"
}}
end
describe 'setting user' do
let(:params) {{ user: { id: repo.owner.id } }}
example { expect(last_response.status).to be == 202 }
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'master',
config: {}
}}
end
describe 'setting branch' do
let(:params) {{ user: { id: repo.owner.id }, branch: 'example' }}
example { expect(last_response.status).to be == 202 }
example { expect(sidekiq_payload).to be == {
repository: { id: repo.id },
user: { id: repo.owner.id },
message: nil,
branch: 'example',
config: {}
}}
end
end
end