diff options
author | azul <azul@leap.se> | 2014-07-17 12:16:07 +0200 |
---|---|---|
committer | azul <azul@leap.se> | 2014-07-17 12:16:07 +0200 |
commit | ade74d8a9091ae607586d7b287a0579a2ee7af8e (patch) | |
tree | 74273b8ba7e35d0fb3c96aa79e63c93086d15146 | |
parent | 952bc18e8333ca5c3e6e16f8059f84a1414d5f6f (diff) | |
parent | e86cccb4b89540f3bd403110d051b2723be781b9 (diff) |
Merge pull request #176 from azul/feature/api-authenticated-configs
API: Authenticated access to config settings
48 files changed, 635 insertions, 106 deletions
@@ -55,6 +55,9 @@ group :test do # billing tests gem 'fake_braintree', require: false + + # we use cucumber to document and test the api + gem 'cucumber-rails', require: false end group :test, :development do diff --git a/Gemfile.lock b/Gemfile.lock index 1060d70..38a8793 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,18 @@ GEM actionpack couchrest couchrest_model + cucumber (1.3.15) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.1) + cucumber-rails (1.4.1) + capybara (>= 1.1.2, < 3) + cucumber (>= 1.3.8, < 2) + mime-types (~> 1.16) + nokogiri (~> 1.5) + rails (>= 3, < 5) daemons (1.1.9) debugger (1.6.6) columnize (>= 0.3.1) @@ -95,6 +107,7 @@ GEM debugger-ruby_core_source (~> 1.3.2) debugger-linecache (1.2.0) debugger-ruby_core_source (1.3.2) + diff-lcs (1.2.5) erubis (2.7.0) eventmachine (1.0.3) execjs (2.0.2) @@ -113,6 +126,8 @@ GEM faker (1.2.0) i18n (~> 0.5) ffi (1.9.3) + gherkin (2.12.2) + multi_json (~> 1.3) haml (3.1.8) haml-rails (0.3.5) actionpack (>= 3.1, < 4.1) @@ -146,6 +161,7 @@ GEM mocha (0.13.3) metaclass (~> 0.0.1) multi_json (1.10.0) + multi_test (0.1.1) nokogiri (1.6.1) mini_portile (~> 0.5.0) phantomjs-binaries (1.9.2.3) @@ -249,6 +265,7 @@ DEPENDENCIES couchrest (~> 1.1.3) couchrest_model (~> 2.0.0) couchrest_session_store (~> 0.2.4) + cucumber-rails debugger factory_girl_rails fake_braintree diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb new file mode 100644 index 0000000..0aa9507 --- /dev/null +++ b/app/controllers/api_controller.rb @@ -0,0 +1,11 @@ +class ApiController < ApplicationController + + skip_before_filter :verify_authenticity_token + respond_to :json + + def require_login + require_token + end + +end + diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 1f73f38..e2b24f0 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -16,7 +16,7 @@ module ControllerExtension::Authentication end def require_login - access_denied unless logged_in? + login_required unless logged_in? end # some actions only make sense if you are not logged in yet. @@ -26,21 +26,6 @@ module ControllerExtension::Authentication redirect_to home_url if logged_in? end - def access_denied - respond_to do |format| - format.html do - if logged_in? - redirect_to home_url, :alert => t(:not_authorized) - else - redirect_to login_url, :alert => t(:not_authorized_login) - end - end - format.json do - render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity - end - end - end - def admin? current_user.is_admin? end diff --git a/app/controllers/controller_extension/errors.rb b/app/controllers/controller_extension/errors.rb new file mode 100644 index 0000000..8f8edde --- /dev/null +++ b/app/controllers/controller_extension/errors.rb @@ -0,0 +1,34 @@ +module ControllerExtension::Errors + extend ActiveSupport::Concern + + protected + + def access_denied + respond_to_error :not_authorized, :forbidden, home_url + end + + def login_required + # Warden will intercept the 401 response and call + # SessionController#unauthenticated instead. + respond_to_error :not_authorized_login, :unauthorized, login_url + end + + def not_found + respond_to_error :not_found, :not_found, home_url + end + + + def respond_to_error(message, status=nil, redirect=nil) + error = message + message = t(message) if message.is_a?(Symbol) + respond_to do |format| + format.html do + redirect_to redirect, alert: message + end + format.json do + status ||= :unprocessable_entity + render json: {error: error, message: message}, status: status + end + end + end +end diff --git a/app/controllers/users_base_controller.rb b/app/controllers/controller_extension/fetch_user.rb index 9becf0d..695d723 100644 --- a/app/controllers/users_base_controller.rb +++ b/app/controllers/controller_extension/fetch_user.rb @@ -1,8 +1,10 @@ # -# common base class for all user related controllers +# fetch the user taking into account permissions. +# While normal users can only change settings for themselves +# admins can change things for all users. # - -class UsersBaseController < ApplicationController +module ControllerExtension::FetchUser + extend ActiveSupport::Concern protected diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb new file mode 100644 index 0000000..6be919a --- /dev/null +++ b/app/controllers/controller_extension/json_file.rb @@ -0,0 +1,23 @@ +module ControllerExtension::JsonFile + extend ActiveSupport::Concern + include ControllerExtension::Errors + + protected + + def send_file + if stale?(:last_modified => @file.mtime) + response.content_type = 'application/json' + render :text => @file.read + end + end + + def fetch_file + if File.exists?(@filename) + @file = File.new(@filename) + else + not_found + end + end + +end + diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb index b0ed624..4ad1977 100644 --- a/app/controllers/controller_extension/token_authentication.rb +++ b/app/controllers/controller_extension/token_authentication.rb @@ -1,6 +1,8 @@ module ControllerExtension::TokenAuthentication extend ActiveSupport::Concern + protected + def token @token ||= authenticate_with_http_token do |token, options| Token.find_by_token(token) @@ -12,7 +14,7 @@ module ControllerExtension::TokenAuthentication end def require_token - access_denied unless token_authenticate + login_required unless token_authenticate end def logout diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8919a4d..66eba40 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -16,6 +16,13 @@ class SessionsController < ApplicationController end # + # Warden will catch all 401s and run this instead: + # + def unauthenticated + login_required + end + + # # this is a bad hack, but user_url(user) is not available # also, this doesn't work because the redirect happens as a PUT. no idea why. # diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb index c669316..c78e006 100644 --- a/app/controllers/static_config_controller.rb +++ b/app/controllers/static_config_controller.rb @@ -2,23 +2,28 @@ # This controller is responsible for returning some static config files, such as /provider.json # class StaticConfigController < ActionController::Base + include ControllerExtension::JsonFile - PROVIDER_JSON = File.join(Rails.root, 'config', 'provider', 'provider.json') + before_filter :set_minimum_client_version + before_filter :set_filename + before_filter :fetch_file + + PROVIDER_JSON = Rails.root.join('config', 'provider', 'provider.json') - # - # return the provider.json, ensuring that the header X-Minimum-Client-Version is sent - # regardless if a 200 or 304 (not modified) response is sent. - # def provider - response.headers["X-Minimum-Client-Version"] = APP_CONFIG[:minimum_client_version].to_s - if File.exists?(PROVIDER_JSON) - if stale?(:last_modified => File.mtime(PROVIDER_JSON)) - response.content_type = 'application/json' - render :text => File.read(PROVIDER_JSON) - end - else - render :text => 'not found', :status => 404 - end + send_file end -end
\ No newline at end of file + protected + + # ensure that the header X-Minimum-Client-Version is sent + # regardless if a 200 or 304 (not modified) or 404 response is sent. + def set_minimum_client_version + response.headers["X-Minimum-Client-Version"] = + APP_CONFIG[:minimum_client_version].to_s + end + + def set_filename + @filename = PROVIDER_JSON + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0f822cb..dcf7607 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,7 +2,8 @@ # This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb # -class UsersController < UsersBaseController +class UsersController < ApplicationController + include ControllerExtension::FetchUser before_filter :require_login, :except => [:new] before_filter :redirect_if_logged_in, :only => [:new] diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index b6d1d0b..68d6586 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,4 +1,4 @@ -class V1::CertsController < ApplicationController +class V1::CertsController < ApiController before_filter :require_login, :unless => :anonymous_certs_allowed? diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb new file mode 100644 index 0000000..accdf5a --- /dev/null +++ b/app/controllers/v1/configs_controller.rb @@ -0,0 +1,34 @@ +class V1::ConfigsController < ApiController + include ControllerExtension::JsonFile + + before_filter :require_login + before_filter :sanitize_filename, only: :show + before_filter :fetch_file, only: :show + + def index + render json: {services: service_paths} + end + + def show + send_file + end + + SERVICES = { + soledad: "soledad-service.json", + eip: "eip-service.json", + smtp: "smtp-service.json" + } + + protected + + def service_paths + Hash[SERVICES.map{|k,v| [k,"/1/configs/#{v}"] } ] + end + + def sanitize_filename + @filename = params[:id].downcase + @filename += '.json' unless @filename.ends_with?('.json') + access_denied unless SERVICES.values.include? name + @filename = Rails.root.join('public', '1', 'config', @filename) + end +end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb index 85156b7..a9b93a9 100644 --- a/app/controllers/v1/messages_controller.rb +++ b/app/controllers/v1/messages_controller.rb @@ -1,10 +1,7 @@ module V1 - class MessagesController < ApplicationController + class MessagesController < ApiController - skip_before_filter :verify_authenticity_token - before_filter :require_token - - respond_to :json + before_filter :require_login def index render json: current_user.messages diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/v1/services_controller.rb index 594940e..114870f 100644 --- a/app/controllers/v1/services_controller.rb +++ b/app/controllers/v1/services_controller.rb @@ -1,6 +1,4 @@ -class V1::ServicesController < ApplicationController - - respond_to :json +class V1::ServicesController < ApiController def show respond_with current_user.effective_service_level diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index d88fcdc..a343d9b 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -1,8 +1,7 @@ module V1 - class SessionsController < ApplicationController + class SessionsController < ApiController - skip_before_filter :verify_authenticity_token - before_filter :require_token, only: :destroy + before_filter :require_login, only: :destroy def new @session = Session.new diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index 377a49c..fa53b26 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -1,4 +1,4 @@ -class V1::SmtpCertsController < ApplicationController +class V1::SmtpCertsController < ApiController before_filter :require_login before_filter :require_email_account diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index abaefd8..bfa04fc 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,10 +1,10 @@ module V1 - class UsersController < UsersBaseController + class UsersController < ApiController + include ControllerExtension::FetchUser - skip_before_filter :verify_authenticity_token before_filter :fetch_user, :only => [:update] before_filter :require_admin, :only => [:index] - before_filter :require_token, :only => [:update] + before_filter :require_login, :only => [:index, :update] before_filter :require_registration_allowed, only: :create respond_to :json @@ -29,11 +29,12 @@ module V1 respond_with @user end + protected + def require_registration_allowed unless APP_CONFIG[:allow_registration] head :forbidden end end - end end diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000..19b288d --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/config/initializers/add_controller_methods.rb b/config/initializers/add_controller_methods.rb index 03e8393..f107544 100644 --- a/config/initializers/add_controller_methods.rb +++ b/config/initializers/add_controller_methods.rb @@ -2,4 +2,5 @@ ActiveSupport.on_load(:application_controller) do include ControllerExtension::Authentication include ControllerExtension::TokenAuthentication include ControllerExtension::Flash + include ControllerExtension::Errors end diff --git a/config/routes.rb b/config/routes.rb index 468e14e..3936824 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,7 @@ LeapWeb::Application.routes.draw do resource :cert, :only => [:show, :create] resource :smtp_cert, :only => [:create] resource :service, :only => [:show] + resources :configs, :only => [:index, :show] end scope "(:locale)", :locale => MATCH_LOCALE do diff --git a/engines/billing/test/functional/customers_controller_test.rb b/engines/billing/test/functional/customers_controller_test.rb index 46c33c9..cc82fc1 100644 --- a/engines/billing/test/functional/customers_controller_test.rb +++ b/engines/billing/test/functional/customers_controller_test.rb @@ -27,11 +27,11 @@ class CustomersControllerTest < ActionController::TestCase test "no access if not logged in" do get :new - assert_access_denied(true, false) + assert_login_required get :show, :id => @customer.braintree_customer_id - assert_access_denied(true, false) + assert_login_required get :edit, :id => @customer.braintree_customer_id - assert_access_denied(true, false) + assert_login_required end diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml index 711421d..f285b8b 100644 --- a/engines/support/app/views/tickets/_new_comment_form.html.haml +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -4,10 +4,17 @@ = simple_form_for @ticket, :html => {:class => 'slim'} do |f| = hidden_ticket_fields = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c| - = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} + = c.input :body, :label => false, :as => :text, + :input_html => {:class => "full-width", :rows=> 5} - if admin? - = c.input :private, :as => :boolean, :label => false, :inline_label => true - = f.button :loading, t(".post_reply"), class: 'btn-primary', value: 'post_reply' + = c.input :private, + :as => :boolean, + :label => false, + :inline_label => true + = f.button :loading, t(".post_reply", cascade: true), + class: 'btn-primary', + value: 'post_reply' - if logged_in? && @ticket.is_open - = f.button :loading, t(".reply_and_close"), value: 'reply_and_close' + = f.button :loading, t(".reply_and_close", cascade: true), + value: 'reply_and_close' = btn t(".cancel"), auto_tickets_path diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index e36f5f6..a7a2011 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -45,7 +45,7 @@ class TicketsControllerTest < ActionController::TestCase user = find_record :user ticket = find_record :ticket, :created_by => user.id get :show, :id => ticket.id - assert_login_required + assert_access_denied end test "user tickets are visible to creator" do diff --git a/features/authentication.feature b/features/authentication.feature new file mode 100644 index 0000000..52b562f --- /dev/null +++ b/features/authentication.feature @@ -0,0 +1,24 @@ +Feature: Authentication + + Authentication is handled with SRP. Once the SRP handshake has been successful a token will be transmitted. This token is used to authenticate further requests. + + In the scenarios MY_AUTH_TOKEN will serve as a placeholder for the actual token received. + + Background: + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + + Scenario: Submitting a valid token + Given I authenticated + And I set headers: + | Authorization | Token token="MY_AUTH_TOKEN" | + When I send a GET request to "/1/configs.json" + Then the response status should be "200" + + Scenario: Submitting an invalid token + Given I authenticated + And I set headers: + | Authorization | Token token="InvalidToken" | + When I send a GET request to "/1/configs.json" + Then the response status should be "401" diff --git a/features/config.feature b/features/config.feature new file mode 100644 index 0000000..6adaed9 --- /dev/null +++ b/features/config.feature @@ -0,0 +1,46 @@ +Feature: Download Provider Configuration + + The LEAP Provider exposes parts of its configuration through the API. + + This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. Which is available without authentication (see unauthenticated.feature). + + More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`. + + Background: + Given I authenticated + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + | Authorization | Token token="MY_AUTH_TOKEN" | + + @tempfile + Scenario: Fetch provider config + Given the provider config is: + """ + {"config": "me"} + """ + When I send a GET request to "/provider.json" + Then the response status should be "200" + And the response should be: + """ + {"config": "me"} + """ + + Scenario: Missing provider config + When I send a GET request to "/provider.json" + Then the response status should be "404" + And the response should have "error" with "not_found" + + Scenario: Fetch list of available configs + When I send a GET request to "/1/configs.json" + Then the response status should be "200" + And the response should be: + """ + { + "services": { + "soledad": "/1/configs/soledad-service.json", + "eip": "/1/configs/eip-service.json", + "smtp": "/1/configs/smtp-service.json" + } + } + """ diff --git a/features/step_definitions/.gitkeep b/features/step_definitions/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/features/step_definitions/.gitkeep diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb new file mode 100644 index 0000000..a4f369c --- /dev/null +++ b/features/step_definitions/api_steps.rb @@ -0,0 +1,131 @@ +if defined?(Rack) + + # Monkey patch Rack::MockResponse to work properly with response debugging + class Rack::MockResponse + def to_str + body + end + end + + World(Rack::Test::Methods) + +end + +Given /^I set headers:$/ do |headers| + headers.rows_hash.each do |key,value| + value.sub!('MY_AUTH_TOKEN', @my_auth_token.to_s) if @my_auth_token + header key, value + end +end + +Given /^I send and accept (XML|JSON)$/ do |type| + header 'Accept', "application/#{type.downcase}" + header 'Content-Type', "application/#{type.downcase}" +end + +Given /^I send and accept HTML$/ do + header 'Accept', "text/html" + header 'Content-Type', "application/x-www-form-urlencoded" +end + +When /^I authenticate as the user "([^"]*)" with the password "([^"]*)"$/ do |user, pass| + authorize user, pass +end + +When /^I digest\-authenticate as the user "(.*?)" with the password "(.*?)"$/ do |user, pass| + digest_authorize user, pass +end + +When /^I send a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args| + request_type = args.shift + path = args.shift + input = args.shift + + request_opts = {method: request_type.downcase.to_sym} + + unless input.nil? + if input.class == Cucumber::Ast::Table + request_opts[:params] = input.rows_hash + else + request_opts[:input] = input + end + end + + request path, request_opts +end + +Then /^show me the (unparsed)?\s?response$/ do |unparsed| + if unparsed == 'unparsed' + puts last_response.body + elsif last_response.headers['Content-Type'] =~ /json/ + json_response = JSON.parse(last_response.body) + puts JSON.pretty_generate(json_response) + else + puts last_response.headers + puts last_response.body + end +end + +Then /^the response status should be "([^"]*)"$/ do |status| + if self.respond_to? :should + last_response.status.should == status.to_i + else + assert_equal status.to_i, last_response.status + end +end + +Then /^the response should (not)?\s?have "([^"]*)"$/ do |negative, key| + json = JSON.parse(last_response.body) + if self.respond_to?(:should) + if negative.present? + json[key].should be_blank + else + json[key].should be_present + end + else + if negative.present? + assert json[key].blank? + else + assert json[key].present? + end + end +end + + +Then /^the response should (not)?\s?have "([^"]*)" with(?: the text)? "([^"]*)"$/ do |negative, key, text| + json = JSON.parse(last_response.body) + if self.respond_to?(:should) + if negative.present? + json[key].should_not == text + else + results.should == text + end + else + if negative.present? + assert ! json[key] == text + else + assert_equal text, json[key] + end + end +end + +Then /^the response should be:$/ do |json| + expected = JSON.parse(json) + actual = JSON.parse(last_response.body) + + if self.respond_to?(:should) + actual.should == expected + else + assert_equal expected, actual + end +end + +Then /^the response should have "([^"]*)" with a length of (\d+)$/ do |json_path, length| + json = JSON.parse(last_response.body) + results = JsonPath.new(json_path).on(json) + if self.respond_to?(:should) + results.length.should == length.to_i + else + assert_equal length.to_i, results.length + end +end diff --git a/features/step_definitions/auth_steps.rb b/features/step_definitions/auth_steps.rb new file mode 100644 index 0000000..00d9004 --- /dev/null +++ b/features/step_definitions/auth_steps.rb @@ -0,0 +1,6 @@ + +Given /^I authenticated$/ do + @user = FactoryGirl.create(:user) + @my_auth_token = Token.create user_id: @user.id +end + diff --git a/features/step_definitions/config_steps.rb b/features/step_definitions/config_steps.rb new file mode 100644 index 0000000..50ae829 --- /dev/null +++ b/features/step_definitions/config_steps.rb @@ -0,0 +1,6 @@ +Given /the provider config is:$/ do |config| + @tempfile = Tempfile.new('provider.json') + @tempfile.write config + @tempfile.close + StaticConfigController::PROVIDER_JSON = @tempfile.path +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000..d3067db --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,58 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +require 'cucumber/rails' + +# Capybara defaults to CSS3 selectors rather than XPath. +# If you'd prefer to use XPath, just uncomment this line and adjust any +# selectors in your step definitions to use the XPath syntax. +# Capybara.default_selector = :xpath + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + #DatabaseCleaner.strategy = :truncation +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { :except => [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation + diff --git a/features/support/hooks.rb b/features/support/hooks.rb new file mode 100644 index 0000000..19928d8 --- /dev/null +++ b/features/support/hooks.rb @@ -0,0 +1,18 @@ +After '@tempfile' do + if @tempfile + @tempfile.close + @tempfile.unlink + end +end + +After do |scenario| + if scenario.failed? + logfile_path = Rails.root + 'tmp' + logfile_path += "#{scenario.title.gsub(/\s/, '_')}.log" + File.open(logfile_path, 'w') do |test_log| + test_log.puts scenario.title + test_log.puts "=========================" + test_log.puts `tail log/test.log -n 200` + end + end +end diff --git a/features/unauthenticated.feature b/features/unauthenticated.feature new file mode 100644 index 0000000..120274b --- /dev/null +++ b/features/unauthenticated.feature @@ -0,0 +1,29 @@ +Feature: Unauthenticated API endpoints + + Most of the LEAP Provider API requires authentication. + However there are a few exceptions - mostly prerequisits of authenticating. This feature and the authentication feature document these. + + Background: + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + + @tempfile + Scenario: Fetch provider config + Given the provider config is: + """ + {"config": "me"} + """ + When I send a GET request to "/provider.json" + Then the response status should be "200" + And the response should be: + """ + {"config": "me"} + """ + + Scenario: Authentication required for all other API endpoints + When I send a GET request to "/1/configs" + Then the response status should be "401" + And the response should have "error" with "not_authorized_login" + And the response should have "message" + diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000..9f53ce4 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,65 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({:ok => 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({:wip => 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({:rerun => 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task :all => [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + end + desc 'Alias for cucumber:ok' + task :cucumber => 'cucumber:ok' + + task :default => :cucumber + + task :features => :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task :stats => 'cucumber:statsetup' +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/script/cucumber b/script/cucumber new file mode 100755 index 0000000..7fa5c92 --- /dev/null +++ b/script/cucumber @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index c4c922b..b558ad8 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -9,13 +9,13 @@ class ApplicationControllerTest < ActionController::TestCase def test_require_login_redirect @controller.send(:require_login) - assert_access_denied(true, false) + assert_login_required end def test_require_login login @controller.send(:require_login) - assert_access_denied(false) + assert_access_granted end def test_require_admin diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 4af9ca6..7d1745c 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -52,7 +52,7 @@ class UsersControllerTest < ActionController::TestCase nonid = 'thisisnotanexistinguserid' get :show, :id => nonid - assert_access_denied(true, false) + assert_login_required end test "may not show non-existing user without admin" do diff --git a/test/functional/v1/messages_controller_test.rb b/test/functional/v1/messages_controller_test.rb index 24a5b1f..a50fded 100644 --- a/test/functional/v1/messages_controller_test.rb +++ b/test/functional/v1/messages_controller_test.rb @@ -51,7 +51,7 @@ class V1::MessagesControllerTest < ActionController::TestCase test "fails if not authenticated" do get :index, :format => :json - assert_access_denied + assert_login_required end end diff --git a/test/functional/v1/users_controller_test.rb b/test/functional/v1/users_controller_test.rb index fe3cfe7..ffe2484 100644 --- a/test/functional/v1/users_controller_test.rb +++ b/test/functional/v1/users_controller_test.rb @@ -34,7 +34,9 @@ class V1::UsersControllerTest < ActionController::TestCase test "user cannot update other user" do user = find_record :user login - put :update, :user => record_attributes_for(:user_with_settings), :id => user.id, :format => :json + put :update, id: user.id, + user: record_attributes_for(:user_with_settings), + :format => :json assert_access_denied end diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb index 74d439a..118fb9f 100644 --- a/test/integration/api/cert_test.rb +++ b/test/integration/api/cert_test.rb @@ -14,7 +14,7 @@ class CertTest < ApiIntegrationTest test "fetching certs requires login by default" do get '/1/cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_login_required end test "retrieve anonymous eip cert" do diff --git a/test/integration/api/login_test.rb b/test/integration/api/login_test.rb index 92d153f..f37639e 100644 --- a/test/integration/api/login_test.rb +++ b/test/integration/api/login_test.rb @@ -45,6 +45,6 @@ class LoginTest < SrpTest test "logout requires token" do authenticate logout(nil, {}) - assert_equal 422, last_response.status + assert_login_required end end diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index 7697e44..b1bfd43 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -42,13 +42,13 @@ class SmtpCertTest < ApiIntegrationTest test "fetching smtp certs requires email account" do login post '/1/smtp_cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_access_denied end test "no anonymous smtp certs" do with_config allow_anonymous_certs: true do post '/1/smtp_cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_login_required end end end diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb index 26adc8c..946450e 100644 --- a/test/integration/api/srp_test.rb +++ b/test/integration/api/srp_test.rb @@ -1,5 +1,4 @@ class SrpTest < RackTest - include AssertResponses teardown do if @user diff --git a/test/integration/api/update_account_test.rb b/test/integration/api/update_account_test.rb index 63429e7..16bbb8c 100644 --- a/test/integration/api/update_account_test.rb +++ b/test/integration/api/update_account_test.rb @@ -16,7 +16,7 @@ class UpdateAccountTest < SrpTest authenticate put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', user_params(password: "No! Verify me instead.") - assert_access_denied + assert_login_required end test "update password via api" do diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb index 19c2768..1c9d49d 100644 --- a/test/support/assert_responses.rb +++ b/test/support/assert_responses.rb @@ -55,6 +55,25 @@ module AssertResponses get_response.headers["Content-Disposition"] end + def assert_login_required + assert_error_response :not_authorized_login, :unauthorized + end + + def assert_access_denied + assert_error_response :not_authorized, :forbidden + end + + def assert_error_response(key, status=nil) + message = I18n.t(key) + if content_type == 'application/json' + status ||= :unprocessable_entity + assert_json_response('error' => key.to_s, 'message' => message) + assert_response status + else + assert_equal({:alert => message}, flash.to_hash) + end + end + end class ::ActionController::TestCase diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb index 38c2ea1..7af3341 100644 --- a/test/support/auth_test_helper.rb +++ b/test/support/auth_test_helper.rb @@ -19,27 +19,9 @@ module AuthTestHelper return @current_user end - def assert_login_required - assert_access_denied(true, false) - end - - def assert_access_denied(denied = true, logged_in = true) - if denied - if @response.content_type == 'application/json' - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity - else - if logged_in - assert_equal({:alert => I18n.t(:not_authorized)}, flash.to_hash) - assert_redirected_to home_url - else - assert_equal({:alert => I18n.t(:not_authorized_login)}, flash.to_hash) - assert_redirected_to login_url - end - end - else - assert flash[:alert].blank? - end + def assert_access_granted + assert flash[:alert].blank?, + "expected to have access but there was a flash alert" end def expect_logout diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb index 806339a..2c9fa9a 100644 --- a/test/support/rack_test.rb +++ b/test/support/rack_test.rb @@ -3,6 +3,7 @@ require_relative 'assert_responses' class RackTest < ActiveSupport::TestCase include Rack::Test::Methods include Warden::Test::Helpers + include AssertResponses CONFIG_RU = (Rails.root + 'config.ru').to_s OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first @@ -11,11 +12,6 @@ class RackTest < ActiveSupport::TestCase OUTER_APP end - def assert_access_denied - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity - end - # inspired by rails 4 # -> actionpack/lib/action_dispatch/testing/assertions/response.rb def assert_response(type, message = nil) diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index cb0f6bd..f5c95f8 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -14,23 +14,23 @@ class IdentityTest < ActiveSupport::TestCase end end - test "blank @identity does not crash on valid?" do + test "blank identity does not crash on valid?" do @id = Identity.new assert !@id.valid? end - test "enabled @identity requires destination" do + test "enabled identity requires destination" do @id = Identity.new user: @user, address: @user.email_address assert !@id.valid? assert_equal ["can't be blank"], @id.errors[:destination] end - test "disabled @identity requires no destination" do + test "disabled identity requires no destination" do @id = Identity.new address: @user.email_address assert @id.valid? end - test "initial @identity for a user" do + test "initial identity for a user" do @id = Identity.for(@user) assert_equal @user.email_address, @id.address assert_equal @user.email_address, @id.destination @@ -90,7 +90,7 @@ class IdentityTest < ActiveSupport::TestCase assert_equal @id.keys[:pgp], result["value"] end - test "fail to add non-local email address as @identity address" do + test "fail to add non-local email address as identity address" do @id = Identity.for @user, address: forward_address assert !@id.valid? assert_match /needs to end in/, @id.errors[:address].first @@ -110,7 +110,7 @@ class IdentityTest < ActiveSupport::TestCase assert @id.errors.messages[:destination].include? "needs to be a valid email address" end - test "disabled @identity" do + test "disabled identity" do @id = Identity.for(@user) @id.disable assert_equal @user.email_address, @id.address @@ -120,7 +120,7 @@ class IdentityTest < ActiveSupport::TestCase assert @id.valid? end - test "disabled @identity blocks handle" do + test "disabled identity blocks handle" do @id = Identity.for(@user) @id.disable @id.save @@ -177,7 +177,9 @@ class IdentityTest < ActiveSupport::TestCase end def cert_stub - @cert_stub ||= stub expiry: 1.month.from_now, + # make this expire later than the others so it's on top + # when sorting by expiry descending. + @cert_stub ||= stub expiry: 2.month.from_now, fingerprint: SecureRandom.hex end end |