From 7baf61054ccb03d1b347fae93aa5b003288e642f Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Fri, 27 Jul 2012 15:55:57 +0200 Subject: [PATCH] rewrite all the things --- .travis.yml | 7 + Gemfile | 35 ++-- Gemfile.lock | 86 +++++--- Procfile | 1 + README.md | 60 ++++++ Rakefile | 13 ++ config.ru | 5 +- lib/travis/api/app.rb | 145 +++++--------- lib/travis/api/app/endpoint.rb | 16 ++ lib/travis/api/app/endpoint/artifacts.rb | 11 ++ lib/travis/api/app/endpoint/branches.rb | 11 ++ lib/travis/api/app/endpoint/builds.rb | 28 +++ lib/travis/api/app/endpoint/documentation.rb | 185 ++++++++++++++++++ lib/travis/api/app/endpoint/endpoints.rb | 68 +++++++ lib/travis/api/app/endpoint/home.rb | 15 ++ lib/travis/api/app/endpoint/hooks.rb | 14 ++ lib/travis/api/app/endpoint/jobs.rb | 14 ++ lib/travis/api/app/endpoint/profile.rb | 14 ++ lib/travis/api/app/endpoint/repositories.rb | 20 ++ lib/travis/api/app/endpoint/workers.rb | 11 ++ lib/travis/api/app/extensions.rb | 8 + .../api/app/extensions/smart_constants.rb | 28 +++ .../api/app/extensions/subclass_tracker.rb | 25 +++ lib/travis/api/app/helpers.rb | 8 + lib/travis/api/app/helpers/json_renderer.rb | 32 +++ lib/travis/api/app/middleware.rb | 7 + lib/travis/api/app/middleware/access_token.rb | 9 + lib/travis/api/{ => app/middleware}/cors.rb | 16 +- lib/travis/api/app/middleware/logging.rb | 15 ++ lib/travis/api/app/responder.rb | 27 +++ lib/travis/api/app/service.rb | 20 -- lib/travis/api/app/service/artifacts.rb | 14 -- lib/travis/api/app/service/builds.rb | 27 --- lib/travis/api/app/service/hooks.rb | 39 ---- lib/travis/api/app/service/jobs.rb | 23 --- lib/travis/api/app/service/profile.rb | 32 --- lib/travis/api/app/service/repos.rb | 27 --- lib/travis/api/app/service/workers.rb | 14 -- script/server | 8 + spec/app_spec.rb | 10 + spec/default_spec.rb | 15 ++ spec/endpoint/artifacts_spec.rb | 12 ++ spec/endpoint/branches_spec.rb | 5 + spec/endpoint/builds_spec.rb | 5 + spec/endpoint/documentation_spec.rb | 5 + spec/endpoint/endpoints_spec.rb | 5 + spec/endpoint/hooks_spec.rb | 5 + spec/endpoint/jobs_spec.rb | 5 + spec/endpoint/profile_spec.rb | 5 + spec/endpoint/repositories_spec.rb | 5 + spec/endpoint/workers_spec.rb | 5 + spec/endpoint_spec.rb | 18 ++ spec/extensions/smart_constants_spec.rb | 22 +++ spec/extensions/subclass_tracker_spec.rb | 31 +++ spec/helpers/json_renderer_spec.rb | 16 ++ spec/middleware/access_token_spec.rb | 16 ++ spec/middleware/cors_spec.rb | 50 +++++ spec/middleware/logging_spec.rb | 19 ++ spec/middleware_spec.rb | 12 ++ spec/spec_helper.rb | 17 ++ spec/support/factories.rb | 99 ++++++++++ travis-api.gemspec | 8 - 62 files changed, 1177 insertions(+), 351 deletions(-) create mode 100644 .travis.yml create mode 100644 Procfile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 lib/travis/api/app/endpoint.rb create mode 100644 lib/travis/api/app/endpoint/artifacts.rb create mode 100644 lib/travis/api/app/endpoint/branches.rb create mode 100644 lib/travis/api/app/endpoint/builds.rb create mode 100644 lib/travis/api/app/endpoint/documentation.rb create mode 100644 lib/travis/api/app/endpoint/endpoints.rb create mode 100644 lib/travis/api/app/endpoint/home.rb create mode 100644 lib/travis/api/app/endpoint/hooks.rb create mode 100644 lib/travis/api/app/endpoint/jobs.rb create mode 100644 lib/travis/api/app/endpoint/profile.rb create mode 100644 lib/travis/api/app/endpoint/repositories.rb create mode 100644 lib/travis/api/app/endpoint/workers.rb create mode 100644 lib/travis/api/app/extensions.rb create mode 100644 lib/travis/api/app/extensions/smart_constants.rb create mode 100644 lib/travis/api/app/extensions/subclass_tracker.rb create mode 100644 lib/travis/api/app/helpers.rb create mode 100644 lib/travis/api/app/helpers/json_renderer.rb create mode 100644 lib/travis/api/app/middleware.rb create mode 100644 lib/travis/api/app/middleware/access_token.rb rename lib/travis/api/{ => app/middleware}/cors.rb (59%) create mode 100644 lib/travis/api/app/middleware/logging.rb create mode 100644 lib/travis/api/app/responder.rb delete mode 100644 lib/travis/api/app/service.rb delete mode 100644 lib/travis/api/app/service/artifacts.rb delete mode 100644 lib/travis/api/app/service/builds.rb delete mode 100644 lib/travis/api/app/service/hooks.rb delete mode 100644 lib/travis/api/app/service/jobs.rb delete mode 100644 lib/travis/api/app/service/profile.rb delete mode 100644 lib/travis/api/app/service/repos.rb delete mode 100644 lib/travis/api/app/service/workers.rb create mode 100755 script/server create mode 100644 spec/app_spec.rb create mode 100644 spec/default_spec.rb create mode 100644 spec/endpoint/artifacts_spec.rb create mode 100644 spec/endpoint/branches_spec.rb create mode 100644 spec/endpoint/builds_spec.rb create mode 100644 spec/endpoint/documentation_spec.rb create mode 100644 spec/endpoint/endpoints_spec.rb create mode 100644 spec/endpoint/hooks_spec.rb create mode 100644 spec/endpoint/jobs_spec.rb create mode 100644 spec/endpoint/profile_spec.rb create mode 100644 spec/endpoint/repositories_spec.rb create mode 100644 spec/endpoint/workers_spec.rb create mode 100644 spec/endpoint_spec.rb create mode 100644 spec/extensions/smart_constants_spec.rb create mode 100644 spec/extensions/subclass_tracker_spec.rb create mode 100644 spec/helpers/json_renderer_spec.rb create mode 100644 spec/middleware/access_token_spec.rb create mode 100644 spec/middleware/cors_spec.rb create mode 100644 spec/middleware/logging_spec.rb create mode 100644 spec/middleware_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/factories.rb delete mode 100644 travis-api.gemspec diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..eee8c5e1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: ruby +rvm: + - 1.9.3 +before_script: + - 'RAILS_ENV=test rake db:create db:schema:load --trace' +notifications: + irc: "irc.freenode.org#travis" \ No newline at end of file diff --git a/Gemfile b/Gemfile index 1e107130..a49f6b13 100644 --- a/Gemfile +++ b/Gemfile @@ -1,23 +1,34 @@ source :rubygems - ruby '1.9.3' rescue nil -gem 'travis-core', github: 'travis-ci/travis-core' gem 'travis-support', github: 'travis-ci/travis-support' +gem 'travis-core', github: 'travis-ci/travis-core' +gem 'hubble', github: 'roidrage/hubble' +gem 'backports', '~> 2.5' +gem 'pg', '~> 0.13.2' +gem 'newrelic_rpm', '~> 3.3.0' +gem 'thin', '~> 1.4' gem 'sinatra' gem 'sinatra-contrib' -# gem 'sinatra-cross_origin', github: 'britg/sinatra-cross_origin' -gem 'rack-contrib', github: 'rack/rack-contrib', require: 'rack/contrib' +gem 'redcarpet' -gem 'rake', '~> 0.9.2.2' -gem 'versionist', '~> 0.2.0' +group :production do + gem 'rack-ssl' +end -# db -gem 'pg', '~> 0.13.2' -gem 'newrelic_rpm', '~> 3.3.0' -gem 'hubble', git: 'git://github.com/roidrage/hubble' +group :test do + gem 'rspec', '~> 2.11' + gem 'factory_girl', '~> 2.4.0' +end -# heroku -gem 'unicorn', '~> 4.1.1' +group :development do + gem 'yard-sinatra', github: 'rkh/yard-sinatra' + gem 'foreman' + gem 'rerun' +end +group :development, :test do + gem 'rake', '~> 0.9.2' + gem 'micro_migrations', git: 'git://gist.github.com/2087829.git' +end diff --git a/Gemfile.lock b/Gemfile.lock index 1be78011..21b178b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,18 @@ GIT - remote: git://github.com/rack/rack-contrib.git - revision: b7e7c38fd02c3b5da91aa57af78b3f571c6ebcd0 + remote: git://gist.github.com/2087829.git + revision: c766c06b0bdbda3bd96c3f4e376249cafcbbfaaa specs: - rack-contrib (1.1.0) - rack (>= 0.9.1) + micro_migrations (0.0.1) GIT - remote: git://github.com/roidrage/hubble + remote: git://github.com/rkh/yard-sinatra.git + revision: 3b1064eef407d2d288a5b96d258178a1e67b3b80 + specs: + yard-sinatra (1.0.0) + yard (~> 0.7) + +GIT + remote: git://github.com/roidrage/hubble.git revision: 5220415d5542a2868d54f7be9f35fc1d66126b8e specs: hubble (0.1.2) @@ -63,25 +69,29 @@ GEM activesupport (= 3.2.6) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.6) - activemodel (= 3.2.6) - activesupport (= 3.2.6) activesupport (3.2.6) i18n (~> 0.6) multi_json (~> 1.0) - addressable (2.2.8) + addressable (2.3.1) arel (3.0.2) atomic (1.0.1) avl_tree (1.1.3) - backports (2.6.1) + backports (2.6.2) builder (3.0.0) + daemons (1.1.8) data_migrations (0.0.1) activerecord rake + diff-lcs (1.1.3) erubis (2.7.0) eventmachine (0.12.10) + factory_girl (2.4.2) + activesupport faraday (0.8.1) multipart-post (~> 1.1) + ffi (1.1.0) + foreman (0.53.0) + thor (>= 0.13.6) gh (0.7.3) addressable backports (~> 2.3) @@ -95,7 +105,10 @@ GEM i18n (0.6.0) journey (1.0.4) json (1.6.7) - kgio (2.7.4) + listen (0.4.7) + rb-fchange (~> 0.0.5) + rb-fsevent (~> 0.9.1) + rb-inotify (~> 0.8.8) mail (2.4.4) i18n (>= 0.4.0) mime-types (~> 1.16) @@ -131,14 +144,6 @@ GEM rack rack-test (0.6.1) rack (>= 1.0) - rails (3.2.6) - actionmailer (= 3.2.6) - actionpack (= 3.2.6) - activerecord (= 3.2.6) - activeresource (= 3.2.6) - activesupport (= 3.2.6) - bundler (~> 1.0) - railties (= 3.2.6) railties (3.2.6) actionpack (= 3.2.6) activesupport (= 3.2.6) @@ -146,12 +151,27 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.10.0) rake (0.9.2.2) + rb-fchange (0.0.5) + ffi + rb-fsevent (0.9.1) + rb-inotify (0.8.8) + ffi (>= 0.5.0) rdoc (3.12) json (~> 1.4) + redcarpet (2.1.1) redis (3.0.1) + rerun (0.7.1) + listen rollout (1.1.0) + rspec (2.11.0) + rspec-core (~> 2.11.0) + rspec-expectations (~> 2.11.0) + rspec-mocks (~> 2.11.0) + rspec-core (2.11.1) + rspec-expectations (2.11.2) + diff-lcs (~> 1.1.3) + rspec-mocks (2.11.1) signature (0.1.3) simple_states (0.1.1) activesupport @@ -171,33 +191,37 @@ GEM hike (~> 1.2) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + thin (1.4.1) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) thor (0.14.6) tilt (1.3.3) treetop (1.4.10) polyglot polyglot (>= 0.3.1) tzinfo (0.3.33) - unicorn (4.1.1) - kgio (~> 2.4) - rack - raindrops (~> 0.6) - versionist (0.2.3) - rails (~> 3.0) - yard (~> 0.7) yard (0.8.2.1) PLATFORMS ruby DEPENDENCIES + backports (~> 2.5) + factory_girl (~> 2.4.0) + foreman hubble! + micro_migrations! newrelic_rpm (~> 3.3.0) pg (~> 0.13.2) - rack-contrib! - rake (~> 0.9.2.2) + rack-ssl + rake (~> 0.9.2) + redcarpet + rerun + rspec (~> 2.11) sinatra sinatra-contrib + thin (~> 1.4) travis-core! travis-support! - unicorn (~> 4.1.1) - versionist (~> 0.2.0) + yard-sinatra! diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..3f760dc7 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: bundle exec ./script/server diff --git a/README.md b/README.md new file mode 100644 index 00000000..c4abc61c --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# The public Travis API + +This is the app (eventually) running on https://api.travis-ci.org/ + +## Installation + +Setup: + + $ bundle install + +Run tests: + + $ RAILS_ENV=test rake db:create db:schema:load + $ rake spec + +Run the server: + + $ rake db:create db:schema:load + $ foreman start + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +### API documentation + +We use source code comments to add documentation. If the server is running, you +can browse an HTML documenation at [`/docs`](http://localhost:5000/docs). + +### Project architecture + + lib + `-- travis + `-- api + `-- app + |-- endpoint # API endpoints + |-- extensions # Sinatra extensions + |-- helpers # Sinatra helpers + `-- middleware # Rack middleware + +Classes inheriting from `Endpoint` or `Middleware`, they will automatically be +set up properly. + +Each endpoint class gets mapped to a prefix, which defaults to the snake-case +class name (i.e. `Travis::Api::App::Profile` will map to `/profile`). +It can be overridden by setting `:prefix`: + +``` ruby +require 'travis/api/app' + +class Travis::Api::App + class MyRouts < Endpoint + set :prefix, '/awesome' + end +end +``` \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..60a72d37 --- /dev/null +++ b/Rakefile @@ -0,0 +1,13 @@ +require 'bundler/setup' +ENV['SCHEMA'] = "#{Gem.loaded_specs['travis-core'].full_gem_path}/db/schema.rb" + +require 'micro_migrations' +require 'travis' + +begin + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new + task default: :spec +rescue LoadError + warn "could not load rspec" +end diff --git a/config.ru b/config.ru index 9d32d914..a861a79e 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,2 @@ -$:.unshift 'lib' - require 'travis/api/app' - -run Travis::Api::App +run Travis::Api::App.new diff --git a/lib/travis/api/app.rb b/lib/travis/api/app.rb index ff8095ab..65b7e3e0 100644 --- a/lib/travis/api/app.rb +++ b/lib/travis/api/app.rb @@ -1,105 +1,64 @@ -require 'sinatra' -require 'sinatra/reloader' -require 'travis/api/cors' -require 'json' +# Make sure we set that before everything +ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] || ENV['ENV'] +ENV['RAILS_ENV'] = ENV['RACK_ENV'] + require 'travis' +require 'backports' +require 'rack' +require 'rack/protection' +require 'active_record' -Travis::Database.connect +# Rack class implementing the HTTP API. +# Instances respond to #call. +# +# run Travis::Api::App.new +# +# Requires TLS in production. +class Travis::Api::App + autoload :Responder, 'travis/api/app/responder' + autoload :Endpoint, 'travis/api/app/endpoint' + autoload :Extensions, 'travis/api/app/extensions' + autoload :Helpers, 'travis/api/app/helpers' + autoload :Middleware, 'travis/api/app/middleware' -module Travis - module Api - class App < Sinatra::Application - autoload :Service, 'travis/api/app/service' - disable :protection + Rack.autoload :SSL, 'rack/ssl' - use ActiveRecord::ConnectionAdapters::ConnectionManagement - use Travis::API::CORS + # Used to track if setup already ran. + def self.setup? + @setup ||= false + end - error ActiveRecord::RecordNotFound do - not_found - end + # Loads all endpoints and middleware and hooks them up properly. + # Calls #setup on any middleware and endpoint. + # + # This method is not threadsafe, but called when loading + # the environment, so no biggy. + def self.setup(options = {}) + return if setup? + Travis::Database.connect - configure :development do - register Sinatra::Reloader - set :show_exceptions, :after_handler - end + Responder.set(options) if options + Backports.require_relative_dir 'app/middleware' + Backports.require_relative_dir 'app/endpoint' + Responder.subclasses.each(&:setup) - before do - content_type :json - end + @setup = true + end - get '/repositories' do - respond_with Service::Repos.new(params).collection - end + attr_accessor :app - get '/repositories/:id' do - respond_with Service::Repos.new(params).item - # raise if not params[:format] == 'png' - end - - get '/builds' do - respond_with Service::Builds.new(params).collection - end - - get '/builds/:id' do - respond_with Service::Builds.new(params).item - end - - get '/branches' do - # respond_with Service::Repos.new(params).item, :type => :branches - { branches: [] }.to_json - end - - get '/jobs' do - respond_with Service::Jobs.new(params).collection, :type => 'jobs' - end - - get '/jobs/:id' do - respond_with Service::Jobs.new(params).item, :type => 'job' - end - - get '/artifacts/:id' do - respond_with Service::Artifacts.new(params).item - end - - get '/workers' do - respond_with Service::Workers.new(params).collection - end - - get '/hooks' do - authenticate_user! - respond_with Service::Hooks.new(user, params).item - # rescue_from ActiveRecord::RecordInvalid, :with => Proc.new { head :not_acceptable } - end - - put '/hooks/:id' do - authenticate_user! - respond_with Service::Hooks.new(user, params).update - end - - get '/profile' do - authenticate_user! - respond_with Service::Profile.new(user).update - end - - post '/profile/sync' do - authenticate_user! - respond_with Service::Profile.new(user).sync - end - - private - - def authenticate_user! - @user = User.find_by_login('svenfuchs') - end - - def respond_with(resource, options = {}) - Travis::Api.data(resource, { :params => params, :version => version }.merge(options)).to_json - end - - def version - 'v2' - end + def initialize + Travis::Api::App.setup + @app = Rack::Builder.app do + use Rack::Protection::PathTraversal + use Rack::SSL if Endpoint.production? + Middleware.subclasses.each { |m| use(m) } + Endpoint.subclasses.each { |e| map(e.prefix) { run(e) } } end end + + # Rack protocol + def call(env) + app.call(env) + end end diff --git a/lib/travis/api/app/endpoint.rb b/lib/travis/api/app/endpoint.rb new file mode 100644 index 00000000..7ab1f090 --- /dev/null +++ b/lib/travis/api/app/endpoint.rb @@ -0,0 +1,16 @@ +require 'travis/api/app' + +class Travis::Api::App + # Superclass for HTTP endpoints. Takes care of prefixing. + class Endpoint < Responder + set(:prefix) { "/" << name[/[^:]+$/].underscore } + before { content_type :json } + + error(ActiveRecord::RecordNotFound, Sinatra::NotFound) { not_found } + not_found { content_type =~ /json/ ? { 'file' => 'not found' } : 'file not found' } + + # TODO: Dummy method. + def self.scope(name) + end + end +end diff --git a/lib/travis/api/app/endpoint/artifacts.rb b/lib/travis/api/app/endpoint/artifacts.rb new file mode 100644 index 00000000..f091881b --- /dev/null +++ b/lib/travis/api/app/endpoint/artifacts.rb @@ -0,0 +1,11 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Artifacts < Endpoint + # TODO: Add documentation. + get('/:id') { |id| Artifact.find(id) } + end + end +end diff --git a/lib/travis/api/app/endpoint/branches.rb b/lib/travis/api/app/endpoint/branches.rb new file mode 100644 index 00000000..33066f3e --- /dev/null +++ b/lib/travis/api/app/endpoint/branches.rb @@ -0,0 +1,11 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Branches < Endpoint + # TODO: Add better implementation and documentation. + get('/') {{ branches: [] }} + end + end +end diff --git a/lib/travis/api/app/endpoint/builds.rb b/lib/travis/api/app/endpoint/builds.rb new file mode 100644 index 00000000..34780554 --- /dev/null +++ b/lib/travis/api/app/endpoint/builds.rb @@ -0,0 +1,28 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Builds < Endpoint + # TODO: Add documentation. + get '/' do + scope = repository.builds.by_event_type(params[:event_type] || 'push') + scope = params[:after] ? scope.older_than(params[:after]) : scope.recent + scope + end + + # TODO: Add documentation. + get '/:id' do + one = params[:repository_id] ? repository.builds : Build + one.includes(:commit, :matrix => [:commit, :log]).find(params[:id]) + end + + private + + def repository + pass if params.empty? + Repository.find_by(params) || not_found + end + end + end +end diff --git a/lib/travis/api/app/endpoint/documentation.rb b/lib/travis/api/app/endpoint/documentation.rb new file mode 100644 index 00000000..d79cbf5a --- /dev/null +++ b/lib/travis/api/app/endpoint/documentation.rb @@ -0,0 +1,185 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # Generated API documentation. + class Documentation < Endpoint + set prefix: '/docs' + enable :inline_templates + + # HTML view for [/endpoints](#/endpoints/). + get '/' do + content_type :html + endpoints = Endpoints.endpoints + erb :index, {}, :endpoints => endpoints.keys.sort.map { |k| endpoints[k] } + end + + helpers do + def icon_for(verb) + # GET, POST, PATCH, PUT, DELETE" + case verb + when 'GET' then 'file' + when 'POST' then 'edit' + when 'PATCH' then 'wrench' + when 'PUT' then 'share' + when 'DELETE' then 'trash' + else 'question-sign' + end + end + + def slug_for(route) + return route['uri'] if route['verb'] == 'GET' + route['verb'] + " " + route['uri'] + end + + def docs_for(entry) + markdown(entry['doc']). + gsub('/, ''). + gsub(/TODO:?/, 'TODO') + end + end + end + end +end + +__END__ + +@@ index + + + + + + Travis API documentation + + + + + + + + + + + + + + +
+
+
+

The Travis API

+

All the routes, just waiting for you to build something awesome.

+
+
+ +
+ + + +
+ + <% endpoints.each do |endpoint| %> +
+ + <%= docs_for endpoint %> + <% endpoint['routes'].each do |route| %> +
+

<%= route['verb'] %> <%= route['uri'] %>

+

+

Required autorization scope: <%= route['scope'] %>
+

+ <%= docs_for route %> +
+ <% end %> +
+ <% end %> + +
+
+
+ + diff --git a/lib/travis/api/app/endpoint/endpoints.rb b/lib/travis/api/app/endpoint/endpoints.rb new file mode 100644 index 00000000..8f395588 --- /dev/null +++ b/lib/travis/api/app/endpoint/endpoints.rb @@ -0,0 +1,68 @@ +require 'travis/api/app' +require 'yard/sinatra' + +class Travis::Api::App + class Endpoint + # Documents all available API endpoints for the currently deployed version. + # Text is actually parsed from the source code upon server start. + class Endpoints < Endpoint + set :endpoints, {} + + set :setup do + endpoint_files = Dir.glob(File.expand_path("../*.rb", __FILE__)) + YARD::Registry.load(endpoint_files, true) + + YARD::Sinatra.routes.each do |route| + namespace = route.namespace + controller = namespace.to_s.constantize + route_info = { + 'uri' => (controller.prefix + route.http_path[1..-2]).gsub('//', '/'), + 'verb' => route.http_verb, + 'doc' => route.docstring, + 'scope' => /scope\W+(\w+)/.match(route.source).try(:[], 1) || 'public' + } + endpoint = endpoints[controller.prefix] ||= { + 'name' => namespace.name, + 'doc' => namespace.docstring, + 'prefix' => controller.prefix, + 'routes' => [] + } + endpoint['routes'] << route_info + end + + set :json, endpoints.keys.sort.map { |k| endpoints[k] }.to_json + endpoints.each_value { |r| r[:json] = r.to_json if r.respond_to? :to_hash } + end + + # Lists all available API endpoints by URI prefix. + # + # Values in the resulting array correspond to return values of + # [`/endpoints/:prefix`](#/endpoints/:prefix). + get '/' do + settings.json + end + + # Infos about a specific controller. + # + # Example response: + # + # { + # name: "Endpoints", + # doc: "Documents all available API endpoints...", + # prefix: "/endpoints", + # routes: [ + # { + # uri: "/endpoints/:prefix", + # verb: "GET", + # doc: "Infos about...", + # scope: "public" + # } + # ] + # } + get '/:prefix' do |prefix| + pass unless endpoint = settings.endpoints["/#{prefix}"] + endpoint[:json] + end + end + end +end diff --git a/lib/travis/api/app/endpoint/home.rb b/lib/travis/api/app/endpoint/home.rb new file mode 100644 index 00000000..fab79ec6 --- /dev/null +++ b/lib/travis/api/app/endpoint/home.rb @@ -0,0 +1,15 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + class Home < Endpoint + set(:prefix, '/') + + # Landing point. Redirects web browsers to [API documentation](#/docs/). + get '/' do + redirect to('/docs/') if request.preferred_type('application/json', 'text/html') == 'text/html' + { 'hello' => 'world' } + end + end + end +end diff --git a/lib/travis/api/app/endpoint/hooks.rb b/lib/travis/api/app/endpoint/hooks.rb new file mode 100644 index 00000000..b8dfbc50 --- /dev/null +++ b/lib/travis/api/app/endpoint/hooks.rb @@ -0,0 +1,14 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Hooks < Endpoint + # TODO: Add implementation and documentation. + get('/', scope: :private) { raise NotImplementedError } + + # TODO: Add implementation and documentation. + put('/:id', scope: :admin) { raise NotImplementedError } + end + end +end diff --git a/lib/travis/api/app/endpoint/jobs.rb b/lib/travis/api/app/endpoint/jobs.rb new file mode 100644 index 00000000..ea99a530 --- /dev/null +++ b/lib/travis/api/app/endpoint/jobs.rb @@ -0,0 +1,14 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Jobs < Endpoint + # TODO: Add implementation and documentation. + get('/') { raise NotImplementedError } + + # TODO: Add implementation and documentation. + get('/:id') { raise NotImplementedError } + end + end +end diff --git a/lib/travis/api/app/endpoint/profile.rb b/lib/travis/api/app/endpoint/profile.rb new file mode 100644 index 00000000..31c41502 --- /dev/null +++ b/lib/travis/api/app/endpoint/profile.rb @@ -0,0 +1,14 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Profile < Endpoint + # TODO: Add implementation and documentation. + get('/', scope: :private) { raise NotImplementedError } + + # TODO: Add implementation and documentation. + post('/sync', scope: :private) { raise NotImplementedError } + end + end +end diff --git a/lib/travis/api/app/endpoint/repositories.rb b/lib/travis/api/app/endpoint/repositories.rb new file mode 100644 index 00000000..52329b15 --- /dev/null +++ b/lib/travis/api/app/endpoint/repositories.rb @@ -0,0 +1,20 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Repositories < Endpoint + # TODO: Add documentation. + get '/' do + scope = Repository.timeline.recent + scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name] + scope = scope.by_slug(params[:slug]) if params[:slug] + scope = scope.search(params[:search]) if params[:search].present? + scope + end + + # TODO: Add documentation. + get('/:id') { Repository.find_by(params) } + end + end +end diff --git a/lib/travis/api/app/endpoint/workers.rb b/lib/travis/api/app/endpoint/workers.rb new file mode 100644 index 00000000..4c5c25f3 --- /dev/null +++ b/lib/travis/api/app/endpoint/workers.rb @@ -0,0 +1,11 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # TODO: Add documentation. + class Workers < Endpoint + # TODO: Add implementation and documentation. + get('/') { Worker.order(:host, :name) } + end + end +end diff --git a/lib/travis/api/app/extensions.rb b/lib/travis/api/app/extensions.rb new file mode 100644 index 00000000..dd189114 --- /dev/null +++ b/lib/travis/api/app/extensions.rb @@ -0,0 +1,8 @@ +require 'travis/api/app' + +class Travis::Api::App + # Namespace for Sinatra extensions. + module Extensions + Backports.require_relative_dir 'extensions' + end +end diff --git a/lib/travis/api/app/extensions/smart_constants.rb b/lib/travis/api/app/extensions/smart_constants.rb new file mode 100644 index 00000000..e4991d2d --- /dev/null +++ b/lib/travis/api/app/extensions/smart_constants.rb @@ -0,0 +1,28 @@ +require 'travis/api/app' + +class Travis::Api::App + module Extensions + # Allows writing + # + # helpers :some_helper + # + # Instead of + # + # helpers Travis::Api::App::Helpers::SomeHelper + module SmartConstants + def helpers(*list, &block) + super(*resolve_constants(list, Helpers), &block) + end + + def register(*list, &block) + super(*resolve_constants(list, Extensions), &block) + end + + private + + def resolve_constants(list, namespace) + list.map { |e| Symbol === e ? namespace.const_get(e.to_s.camelize) : e } + end + end + end +end diff --git a/lib/travis/api/app/extensions/subclass_tracker.rb b/lib/travis/api/app/extensions/subclass_tracker.rb new file mode 100644 index 00000000..d3e046d6 --- /dev/null +++ b/lib/travis/api/app/extensions/subclass_tracker.rb @@ -0,0 +1,25 @@ +require 'travis/api/app' + +class Travis::Api::App + module Extensions + # Keeps track of subclasses. Used for endpoint and middleware detection. + # This will prevent garbage collection of subclasses. + module SubclassTracker + def direct_subclasses + @direct_subclasses ||= [] + end + + # List of "leaf" subclasses (ie subclasses without subclasses). + def subclasses + return [self] if direct_subclasses.empty? + direct_subclasses.map(&:subclasses).flatten.uniq + end + + def inherited(subclass) + super + subclass.set app_file: caller_files.first + direct_subclasses << subclass + end + end + end +end diff --git a/lib/travis/api/app/helpers.rb b/lib/travis/api/app/helpers.rb new file mode 100644 index 00000000..a009b025 --- /dev/null +++ b/lib/travis/api/app/helpers.rb @@ -0,0 +1,8 @@ +require 'travis/api/app' + +class Travis::Api::App + # Namespace for helpers. + module Helpers + Backports.require_relative_dir 'helpers' + end +end diff --git a/lib/travis/api/app/helpers/json_renderer.rb b/lib/travis/api/app/helpers/json_renderer.rb new file mode 100644 index 00000000..835253b0 --- /dev/null +++ b/lib/travis/api/app/helpers/json_renderer.rb @@ -0,0 +1,32 @@ +require 'travis/api/app' + +class Travis::Api::App + module Helpers + # Allows routes to return either hashes or anything Travis::API.data can + # convert (in addition to the return values supported by Sinatra, of + # course). These values will be encoded in JSON. + module JsonRenderer + def respond_with(resource, options = {}) + halt render_json(resource, options) + end + + def body(value = nil, &block) + value = render_json(value) if value + super(value, &block) + end + + private + + def render_json(resource, options = {}) + options[:version] ||= 'v2' # TODO: Content negotiation + options[:params] ||= params + + builder = Travis::Api.builder(resource, options) + resource = builder.new(resource, options[:params]).data.to_json if builder + resource = resource.to_json if resource.is_a? Hash + + resource + end + end + end +end diff --git a/lib/travis/api/app/middleware.rb b/lib/travis/api/app/middleware.rb new file mode 100644 index 00000000..a70f473b --- /dev/null +++ b/lib/travis/api/app/middleware.rb @@ -0,0 +1,7 @@ +require 'travis/api/app' + +class Travis::Api::App + # Superclass for all middleware. + class Middleware < Responder + end +end diff --git a/lib/travis/api/app/middleware/access_token.rb b/lib/travis/api/app/middleware/access_token.rb new file mode 100644 index 00000000..30db73bd --- /dev/null +++ b/lib/travis/api/app/middleware/access_token.rb @@ -0,0 +1,9 @@ +require 'travis/api/app' + +class Travis::Api::App + class Middleware + # Checks access tokens and sets appropriate scopes. + class AccessToken < Middleware + end + end +end diff --git a/lib/travis/api/cors.rb b/lib/travis/api/app/middleware/cors.rb similarity index 59% rename from lib/travis/api/cors.rb rename to lib/travis/api/app/middleware/cors.rb index f27f7e8a..eea34fda 100644 --- a/lib/travis/api/cors.rb +++ b/lib/travis/api/app/middleware/cors.rb @@ -1,10 +1,12 @@ -require 'sinatra/base' - -module Travis - module API - class CORS < Sinatra::Base - disable :protection +require 'travis/api/app' +class Travis::Api::App + class Middleware + # Implements Cross-Origin Resource Sharing. Supported by all major browsers. + # See http://www.w3.org/TR/cors/ + # + # TODO: Be smarter about origin. + class Cors < Middleware before do headers['Access-Control-Allow-Origin'] = "*" headers['Access-Control-Allow-Credentials'] = "true" @@ -13,7 +15,7 @@ module Travis options // do headers['Access-Control-Allow-Methods'] = "GET, POST, PATCH, PUT, DELETE" - headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization" + headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization, Accept" end end end diff --git a/lib/travis/api/app/middleware/logging.rb b/lib/travis/api/app/middleware/logging.rb new file mode 100644 index 00000000..94b099c4 --- /dev/null +++ b/lib/travis/api/app/middleware/logging.rb @@ -0,0 +1,15 @@ +require 'travis/api/app' + +class Travis::Api::App + class Middleware + # Makes sure we use Travis.logger everywhere. + class Logging < Middleware + set(:setup) { ActiveRecord::Base.logger = Travis.logger } + + before do + env['rack.logger'] = Travis.logger + env['rack.errors'] = Travis.logger.instance_variable_get(:@logdev).dev rescue nil + end + end + end +end diff --git a/lib/travis/api/app/responder.rb b/lib/travis/api/app/responder.rb new file mode 100644 index 00000000..d119e8f0 --- /dev/null +++ b/lib/travis/api/app/responder.rb @@ -0,0 +1,27 @@ +require 'travis/api/app' +require 'sinatra/base' + +class Travis::Api::App + # Superclass for any endpoint and middleware. + # Pulls in relevant helpers and extensions. + class Responder < Sinatra::Base + register Extensions::SmartConstants + + configure do + # We pull in certain protection middleware in App. + # Being token based makes us invulnerable to common + # CSRF attack. + # + # Logging is set up by custom middleware + disable :protection, :logging, :setup + register :subclass_tracker + helpers :json_renderer + end + + configure :development do + # We want error pages in development, but only + # when we don't have an error handler specified + set :show_exceptions, :after_handler + end + end +end diff --git a/lib/travis/api/app/service.rb b/lib/travis/api/app/service.rb deleted file mode 100644 index 97479805..00000000 --- a/lib/travis/api/app/service.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Travis - module Api - class App - class Service - autoload :Artifacts, 'travis/api/app/service/artifacts' - autoload :Builds, 'travis/api/app/service/builds' - autoload :Hooks, 'travis/api/app/service/hooks' - autoload :Jobs, 'travis/api/app/service/jobs' - autoload :Repos, 'travis/api/app/service/repos' - autoload :Workers, 'travis/api/app/service/workers' - - attr_reader :params - - def initialize(params) - @params = params - end - end - end - end -end diff --git a/lib/travis/api/app/service/artifacts.rb b/lib/travis/api/app/service/artifacts.rb deleted file mode 100644 index 5435659a..00000000 --- a/lib/travis/api/app/service/artifacts.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Travis - module Api - class App - class Service - class Artifacts < Service - def item - Artifact.find(params[:id]) - end - end - end - end - end -end - diff --git a/lib/travis/api/app/service/builds.rb b/lib/travis/api/app/service/builds.rb deleted file mode 100644 index ca138331..00000000 --- a/lib/travis/api/app/service/builds.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Travis - module Api - class App - class Service - class Builds < Service - def collection - scope = repository.builds.by_event_type(params[:event_type] || 'push') - scope = params[:after] ? scope.older_than(params[:after]) : scope.recent - scope - end - - def item - one = params[:repository_id] ? repository.builds : Build - one.includes(:commit, :matrix => [:commit, :log]).find(params[:id]) - end - - private - - def repository - Repository.find_by(params) || not_found # TODO needs to return nil if params are empty - end - end - end - end - end -end - diff --git a/lib/travis/api/app/service/hooks.rb b/lib/travis/api/app/service/hooks.rb deleted file mode 100644 index 446888b8..00000000 --- a/lib/travis/api/app/service/hooks.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Travis - module Api - class App - class Service - class Hooks < Service - attr_reader :user - - def initialize(user, params) - super(params) - @user = user - end - - def collection - user.service_hooks - end - - def update - hook.set(payload[:active], user) - hook - end - - private - - def hook - repository.service_hook - end - - def repository - Repository.find_or_create_by_owner_name_and_name(params[:owner_name], params[:name]) - end - - def payload - params[:service_hook] || {} - end - end - end - end - end -end diff --git a/lib/travis/api/app/service/jobs.rb b/lib/travis/api/app/service/jobs.rb deleted file mode 100644 index 9c0b360b..00000000 --- a/lib/travis/api/app/service/jobs.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Travis - module Api - class App - class Service - class Jobs < Service - def collection - if params[:ids] - Job::Test.where(:id => params[:ids]).includes(:commit, :log) - else - jobs = Job::Test.queued.includes(:commit, :log) - jobs = jobs.where(:queue => params[:queue]) if params[:queue] - jobs - end - end - - def item - Job::Test.find(params[:id]) - end - end - end - end - end -end diff --git a/lib/travis/api/app/service/profile.rb b/lib/travis/api/app/service/profile.rb deleted file mode 100644 index 1b6c9e85..00000000 --- a/lib/travis/api/app/service/profile.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Travis - module Api - class App - class Service - class Profile < Service - attr_reader :user - - def initialize(user) - @user = user - end - - def item - user - end - - def sync - unless user.is_syncing? - publisher.publish({ user_id: user.id }, type: 'sync') - user.update_attribute(:is_syncing, true) - end - end - - private - - def publisher - Travis::Amqp::Publisher.new('sync.user') - end - end - end - end - end -end diff --git a/lib/travis/api/app/service/repos.rb b/lib/travis/api/app/service/repos.rb deleted file mode 100644 index 3ce66f24..00000000 --- a/lib/travis/api/app/service/repos.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Travis - module Api - class App - class Service - class Repos - attr_reader :params - - def initialize(params) - @params = params - end - - def collection - scope = Repository.timeline.recent - scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name] - scope = scope.by_slug(params[:slug]) if params[:slug] - scope = scope.search(params[:search]) if params[:search].present? - scope - end - - def item - Repository.find_by(params) - end - end - end - end - end -end diff --git a/lib/travis/api/app/service/workers.rb b/lib/travis/api/app/service/workers.rb deleted file mode 100644 index ba23c943..00000000 --- a/lib/travis/api/app/service/workers.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Travis - module Api - class App - class Service - class Workers < Service - def collection - Worker.order(:host, :name) - end - end - end - end - end -end - diff --git a/script/server b/script/server new file mode 100755 index 00000000..ef6391d4 --- /dev/null +++ b/script/server @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")/.." +[ $PORT ] || PORT=5000 +[ $RACK_ENV ] || RACK_ENV=development + +cmd="ruby -I lib -S bundle exec ruby -I lib -S thin start -p $PORT -e $RACK_ENV --threaded" +[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -a 127.0.0.1" +exec $cmd diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 00000000..1c3394c2 --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe Travis::Api::App do + describe :setup? do + it 'indicates if #setup has been called' do + Travis::Api::App.setup + Travis::Api::App.should be_setup + end + end +end diff --git a/spec/default_spec.rb b/spec/default_spec.rb new file mode 100644 index 00000000..98e0df3b --- /dev/null +++ b/spec/default_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Home do + describe 'GET /' do + it 'replies with a json response by default' do + get('/')["Content-Type"].should include("json") + end + + it 'redirects HTML requests to /docs' do + get '/', {}, 'HTTP_ACCEPT' => 'text/html' + status.should == 302 + headers['Location'].should end_with('/docs/') + end + end +end diff --git a/spec/endpoint/artifacts_spec.rb b/spec/endpoint/artifacts_spec.rb new file mode 100644 index 00000000..9f251192 --- /dev/null +++ b/spec/endpoint/artifacts_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Artifacts do + let(:artifact) { Factory(:log) } + let(:id) { artifact.id } + + describe 'GET /artifacts/:id' do + it 'loads the artifact' do + get("/artifacts/#{id}").should be_ok + end + end +end diff --git a/spec/endpoint/branches_spec.rb b/spec/endpoint/branches_spec.rb new file mode 100644 index 00000000..5e974a0a --- /dev/null +++ b/spec/endpoint/branches_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Branches do + it 'has to be implemented' +end diff --git a/spec/endpoint/builds_spec.rb b/spec/endpoint/builds_spec.rb new file mode 100644 index 00000000..51e06eb7 --- /dev/null +++ b/spec/endpoint/builds_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Builds do + it 'has to be tested' +end diff --git a/spec/endpoint/documentation_spec.rb b/spec/endpoint/documentation_spec.rb new file mode 100644 index 00000000..6f7fe01c --- /dev/null +++ b/spec/endpoint/documentation_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Documentation do + it 'has to be tested' +end diff --git a/spec/endpoint/endpoints_spec.rb b/spec/endpoint/endpoints_spec.rb new file mode 100644 index 00000000..6676ef3b --- /dev/null +++ b/spec/endpoint/endpoints_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Endpoints do + it 'has to be tested' +end diff --git a/spec/endpoint/hooks_spec.rb b/spec/endpoint/hooks_spec.rb new file mode 100644 index 00000000..e9c9af1e --- /dev/null +++ b/spec/endpoint/hooks_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Hooks do + it 'has to be tested' +end diff --git a/spec/endpoint/jobs_spec.rb b/spec/endpoint/jobs_spec.rb new file mode 100644 index 00000000..763f956a --- /dev/null +++ b/spec/endpoint/jobs_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Jobs do + it 'has to be tested' +end diff --git a/spec/endpoint/profile_spec.rb b/spec/endpoint/profile_spec.rb new file mode 100644 index 00000000..63127901 --- /dev/null +++ b/spec/endpoint/profile_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Profile do + it 'has to be tested' +end diff --git a/spec/endpoint/repositories_spec.rb b/spec/endpoint/repositories_spec.rb new file mode 100644 index 00000000..d9136437 --- /dev/null +++ b/spec/endpoint/repositories_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Repositories do + it 'has to be tested' +end diff --git a/spec/endpoint/workers_spec.rb b/spec/endpoint/workers_spec.rb new file mode 100644 index 00000000..f169b161 --- /dev/null +++ b/spec/endpoint/workers_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint::Workers do + it 'has to be tested' +end diff --git a/spec/endpoint_spec.rb b/spec/endpoint_spec.rb new file mode 100644 index 00000000..d5a01f6b --- /dev/null +++ b/spec/endpoint_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Travis::Api::App::Endpoint do + class MyEndpoint < Travis::Api::App::Endpoint + set :prefix, '/my_endpoint' + get('/') { 'ok' } + end + + it 'sets up endpoints automatically under given prefix' do + get('/my_endpoint/').should be_ok + body.should == "ok" + end + + it 'does not require a trailing slash' do + get('/my_endpoint').should be_ok + body.should == "ok" + end +end diff --git a/spec/extensions/smart_constants_spec.rb b/spec/extensions/smart_constants_spec.rb new file mode 100644 index 00000000..989913e1 --- /dev/null +++ b/spec/extensions/smart_constants_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Travis::Api::App::Extensions::SmartConstants do + let(:some_app) do + Sinatra.new { register Travis::Api::App::Extensions::SmartConstants } + end + + describe :helpers do + it 'works' do # :) + some_app.helpers :json_renderer + some_app.ancestors.should include(Travis::Api::App::Helpers::JsonRenderer) + end + end + + describe :register do + it 'works' do # :) + some_app.register :subclass_tracker + some_app.should be_a(Travis::Api::App::Extensions::SubclassTracker) + end + end + +end diff --git a/spec/extensions/subclass_tracker_spec.rb b/spec/extensions/subclass_tracker_spec.rb new file mode 100644 index 00000000..181d2d15 --- /dev/null +++ b/spec/extensions/subclass_tracker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Travis::Api::App::Extensions::SubclassTracker do + let!(:root) { Sinatra.new { register Travis::Api::App::Extensions::SubclassTracker } } + let!(:left) { Class.new(root) } + let!(:right) { Class.new(root) } + let!(:sub1) { Class.new(right) } + let!(:sub2) { Class.new(right) } + + it 'tracks direct subclasses' do + classes = root.direct_subclasses + classes.size.should == 2 + classes.should include(left) + classes.should include(right) + end + + it 'tracks leaf subclasses' do + classes = root.subclasses + classes.size.should == 3 + classes.should include(left) + classes.should include(sub1) + classes.should include(sub2) + end + + it 'tracks subclasses of subclasses properly' do + classes = right.subclasses + classes.size.should == 2 + classes.should include(sub1) + classes.should include(sub2) + end +end diff --git a/spec/helpers/json_renderer_spec.rb b/spec/helpers/json_renderer_spec.rb new file mode 100644 index 00000000..1a9ce80f --- /dev/null +++ b/spec/helpers/json_renderer_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'json' + +describe Travis::Api::App::Helpers::JsonRenderer do + before do + mock_app do + helpers Travis::Api::App::Helpers::JsonRenderer + get('/') { {'foo' => 'bar'} } + end + end + + it 'renders body as json' do + get('/').should be_ok + JSON.load(body).should == {'foo' => 'bar'} + end +end diff --git a/spec/middleware/access_token_spec.rb b/spec/middleware/access_token_spec.rb new file mode 100644 index 00000000..355e37b6 --- /dev/null +++ b/spec/middleware/access_token_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Travis::Api::App::Middleware::AccessToken do + before do + mock_app do + use Travis::Api::App::Middleware::AccessToken + get('/check_cors') { 'ok' } + end + end + + it 'sets associated scope properly' + it 'lets through requests without a token' + it 'reject requests with an invalide token' + it 'rejects expired tokens' + it 'checks that the token corresponds to Origin' +end \ No newline at end of file diff --git a/spec/middleware/cors_spec.rb b/spec/middleware/cors_spec.rb new file mode 100644 index 00000000..034e1682 --- /dev/null +++ b/spec/middleware/cors_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Travis::Api::App::Middleware::Cors do + before do + mock_app do + use Travis::Api::App::Middleware::Cors + get('/check_cors') { 'ok' } + end + end + + describe 'normal request' do + before { get('/check_cors').should be_ok } + + it 'sets Access-Control-Allow-Origin' do + headers['Access-Control-Allow-Origin'].should == "*" + end + + it 'sets Access-Control-Allow-Credentials' do + headers['Access-Control-Allow-Credentials'].should == "true" + end + + it 'sets Access-Control-Expose-Headers' do + headers['Access-Control-Expose-Headers'].should == "Content-Type" + end + end + + describe 'OPTIONS requests' do + before { options('/').should be_ok } + + it 'sets Access-Control-Allow-Origin' do + headers['Access-Control-Allow-Origin'].should == "*" + end + + it 'sets Access-Control-Allow-Credentials' do + headers['Access-Control-Allow-Credentials'].should == "true" + end + + it 'sets Access-Control-Expose-Headers' do + headers['Access-Control-Expose-Headers'].should == "Content-Type" + end + + it 'sets Access-Control-Allow-Methods' do + headers['Access-Control-Allow-Methods'].should == "GET, POST, PATCH, PUT, DELETE" + end + + it 'sets Access-Control-Allow-Headers' do + headers['Access-Control-Allow-Headers'].should == "Content-Type, Authorization, Accept" + end + end +end diff --git a/spec/middleware/logging_spec.rb b/spec/middleware/logging_spec.rb new file mode 100644 index 00000000..a530d77b --- /dev/null +++ b/spec/middleware/logging_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Travis::Api::App::Middleware::Logging do + it 'configures ActiveRecord' do + ActiveRecord::Base.logger.should == Travis.logger + end + + it 'sets the logger' do + mock_app do + use Travis::Api::App::Middleware::Logging + get '/check_logger' do + logger.should == Travis.logger + 'ok' + end + end + + get('/check_logger').should be_ok + end +end diff --git a/spec/middleware_spec.rb b/spec/middleware_spec.rb new file mode 100644 index 00000000..58dd74ee --- /dev/null +++ b/spec/middleware_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Travis::Api::App::Middleware do + class MyMiddleware < Travis::Api::App::Middleware + get('/my_middleware') { 'ok' } + end + + it 'sets up middleware automatically' do + get('/my_middleware').should be_ok + body.should == "ok" + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..accd27bb --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +ENV['RACK_ENV'] = ENV['RAILS_ENV'] = ENV['ENV'] = 'test' + +require 'rspec' +require 'travis/api/app' +require 'sinatra/test_helpers' +require 'logger' + +Travis.logger = Logger.new(StringIO.new) +Travis::Api::App.setup + +Backports.require_relative_dir 'support' + +RSpec.configure do |config| + config.expect_with :rspec, :stdlib + config.include Sinatra::TestHelpers + config.before(:each) { set_app Travis::Api::App.new } +end diff --git a/spec/support/factories.rb b/spec/support/factories.rb new file mode 100644 index 00000000..1bec8835 --- /dev/null +++ b/spec/support/factories.rb @@ -0,0 +1,99 @@ +require 'factory_girl' + +FactoryGirl.define do + factory :build do + repository { Repository.first || Factory(:repository) } + association :request + association :commit + end + + factory :commit do + repository { Repository.first || Factory(:repository) } + commit '62aae5f70ceee39123ef' + branch 'master' + message 'the commit message' + committed_at '2011-11-11T11:11:11Z' + committer_name 'Sven Fuchs' + committer_email 'svenfuchs@artweb-design.de' + author_name 'Sven Fuchs' + author_email 'svenfuchs@artweb-design.de' + compare_url 'https://github.com/svenfuchs/minimal/compare/master...develop' + end + + factory :test, :class => 'Job::Test' do + repository { Repository.first || Factory(:repository) } + commit { Factory(:commit) } + source { Factory(:build) } + log { Factory(:log) } + queue "ruby" + end + + factory :log, :class => 'Artifact::Log' do + content '$ bundle install --pa' + end + + factory :request do + repository { Repository.first || Factory(:repository) } + association :commit + token 'the-token' + end + + factory :repository do + name 'minimal' + owner_name 'svenfuchs' + owner_email 'svenfuchs@artweb-design.de' + url { |r| "http://github.com/#{r.owner_name}/#{r.name}" } + last_duration 60 + created_at { |r| Time.utc(2011, 01, 30, 5, 25) } + updated_at { |r| r.created_at + 5.minutes } + end + + factory :minimal, :parent => :repository do + end + + factory :enginex, :class => Repository do + name 'enginex' + owner_name 'josevalim' + last_duration 30 + end + + factory :running_build, :parent => :build do + repository { Factory(:repository, :name => 'running_build') } + state 'started' + end + + factory :successful_build, :parent => :build do + repository { Factory(:repository, :name => 'successful_build', :last_build_result => 0) } + result 0 + state 'finished' + started_at { Time.now.utc } + finished_at { Time.now.utc } + end + + factory :broken_build, :parent => :build do + repository { Factory(:repository, :name => 'broken_build', :last_build_result => 1) } + result 1 + state 'finished' + started_at { Time.now.utc } + finished_at { Time.now.utc } + end + + factory :user do + name 'Sven Fuchs' + login 'svenfuchs' + email 'sven@fuchs.com' + tokens { [Token.new] } + end + + factory :worker do + name 'worker-1' + host 'ruby-1.workers.travis-ci.org' + state :working + last_seen_at { Time.now.utc } + end + + factory :ssl_key do + private_key "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDGed1uxl9szL0PVE/B6v9PDso+xRHs9e9YDB8Dm+QYFDyddud1\nn1134ZY39Dxg6zNhXDGKYilHP4E9boIuvgfSADN12eD1clogX46M4oBGgUAhtr5Q\nvGLn9TEW4IbeI+nDshMJLTLethCmB6Hwm5Ld9QnRVT6U/AztOTv9eJ/xKQIDAQAB\nAoGABQ3zcq/AnF+2bN6DzXdzmwrQYbrZEwTMXJyqaYgdzfMt/ACcMmWllrj6/1/L\n7dfvjgowBMstK/BVFUBsNk6GmmoCDHFAU+BgeyyqUxyeb63+0dIDwVYx9LHTL4dr\n9a8cVyeefqc3mqB13B9NUlS40Ij4kuK6EOGP3DZwC1FQVwECQQDtBQFqgRuNdfbV\naGIcXnuMnD4BGrnFHm0IBdLYsK4ULL85gFbhEew6DTYGYlGqX1dXbXYue8F18D8i\nzqL6HOBhAkEA1l6zvLdC2t3J9UnwpkwU0jSPX4BpHH7IkrCoGRggjwtbSxJFcCKB\nRrbPFDNAwchsa2/ldXSBrFg6Y7GlwF3lyQJAaJk+6LuVZzZZ+hAYzCA+Me15x479\n0Kn+v/2h8RL3n9ungD7NGIKKV4wg/WxCUgfFScX608S1udCObFP4xJwdwQJBALtl\nwEQqBGSmXCV0xM3rVoxH7En1TG3fm2E400pUoCnMKLugtlkHoPF7X91tzJ9aoQTu\npa2e8rkBy9FY++gFbZkCQAJ46lGEXZJqcACvLX0t/+RrvmqWMxCydLFG50kOnD8b\nVNILVyUn1lYasTs4aMYr6BRtVZoCxqV5/+rkMhb1eOM=\n-----END RSA PRIVATE KEY-----\n" + public_key "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAMZ53W7GX2zMvQ9UT8Hq/08Oyj7FEez171gMHwOb5BgUPJ1253WfXXfh\nljf0PGDrM2FcMYpiKUc/gT1ugi6+B9IAM3XZ4PVyWiBfjozigEaBQCG2vlC8Yuf1\nMRbght4j6cOyEwktMt62EKYHofCbkt31CdFVPpT8DO05O/14n/EpAgMBAAE=\n-----END RSA PUBLIC KEY-----\n" + end +end diff --git a/travis-api.gemspec b/travis-api.gemspec deleted file mode 100644 index df9c7c12..00000000 --- a/travis-api.gemspec +++ /dev/null @@ -1,8 +0,0 @@ -# encoding: utf-8 - -Gem::Specification.new do |s| - s.name = 'travis-api' - s.version = '0.0.1' - s.require_path = 'lib' -end -