Allow to handle settings collections
This commit is contained in:
parent
196b0815f5
commit
1b524071f4
1
Gemfile
1
Gemfile
|
@ -9,6 +9,7 @@ gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs', require: nil, ref: '
|
||||||
gem 'sinatra'
|
gem 'sinatra'
|
||||||
gem 'sinatra-contrib', require: nil #github: 'sinatra/sinatra-contrib', require: nil
|
gem 'sinatra-contrib', require: nil #github: 'sinatra/sinatra-contrib', require: nil
|
||||||
|
|
||||||
|
gem 'active_model_serializers'
|
||||||
gem 'unicorn'
|
gem 'unicorn'
|
||||||
gem 'sentry-raven', github: 'getsentry/raven-ruby'
|
gem 'sentry-raven', github: 'getsentry/raven-ruby'
|
||||||
gem 'yard-sinatra', github: 'rkh/yard-sinatra'
|
gem 'yard-sinatra', github: 'rkh/yard-sinatra'
|
||||||
|
|
|
@ -96,6 +96,8 @@ GEM
|
||||||
rack-cache (~> 1.2)
|
rack-cache (~> 1.2)
|
||||||
rack-test (~> 0.6.1)
|
rack-test (~> 0.6.1)
|
||||||
sprockets (~> 2.2.1)
|
sprockets (~> 2.2.1)
|
||||||
|
active_model_serializers (0.8.1)
|
||||||
|
activemodel (>= 3.0)
|
||||||
activemodel (3.2.17)
|
activemodel (3.2.17)
|
||||||
activesupport (= 3.2.17)
|
activesupport (= 3.2.17)
|
||||||
builder (~> 3.0.0)
|
builder (~> 3.0.0)
|
||||||
|
@ -279,6 +281,7 @@ PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
active_model_serializers
|
||||||
bunny (~> 0.8.0)
|
bunny (~> 0.8.0)
|
||||||
dalli
|
dalli
|
||||||
database_cleaner (~> 0.8.0)
|
database_cleaner (~> 0.8.0)
|
||||||
|
|
|
@ -18,6 +18,7 @@ require 'sidekiq'
|
||||||
require 'metriks/reporter/logger'
|
require 'metriks/reporter/logger'
|
||||||
require 'travis/support/log_subscriber/active_record_metrics'
|
require 'travis/support/log_subscriber/active_record_metrics'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
|
require 'travis/api/serializer'
|
||||||
|
|
||||||
# Rack class implementing the HTTP API.
|
# Rack class implementing the HTTP API.
|
||||||
# Instances respond to #call.
|
# Instances respond to #call.
|
||||||
|
@ -113,7 +114,11 @@ module Travis::Api
|
||||||
use Travis::Api::App::Middleware::Metriks
|
use Travis::Api::App::Middleware::Metriks
|
||||||
use Travis::Api::App::Middleware::Rewrite
|
use Travis::Api::App::Middleware::Rewrite
|
||||||
|
|
||||||
Endpoint.subclasses.each { |e| map(e.prefix) { run(e.new) } }
|
Endpoint.subclasses.each do |e|
|
||||||
|
next if e == SettingsEndpoint # TODO: add something like abstract? method to check if
|
||||||
|
# class should be registered
|
||||||
|
map(e.prefix) { run(e.new) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -41,15 +41,6 @@ class Travis::Api::App
|
||||||
respond_with service(:find_repo, params.merge(schema: 'cc'))
|
respond_with service(:find_repo, params.merge(schema: 'cc'))
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/:id/settings/ssh_keys' do
|
|
||||||
settings = service(:find_repo_settings, params).run
|
|
||||||
if settings
|
|
||||||
respond_with({ settings: settings.obfuscated }, version: :v2)
|
|
||||||
else
|
|
||||||
status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get settings for a given repository
|
# Get settings for a given repository
|
||||||
#
|
#
|
||||||
get '/:id/settings' do
|
get '/:id/settings' do
|
||||||
|
|
94
lib/travis/api/app/endpoint/setting_endpoint.rb
Normal file
94
lib/travis/api/app/endpoint/setting_endpoint.rb
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
require 'travis/api/app'
|
||||||
|
|
||||||
|
class Travis::Api::App
|
||||||
|
class SettingsEndpoint < Endpoint
|
||||||
|
set(:prefix) { "/settings/" << name[/[^:]+$/].underscore }
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# This method checks if class based on a given name exists or creates
|
||||||
|
# a new SettingsEndpoint subclass, which will be then used as an endpoint
|
||||||
|
def subclass(name)
|
||||||
|
class_name = name.to_s.camelize
|
||||||
|
if Travis::Api::App.const_defined?(class_name)
|
||||||
|
Travis::Api::App.const_get(class_name)
|
||||||
|
else
|
||||||
|
klass = create_settings_class(name)
|
||||||
|
Travis::Api::App.const_set(class_name, klass)
|
||||||
|
klass
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_settings_class(name)
|
||||||
|
klass = Class.new(self) do
|
||||||
|
define_method(:name) { name }
|
||||||
|
get("/", scope: :private) do index end
|
||||||
|
get("/:id", scope: :private) do show end
|
||||||
|
post("/", scope: :private) do create end
|
||||||
|
patch("/:id", scope: :private) do update end
|
||||||
|
delete("/:id", scope: :private) do destroy end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rails style methods for easy overriding
|
||||||
|
def index
|
||||||
|
respond_with(collection, type: name, verson: :v2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
respond_with(record, type: singular_name, verson: :v2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
record.update(params[singular_name])
|
||||||
|
if record.valid?
|
||||||
|
repo_settings.save
|
||||||
|
respond_with(record, type: singular_name, version: :v2)
|
||||||
|
else
|
||||||
|
status 422
|
||||||
|
respond_with(record, type: :validation_error, version: :v2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
record = collection.create(params[singular_name])
|
||||||
|
if record.valid?
|
||||||
|
repo_settings.save
|
||||||
|
respond_with(record, type: singular_name, version: :v2)
|
||||||
|
else
|
||||||
|
status 422
|
||||||
|
respond_with(record, type: :validation_error, version: :v2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
record = collection.destroy(params[:id]) || record_not_found
|
||||||
|
repo_settings.save
|
||||||
|
respond_with(record, type: singular_name, verson: :v2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def singular_name
|
||||||
|
name.to_s.singularize
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection
|
||||||
|
@collection ||= repo_settings.send(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method can't be called "settings" because it clashes with
|
||||||
|
# Sinatra's method
|
||||||
|
def repo_settings
|
||||||
|
@settings ||= begin
|
||||||
|
service(:find_repo_settings, id: params['repository_id'].to_i).run
|
||||||
|
end || halt(404, error: "Couldn't find repository")
|
||||||
|
end
|
||||||
|
|
||||||
|
def record
|
||||||
|
collection.find(params[:id]) || record_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_not_found
|
||||||
|
halt(404, { error: "Could not find a requested setting" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,7 @@ class Travis::Api::App
|
||||||
class Rewrite < Middleware
|
class Rewrite < Middleware
|
||||||
FORMAT = %r(\.(json|xml|png|txt|atom|svg)$)
|
FORMAT = %r(\.(json|xml|png|txt|atom|svg)$)
|
||||||
V1_REPO_URL = %r(^(/[^/]+/[^/]+(?:/builds(?:/[\d]+)?|/cc)?)$)
|
V1_REPO_URL = %r(^(/[^/]+/[^/]+(?:/builds(?:/[\d]+)?|/cc)?)$)
|
||||||
|
SETTINGS_URL = %r(^/settings)
|
||||||
|
|
||||||
helpers :accept
|
helpers :accept
|
||||||
|
|
||||||
|
@ -37,7 +38,9 @@ class Travis::Api::App
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect_v1_named_repo_path
|
def redirect_v1_named_repo_path
|
||||||
force_redirect("/repositories#{$1}.#{env['travis.format']}") if request.path =~ V1_REPO_URL
|
if request.path =~ V1_REPO_URL && request.path !~ SETTINGS_URL
|
||||||
|
force_redirect("/repositories#{$1}.#{env['travis.format']}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def force_redirect(path)
|
def force_redirect(path)
|
||||||
|
|
|
@ -27,7 +27,13 @@ class Travis::Api::App
|
||||||
end
|
end
|
||||||
|
|
||||||
def result
|
def result
|
||||||
builder ? builder.new(resource, params).data : basic_type_resource
|
if builder
|
||||||
|
p = params
|
||||||
|
p[:root] = options[:type] if options[:type]
|
||||||
|
builder.new(resource, p).data
|
||||||
|
else
|
||||||
|
basic_type_resource
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def builder
|
def builder
|
||||||
|
|
23
lib/travis/api/serializer.rb
Normal file
23
lib/travis/api/serializer.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require 'active_model_serializers'
|
||||||
|
|
||||||
|
module Travis
|
||||||
|
module Api
|
||||||
|
class Serializer < ActiveModel::Serializer
|
||||||
|
def data
|
||||||
|
as_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ArraySerializer < ActiveModel::ArraySerializer
|
||||||
|
def data
|
||||||
|
as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(resource, options)
|
||||||
|
options[:each_serializer] ||= Travis::Api::V2::Http.const_get(options[:root].to_s.singularize.camelize)
|
||||||
|
super(resource, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ module Travis
|
||||||
require 'travis/api/v2/http/request'
|
require 'travis/api/v2/http/request'
|
||||||
require 'travis/api/v2/http/ssl_key'
|
require 'travis/api/v2/http/ssl_key'
|
||||||
require 'travis/api/v2/http/user'
|
require 'travis/api/v2/http/user'
|
||||||
|
require 'travis/api/v2/http/validation_error'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
1
lib/travis/api/v2/http/error.rb
Normal file
1
lib/travis/api/v2/http/error.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
39
lib/travis/api/v2/http/validation_error.rb
Normal file
39
lib/travis/api/v2/http/validation_error.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
module Travis
|
||||||
|
module Api
|
||||||
|
module V2
|
||||||
|
module Http
|
||||||
|
class ValidationError
|
||||||
|
attr_reader :resource
|
||||||
|
|
||||||
|
def initialize(resource, options = {})
|
||||||
|
@resource = resource
|
||||||
|
end
|
||||||
|
|
||||||
|
def data
|
||||||
|
response = {
|
||||||
|
message: 'Validation failed'
|
||||||
|
}
|
||||||
|
resource.errors.to_hash.each do |name, errors|
|
||||||
|
response['errors'] ||= []
|
||||||
|
errors.each do |error_code|
|
||||||
|
response['errors'] << { field: name, code: code(error_code) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def code(error_code)
|
||||||
|
case error_code
|
||||||
|
when :blank
|
||||||
|
'missing_field'
|
||||||
|
else
|
||||||
|
error_code.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
178
spec/integration/settings_endpoint_spec.rb
Normal file
178
spec/integration/settings_endpoint_spec.rb
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Travis::Api::App::SettingsEndpoint do
|
||||||
|
let(:repo) { Repository.by_slug('svenfuchs/minimal').first }
|
||||||
|
let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
model_class = Class.new(Repository::Settings::Model) do
|
||||||
|
field :name
|
||||||
|
field :secret, encrypted: true
|
||||||
|
|
||||||
|
validates :name, presence: true
|
||||||
|
validates :secret, presence: true
|
||||||
|
end
|
||||||
|
collection_class = Class.new(Repository::Settings::Collection) do
|
||||||
|
model model_class
|
||||||
|
end
|
||||||
|
Repository::Settings.class_eval do
|
||||||
|
register :items, collection_class
|
||||||
|
end
|
||||||
|
serializer_class = Class.new(Travis::Api::Serializer) do
|
||||||
|
attributes :id, :name
|
||||||
|
end
|
||||||
|
Travis::Api::V2::Http.const_set(:Item, serializer_class)
|
||||||
|
Travis::Api::V2::Http.const_set(:Items, Travis::Api::ArraySerializer)
|
||||||
|
|
||||||
|
add_settings_endpoint :items
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Travis::Api::App.send :remove_const, :Items
|
||||||
|
Travis::Api::V2::Http.send :remove_const, :Items
|
||||||
|
Travis::Api::V2::Http.send :remove_const, :Item
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'with authenticated user' do
|
||||||
|
let(:user) { User.where(login: 'svenfuchs').first }
|
||||||
|
let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: -1) }
|
||||||
|
let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json', 'HTTP_AUTHORIZATION' => "token #{token}" } }
|
||||||
|
|
||||||
|
before { user.permissions.create!(:repository_id => repo.id, :admin => true, :push => true) }
|
||||||
|
|
||||||
|
describe 'GET /items/:id' do
|
||||||
|
it 'returns an item' do
|
||||||
|
settings = repo.settings
|
||||||
|
item = settings.items.create(name: 'an item', secret: 'TEH SECRET')
|
||||||
|
settings.save
|
||||||
|
|
||||||
|
response = get '/settings/items/' + item.id, { repository_id: repo.id }, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['item']['name'].should == 'an item'
|
||||||
|
json['item']['id'].should == item.id
|
||||||
|
json['item'].should_not have_key('secret')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 if item can\'t be found' do
|
||||||
|
response = get '/settings/items/123', { repository_id: repo.id }, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['error'].should == "Could not find a requested setting"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /items' do
|
||||||
|
it 'returns items list' do
|
||||||
|
settings = repo.settings
|
||||||
|
settings.items.create(name: 'an item', secret: 'TEH SECRET')
|
||||||
|
settings.save
|
||||||
|
|
||||||
|
response = get '/settings/items', { repository_id: repo.id }, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['items'].should have(1).items
|
||||||
|
item = json['items'].first
|
||||||
|
item['name'].should == 'an item'
|
||||||
|
item['id'].should_not be_nil
|
||||||
|
item.should_not have_key('secret')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /items' do
|
||||||
|
it 'creates a new item' do
|
||||||
|
params = { repository_id: repo.id, item: { name: 'foo', secret: 'TEH SECRET' } }
|
||||||
|
response = post '/settings/items', params, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['item']['name'].should == 'foo'
|
||||||
|
json['item']['id'].should_not be_nil
|
||||||
|
json['item'].should_not have_key('secret')
|
||||||
|
|
||||||
|
item = repo.reload.settings.items.first
|
||||||
|
item.id.should_not be_nil
|
||||||
|
item.name.should == 'foo'
|
||||||
|
item.secret.decrypt.should == 'TEH SECRET'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error message if item is invalid' do
|
||||||
|
params = { repository_id: repo.id }
|
||||||
|
response = post '/settings/items', params, headers
|
||||||
|
response.status.should == 422
|
||||||
|
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['message'].should == 'Validation failed'
|
||||||
|
json['errors'].should == [{
|
||||||
|
'field' => 'name',
|
||||||
|
'code' => 'missing_field'
|
||||||
|
}, {
|
||||||
|
'field' => 'secret',
|
||||||
|
'code' => 'missing_field'
|
||||||
|
}]
|
||||||
|
|
||||||
|
repo.reload.settings.items.length.should == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /items/:id' do
|
||||||
|
it 'should update an item' do
|
||||||
|
settings = repo.settings
|
||||||
|
item = settings.items.create(name: 'an item', secret: 'TEH SECRET')
|
||||||
|
settings.save
|
||||||
|
|
||||||
|
params = { repository_id: repo.id, item: { name: 'a new name', secret: 'a new secret' } }
|
||||||
|
response = patch '/settings/items/' + item.id, params, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['item']['name'].should == 'a new name'
|
||||||
|
json['item']['id'].should == item.id
|
||||||
|
json['item'].should_not have_key('secret')
|
||||||
|
|
||||||
|
updated_item = repo.reload.settings.items.find(item.id)
|
||||||
|
updated_item.id.should == item.id
|
||||||
|
updated_item.name.should == 'a new name'
|
||||||
|
updated_item.secret.decrypt.should == 'a new secret'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error message if item is invalid' do
|
||||||
|
settings = repo.settings
|
||||||
|
item = settings.items.create(name: 'an item', secret: 'TEH SECRET')
|
||||||
|
settings.save
|
||||||
|
|
||||||
|
params = { repository_id: repo.id, item: { name: '' } }
|
||||||
|
response = patch '/settings/items/' + item.id, params, headers
|
||||||
|
response.status.should == 422
|
||||||
|
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['message'].should == 'Validation failed'
|
||||||
|
json['errors'].should == [{
|
||||||
|
'field' => 'name',
|
||||||
|
'code' => 'missing_field'
|
||||||
|
}]
|
||||||
|
|
||||||
|
updated_item = repo.reload.settings.items.find(item.id)
|
||||||
|
updated_item.id.should == item.id
|
||||||
|
updated_item.name.should == 'an item'
|
||||||
|
updated_item.secret.decrypt.should == 'TEH SECRET'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /items/:id' do
|
||||||
|
it 'should delete an item' do
|
||||||
|
settings = repo.settings
|
||||||
|
item = settings.items.create(name: 'an item', secret: 'TEH SECRET')
|
||||||
|
settings.save
|
||||||
|
|
||||||
|
params = { repository_id: repo.id }
|
||||||
|
response = delete '/settings/items/' + item.id, params, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['item']['name'].should == 'an item'
|
||||||
|
json['item']['id'].should == item.id
|
||||||
|
json['item'].should_not have_key('secret')
|
||||||
|
|
||||||
|
repo.reload.settings.items.should have(0).items
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 if item can\'t be found' do
|
||||||
|
response = delete '/settings/items/123', { repository_id: repo.id }, headers
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['error'].should == "Could not find a requested setting"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,6 +26,11 @@ module TestHelpers
|
||||||
@custom_endpoints ||= []
|
@custom_endpoints ||= []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_settings_endpoint(name)
|
||||||
|
Travis::Api::App::SettingsEndpoint.subclass(name)
|
||||||
|
set_app Travis::Api::App.new
|
||||||
|
end
|
||||||
|
|
||||||
def add_endpoint(prefix, &block)
|
def add_endpoint(prefix, &block)
|
||||||
endpoint = Sinatra.new(Travis::Api::App::Endpoint, &block)
|
endpoint = Sinatra.new(Travis::Api::App::Endpoint, &block)
|
||||||
endpoint.set(prefix: prefix)
|
endpoint.set(prefix: prefix)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user