diff options
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  | 
