summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--Gemfile43
-rw-r--r--Gemfile.lock390
-rw-r--r--README.md58
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/leap.js2
-rw-r--r--app/assets/javascripts/typeahead.bundle.js2451
-rw-r--r--app/assets/javascripts/users.js2
-rw-r--r--app/assets/stylesheets/application.scss4
-rw-r--r--app/assets/stylesheets/backport.scss24
-rw-r--r--app/assets/stylesheets/leap.scss23
-rw-r--r--app/controllers/account_controller.rb17
-rw-r--r--app/controllers/api/certs_controller.rb (renamed from app/controllers/v1/certs_controller.rb)2
-rw-r--r--app/controllers/api/configs_controller.rb (renamed from app/controllers/v1/configs_controller.rb)14
-rw-r--r--app/controllers/api/identities_controller.rb (renamed from app/controllers/v1/identities_controller.rb)2
-rw-r--r--app/controllers/api/messages_controller.rb (renamed from app/controllers/v1/messages_controller.rb)2
-rw-r--r--app/controllers/api/services_controller.rb (renamed from app/controllers/v1/services_controller.rb)2
-rw-r--r--app/controllers/api/sessions_controller.rb (renamed from app/controllers/v1/sessions_controller.rb)2
-rw-r--r--app/controllers/api/smtp_certs_controller.rb (renamed from app/controllers/v1/smtp_certs_controller.rb)2
-rw-r--r--app/controllers/api/users_controller.rb (renamed from app/controllers/v1/users_controller.rb)9
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/controller_extension/fetch_user.rb2
-rw-r--r--app/controllers/controller_extension/json_file.rb23
-rw-r--r--app/controllers/static_config_controller.rb11
-rw-r--r--app/controllers/users_controller.rb28
-rw-r--r--app/helpers/application_helper.rb3
-rw-r--r--app/helpers/link_helper.rb4
-rw-r--r--app/helpers/navigation_helper.rb6
-rw-r--r--app/models/account.rb10
-rw-r--r--app/models/api_monitor_user.rb11
-rw-r--r--app/models/api_user.rb13
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/token.rb4
-rw-r--r--app/models/user.rb3
-rw-r--r--app/views/account/new.html.haml (renamed from app/views/users/new.html.haml)10
-rw-r--r--app/views/api/sessions/new.json.erb (renamed from app/views/v1/sessions/new.json.erb)0
-rw-r--r--app/views/common/_action_buttons.html.haml10
-rw-r--r--app/views/common/_download_button.html.haml8
-rw-r--r--app/views/common/_home_page_buttons.html.haml4
-rw-r--r--app/views/home/_content.html.haml6
-rw-r--r--app/views/layouts/_content.html.haml8
-rw-r--r--app/views/layouts/application.html.haml8
-rw-r--r--app/views/sessions/_warnings.html.haml (renamed from app/views/users/_warnings.html.haml)0
-rw-r--r--app/views/sessions/new.html.haml6
-rw-r--r--app/views/users/_change_password.html.haml2
-rw-r--r--app/views/users/_change_pgp_key.html.haml2
-rw-r--r--app/views/users/_change_service_level.html.haml17
-rw-r--r--app/views/users/_contact_email.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rwxr-xr-xbin/bundle3
-rwxr-xr-xbin/rails4
-rwxr-xr-xbin/rake4
-rwxr-xr-xbin/setup29
-rw-r--r--config/application.rb60
-rw-r--r--config/boot.rb5
-rw-r--r--config/defaults.yml1
-rw-r--r--config/environment.rb6
-rw-r--r--config/environments/development.rb48
-rw-r--r--config/environments/production.rb70
-rw-r--r--config/environments/test.rb33
-rw-r--r--config/initializers/client_side_validations.rb12
-rw-r--r--config/initializers/customization.rb2
-rw-r--r--config/initializers/error_constants.rb5
-rw-r--r--config/initializers/simple_form_bootstrap.rb12
-rw-r--r--config/initializers/validations.rb4
-rw-r--r--config/locales/en/generic.en.yml2
-rw-r--r--config/routes.rb16
-rw-r--r--doc/DEPLOY.md9
-rw-r--r--doc/DEVELOP.md41
-rw-r--r--doc/TROUBLESHOOT.md14
-rw-r--r--engines/billing/config/routes.rb33
-rw-r--r--engines/support/app/helpers/auto_tickets_path_helper.rb15
-rw-r--r--engines/support/app/models/account_extension/tickets.rb13
-rw-r--r--engines/support/app/models/ticket.rb6
-rw-r--r--engines/support/app/views/tickets/_edit_form.html.haml8
-rw-r--r--engines/support/app/views/tickets/edit.html.haml1
-rw-r--r--engines/support/app/views/tickets/index.html.haml2
-rw-r--r--engines/support/app/views/tickets/new.html.haml5
-rw-r--r--engines/support/app/views/tickets/show.html.haml2
-rw-r--r--engines/support/config/initializers/account_lifecycle.rb2
-rw-r--r--engines/support/config/locales/en.yml2
-rw-r--r--engines/support/config/routes.rb4
-rw-r--r--engines/support/lib/account_extension/tickets.rb15
-rw-r--r--engines/support/test/functional/tickets_controller_test.rb6
-rw-r--r--engines/support/test/integration/create_ticket_test.rb2
-rw-r--r--engines/support/test/unit/ticket_test.rb6
-rw-r--r--features/1/anonymous.feature34
-rw-r--r--features/1/authentication.feature24
-rw-r--r--features/1/config.feature58
-rw-r--r--features/1/service.feature33
-rw-r--r--features/1/unauthenticated.feature31
-rw-r--r--features/anonymous.feature8
-rw-r--r--features/authentication.feature4
-rw-r--r--features/config.feature16
-rw-r--r--features/service.feature4
-rw-r--r--features/step_definitions/config_steps.rb9
-rw-r--r--features/support/hooks.rb4
-rw-r--r--features/unauthenticated.feature10
-rw-r--r--lib/email.rb (renamed from app/models/email.rb)0
-rw-r--r--lib/extensions/couchrest.rb4
-rw-r--r--lib/gemfile_tools.rb4
-rw-r--r--lib/local_email.rb (renamed from app/models/local_email.rb)1
-rw-r--r--lib/login_format_validation.rb (renamed from app/models/login_format_validation.rb)0
-rw-r--r--lib/tasks/i18n.rake2
-rw-r--r--lib/tasks/test.rake15
-rw-r--r--test/functional/account_controller_test.rb26
-rw-r--r--test/functional/api/certs_controller_test.rb (renamed from test/functional/v1/certs_controller_test.rb)16
-rw-r--r--test/functional/api/identities_controller_test.rb (renamed from test/functional/v1/identities_controller_test.rb)8
-rw-r--r--test/functional/api/messages_controller_test.rb (renamed from test/functional/v1/messages_controller_test.rb)15
-rw-r--r--test/functional/api/services_controller_test.rb (renamed from test/functional/v1/services_controller_test.rb)8
-rw-r--r--test/functional/api/sessions_controller_test.rb (renamed from test/functional/v1/sessions_controller_test.rb)12
-rw-r--r--test/functional/api/smtp_certs_controller_test.rb (renamed from test/functional/v1/smtp_certs_controller_test.rb)10
-rw-r--r--test/functional/api/token_auth_test.rb (renamed from test/functional/token_auth_test.rb)12
-rw-r--r--test/functional/api/users_controller_test.rb (renamed from test/functional/v1/users_controller_test.rb)40
-rw-r--r--test/functional/static_config_controller_test.rb6
-rw-r--r--test/functional/users_controller_test.rb24
-rw-r--r--test/integration/api/cert_test.rb11
-rw-r--r--test/integration/api/signup_test.rb2
-rw-r--r--test/integration/api/smtp_cert_test.rb14
-rw-r--r--test/integration/api/srp_test.rb18
-rw-r--r--test/integration/api/token_auth_test.rb (renamed from test/integration/api/token_test.rb)4
-rw-r--r--test/integration/api/update_account_test.rb2
-rw-r--r--test/integration/browser/account_livecycle_test.rb114
-rw-r--r--test/integration/browser/account_livecycle_test.rb.orig (renamed from test/integration/browser/account_test.rb)33
-rw-r--r--test/integration/browser/admin_test.rb18
-rw-r--r--test/integration/browser/password_validation_test.rb8
-rw-r--r--test/integration/browser/security_test.rb52
-rw-r--r--test/integration/locale_path_test.rb7
-rw-r--r--test/integration/navigation_test.rb9
-rw-r--r--test/leap_web_users_test.rb7
-rw-r--r--test/performance/browsing_test.rb12
-rw-r--r--test/support/api_controller_test.rb29
-rw-r--r--test/support/api_integration_test.rb4
-rw-r--r--test/support/assert_responses.rb13
-rw-r--r--test/support/browser_integration_test.rb40
-rw-r--r--test/support/record_assertions.rb10
-rw-r--r--test/test_helper.rb6
-rw-r--r--test/unit/email_test.rb1
-rw-r--r--test/unit/identity_test.rb9
-rw-r--r--test/unit/invite_code_validator_test.rb14
-rw-r--r--test/unit/temporary_user_test.rb (renamed from test/unit/tmp_user_test.rb)2
141 files changed, 3795 insertions, 848 deletions
diff --git a/.gitignore b/.gitignore
index 3aa8fc7..8d49deb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,13 @@
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
# Ignore bundler config
/.bundle
-bin
# Ignore the default SQLite database.
/db/*.sqlite3
+/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*.log
@@ -12,7 +16,6 @@ bin
/pkg
/*/pkg
/log
-.*.swp
*/Gemfile.lock
test/dummy/log/*
test/dummy/tmp/*
diff --git a/Gemfile b/Gemfile
index a38d1e9..9689a94 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,12 +4,14 @@ require File.expand_path('../lib/gemfile_tools.rb', __FILE__)
## CORE
# rake 11.x throws lots of warnings about rails 3.2 code
-gem "rake", "~> 10.4"
-gem "rails", "~> 3.2.21"
-gem "couchrest", "~> 1.1.3"
-gem "couchrest_model", "~> 2.0.0"
+gem "rake"
+gem "rails", "~> 4.2.6"
+# TODO: drop this and the respond_with usage
+gem 'responders', '~> 2.0'
+gem "couchrest", "~> 2.0.0.rc3"
+gem "couchrest_model", "~> 2.1.0.beta2"
if ARGV.grep(/assets:precompile/).empty?
- gem "couchrest_session_store", "= 0.3.1"
+ gem "couchrest_session_store", "~> 0.4.2"
end
## AUTHENTICATION
@@ -30,8 +32,7 @@ gem 'rails-i18n' # locale files for built-in validation messages and times
gem 'common_languages', :path => 'vendor/gems/common_languages'
## VIEWS
-gem 'kaminari', "0.13.0" # for pagination. trying 0.13.0 as there seem to be
- # issues with 0.14.0 when using couchrest
+gem 'kaminari'
gem 'rdiscount' # for rendering .md templates
## ASSETS
@@ -39,19 +40,12 @@ gem "jquery-rails"
gem "simple_form"
gem 'client_side_validations'
gem 'client_side_validations-simple_form'
-gem "haml-rails", "= 0.4.0" # The last version of haml-rails to support Rails 3.
-gem "bootstrap-sass", "= 2.3.2.2" # The last 2.x version. Bootstrap-sass versions
- # tracks the version of Bootstrap. We currently require
- # Bootstrap v2 because client side validations is incompatible
- # with Bootstrap v3. When upgrading to Rails 4, see
- # https://github.com/twbs/bootstrap-sass
-gem "sass-rails", "~> 3.2.5" # Only version supported by bootstrap-sass 2.3.2.2
-gem 'quiet_assets' # stops logging all the asset requests
+gem "haml-rails"
+gem "bootstrap-sass"
+gem "sass-rails"
group :production do
- gem "uglifier", "~> 1.2.7" # javascript compression https://github.com/lautis/uglifier
- # this must not be included in development mode, or js
- # will get included twice.
- gem 'therubyracer', "~> 0.12.2", :platforms => :ruby
+ gem "uglifier"
+ gem 'therubyracer', :platforms => :ruby
# ^^ See https://github.com/sstephenson/execjs#readme
# for list of supported runtimes.
end
@@ -68,7 +62,7 @@ group :test do
gem 'phantomjs-binaries' # binaries specific to the os
# moching and stubbing
- gem 'mocha', '~> 0.13.0', :require => false
+ gem 'mocha', :require => false
gem 'minitest-stub-const' # why?
# generating test data
@@ -83,7 +77,6 @@ group :test do
end
group :test, :development do
- gem 'thin'
gem 'i18n-missing_translations'
gem 'pry'
end
@@ -93,17 +86,13 @@ group :production do
end
group :development do
- gem "better_errors", '1.1.0'
+ gem "better_errors"
gem "binding_of_caller"
end
group :test, :debug do
# bundler on jessie doesn't support `:platforms => :ruby_21`
- if RUBY_VERSION < "2.0"
- gem 'debugger'
- else
- gem 'byebug'
- end
+ gem 'byebug'
end
##
diff --git a/Gemfile.lock b/Gemfile.lock
index af414c1..ff69a08 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,13 +1,13 @@
PATH
remote: engines/billing
specs:
- leap_web_billing (0.6.0)
+ leap_web_billing (0.8.0)
braintree
PATH
remote: engines/support
specs:
- leap_web_help (0.6.0)
+ leap_web_help (0.8.0)
PATH
remote: vendor/gems/certificate_authority
@@ -26,236 +26,269 @@ GEM
remote: https://rubygems.org/
specs:
SyslogLogger (2.0)
- actionmailer (3.2.22.2)
- actionpack (= 3.2.22.2)
- mail (~> 2.5.4)
- actionpack (3.2.22.2)
- activemodel (= 3.2.22.2)
- activesupport (= 3.2.22.2)
- builder (~> 3.0.0)
+ actionmailer (4.2.6)
+ actionpack (= 4.2.6)
+ actionview (= 4.2.6)
+ activejob (= 4.2.6)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.6)
+ actionview (= 4.2.6)
+ activesupport (= 4.2.6)
+ rack (~> 1.6)
+ rack-test (~> 0.6.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (4.2.6)
+ activesupport (= 4.2.6)
+ builder (~> 3.1)
erubis (~> 2.7.0)
- journey (~> 1.0.4)
- rack (~> 1.4.5)
- rack-cache (~> 1.2)
- rack-test (~> 0.6.1)
- sprockets (~> 2.2.1)
- activemodel (3.2.22.2)
- activesupport (= 3.2.22.2)
- builder (~> 3.0.0)
- activerecord (3.2.22.2)
- activemodel (= 3.2.22.2)
- activesupport (= 3.2.22.2)
- arel (~> 3.0.2)
- tzinfo (~> 0.3.29)
- activeresource (3.2.22.2)
- activemodel (= 3.2.22.2)
- activesupport (= 3.2.22.2)
- activesupport (3.2.22.2)
- i18n (~> 0.6, >= 0.6.4)
- multi_json (~> 1.0)
- addressable (2.3.6)
- arel (3.0.3)
- better_errors (1.1.0)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ activejob (4.2.6)
+ activesupport (= 4.2.6)
+ globalid (>= 0.3.0)
+ activemodel (4.2.6)
+ activesupport (= 4.2.6)
+ builder (~> 3.1)
+ activerecord (4.2.6)
+ activemodel (= 4.2.6)
+ activesupport (= 4.2.6)
+ arel (~> 6.0)
+ activesupport (4.2.6)
+ i18n (~> 0.7)
+ json (~> 1.7, >= 1.7.7)
+ minitest (~> 5.1)
+ thread_safe (~> 0.3, >= 0.3.4)
+ tzinfo (~> 1.1)
+ addressable (2.4.0)
+ arel (6.0.3)
+ autoprefixer-rails (6.3.6)
+ execjs
+ better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (2.3.2.2)
- sass (~> 3.2)
- braintree (2.48.1)
+ bootstrap-sass (3.3.6)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
+ braintree (2.60.0)
builder (>= 2.0.0)
- builder (3.0.4)
- byebug (8.2.1)
- capybara (2.4.4)
+ builder (3.2.2)
+ byebug (8.2.5)
+ capybara (2.7.1)
+ addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- client_side_validations (3.2.6)
- client_side_validations-simple_form (2.1.0)
- client_side_validations (~> 3.2.5)
- simple_form (~> 2.1.0)
+ client_side_validations (4.2.3)
+ jquery-rails (>= 3.1.2, < 5.0.0)
+ js_regex (~> 1.0, >= 1.0.9)
+ rails (>= 4.0.0, < 4.3.0)
+ client_side_validations-simple_form (3.2.3)
+ client_side_validations (~> 4.2.3)
+ simple_form (~> 3.2)
cliver (0.3.2)
- coderay (1.1.0)
- couchrest (1.1.3)
- mime-types (~> 1.15)
- multi_json (~> 1.0)
- rest-client (~> 1.6.1)
- couchrest_model (2.0.1)
- activemodel (>= 3.0)
- couchrest (~> 1.1.3)
+ coderay (1.1.1)
+ concurrent-ruby (1.0.1)
+ couchrest (2.0.0.rc3)
+ httpclient (~> 2.7)
mime-types (>= 1.15)
+ multi_json (~> 1.7)
+ couchrest_model (2.1.0.beta2)
+ activemodel (~> 4.0)
+ couchrest (= 2.0.0.rc3)
tzinfo (>= 0.3.22)
- couchrest_session_store (0.3.1)
- actionpack (~> 3.0)
- couchrest
- couchrest_model
- cucumber (1.3.17)
+ couchrest_session_store (0.4.2)
+ actionpack (~> 4.0)
+ couchrest (~> 2.0.0.rc3)
+ couchrest_model (~> 2.1.0.beta2)
+ cucumber (2.3.3)
builder (>= 2.1.2)
+ cucumber-core (~> 1.4.0)
+ cucumber-wire (~> 0.0.1)
diff-lcs (>= 1.1.3)
- gherkin (~> 2.12)
+ gherkin (~> 3.2.0)
multi_json (>= 1.7.5, < 2.0)
- multi_test (>= 0.1.1)
- cucumber-rails (1.4.2)
+ multi_test (>= 0.1.2)
+ cucumber-core (1.4.0)
+ gherkin (~> 3.2.0)
+ cucumber-rails (1.4.3)
capybara (>= 1.1.2, < 3)
- cucumber (>= 1.3.8, < 2)
- mime-types (>= 1.16, < 3)
+ cucumber (>= 1.3.8, < 3)
+ mime-types (>= 1.16, < 4)
nokogiri (~> 1.5)
- rails (>= 3, < 5)
- daemons (1.1.9)
+ railties (>= 3, < 5)
+ cucumber-wire (0.0.1)
debug_inspector (0.0.2)
diff-lcs (1.2.5)
erubis (2.7.0)
- eventmachine (1.0.3)
- execjs (2.2.2)
- factory_girl (4.5.0)
+ execjs (2.6.0)
+ factory_girl (4.7.0)
activesupport (>= 3.0.0)
- factory_girl_rails (4.5.0)
- factory_girl (~> 4.5.0)
+ factory_girl_rails (4.7.0)
+ factory_girl (~> 4.7.0)
railties (>= 3.0.0)
- fake_braintree (0.7.0)
+ fake_braintree (0.8.0)
activesupport
braintree (~> 2.32)
- capybara (>= 2.0.3)
+ capybara (>= 2.2.0)
sinatra
- faker (1.4.3)
+ faker (1.6.3)
i18n (~> 0.5)
- ffi (1.9.6)
- gherkin (2.12.2)
- multi_json (~> 1.3)
- haml (4.0.6)
+ ffi (1.9.10)
+ gherkin (3.2.0)
+ globalid (0.3.6)
+ activesupport (>= 4.1.0)
+ haml (4.0.7)
tilt
- haml-rails (0.4)
- actionpack (>= 3.1, < 4.1)
- activesupport (>= 3.1, < 4.1)
- haml (>= 3.1, < 4.1)
- railties (>= 3.1, < 4.1)
- hike (1.2.3)
- http_accept_language (2.0.2)
+ haml-rails (0.9.0)
+ actionpack (>= 4.0.1)
+ activesupport (>= 4.0.1)
+ haml (>= 4.0.6, < 5.0)
+ html2haml (>= 1.0.1)
+ railties (>= 4.0.1)
+ html2haml (2.0.0)
+ erubis (~> 2.7.0)
+ haml (~> 4.0.0)
+ nokogiri (~> 1.6.0)
+ ruby_parser (~> 3.5)
+ http_accept_language (2.0.5)
+ httpclient (2.8.0)
i18n (0.7.0)
i18n-missing_translations (0.0.1)
- journey (1.0.4)
- jquery-rails (3.1.2)
- railties (>= 3.0, < 5.0)
+ jquery-rails (4.1.1)
+ rails-dom-testing (>= 1, < 3)
+ railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
+ js_regex (1.0.14)
+ regexp_parser (= 0.3.3)
json (1.8.3)
- kaminari (0.13.0)
+ kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- railties (>= 3.0.0)
launchy (2.4.3)
addressable (~> 2.3)
- libv8 (3.16.14.11)
- mail (2.5.4)
- mime-types (~> 1.16)
- treetop (~> 1.4.8)
+ libv8 (3.16.14.15)
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
+ mail (2.6.4)
+ mime-types (>= 1.16, < 4)
metaclass (0.0.4)
method_source (0.8.2)
- mime-types (1.25.1)
- mini_portile (0.6.1)
- minitest-stub-const (0.2)
- mocha (0.13.3)
+ mime-types (3.0)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0221)
+ mini_portile2 (2.0.0)
+ minitest (5.8.4)
+ minitest-stub-const (0.5)
+ mocha (1.1.0)
metaclass (~> 0.0.1)
- multi_json (1.11.2)
- multi_test (0.1.1)
- nokogiri (1.6.5)
- mini_portile (~> 0.6.0)
- phantomjs-binaries (1.9.2.4)
+ multi_json (1.11.3)
+ multi_test (0.1.2)
+ nokogiri (1.6.7.2)
+ mini_portile2 (~> 2.0.0.rc2)
+ phantomjs-binaries (2.1.1.0)
sys-uname (= 0.9.0)
- poltergeist (1.5.1)
+ poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
- polyglot (0.3.5)
- pry (0.10.1)
+ pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
- quiet_assets (1.0.3)
- railties (>= 3.1, < 5.0)
- rack (1.4.7)
- rack-cache (1.6.1)
- rack (>= 0.4)
+ rack (1.6.4)
rack-protection (1.5.3)
rack
- rack-ssl (1.3.4)
- rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (3.2.22.2)
- actionmailer (= 3.2.22.2)
- actionpack (= 3.2.22.2)
- activerecord (= 3.2.22.2)
- activeresource (= 3.2.22.2)
- activesupport (= 3.2.22.2)
- bundler (~> 1.0)
- railties (= 3.2.22.2)
- rails-i18n (3.0.1)
- i18n (~> 0.5)
- rails (>= 3.0.0, < 4.0.0)
+ rails (4.2.6)
+ actionmailer (= 4.2.6)
+ actionpack (= 4.2.6)
+ actionview (= 4.2.6)
+ activejob (= 4.2.6)
+ activemodel (= 4.2.6)
+ activerecord (= 4.2.6)
+ activesupport (= 4.2.6)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.2.6)
+ sprockets-rails
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (1.0.7)
+ activesupport (>= 4.2.0.beta, < 5.0)
+ nokogiri (~> 1.6.0)
+ rails-deprecated_sanitizer (>= 1.0.1)
+ rails-html-sanitizer (1.0.3)
+ loofah (~> 2.0)
+ rails-i18n (4.0.8)
+ i18n (~> 0.7)
+ railties (~> 4.0)
rails_warden (0.5.8)
warden (>= 1.0.0)
- railties (3.2.22.2)
- actionpack (= 3.2.22.2)
- activesupport (= 3.2.22.2)
- rack-ssl (~> 1.3.2)
+ railties (4.2.6)
+ actionpack (= 4.2.6)
+ activesupport (= 4.2.6)
rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (>= 0.14.6, < 2.0)
- rake (10.5.0)
- rdiscount (2.1.7.1)
- rdoc (3.12.2)
- json (~> 1.4)
+ thor (>= 0.18.1, < 2.0)
+ rake (11.1.2)
+ rdiscount (2.1.8)
ref (2.0.0)
- rest-client (1.6.9)
- mime-types (~> 1.16)
+ regexp_parser (0.3.3)
+ responders (2.1.2)
+ railties (>= 4.2.0, < 5.1)
ruby-srp (0.2.1)
- sass (3.4.9)
- sass-rails (3.2.6)
- railties (~> 3.2.0)
- sass (>= 3.1.10)
- tilt (~> 1.3)
- simple_form (2.1.2)
- actionpack (~> 3.0)
- activemodel (~> 3.0)
- sinatra (1.4.5)
- rack (~> 1.4)
+ ruby_parser (3.8.1)
+ sexp_processor (~> 4.1)
+ sass (3.4.22)
+ sass-rails (5.0.4)
+ railties (>= 4.0.0, < 5.0)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sexp_processor (4.7.0)
+ simple_form (3.2.1)
+ actionpack (> 4, < 5.1)
+ activemodel (> 4, < 5.1)
+ sinatra (1.4.7)
+ rack (~> 1.5)
rack-protection (~> 1.4)
- tilt (~> 1.3, >= 1.3.4)
+ tilt (>= 1.3, < 3)
slop (3.6.0)
- sprockets (2.2.3)
- hike (~> 1.2)
- multi_json (~> 1.0)
- rack (~> 1.0)
- tilt (~> 1.1, != 1.3.0)
+ sprockets (3.6.0)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.0.4)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
sys-uname (0.9.0)
ffi (>= 1.0.0)
therubyracer (0.12.2)
libv8 (~> 3.16.14.0)
ref
- thin (1.6.3)
- daemons (~> 1.0, >= 1.0.9)
- eventmachine (~> 1.0)
- rack (~> 1.0)
thor (0.19.1)
- tilt (1.4.1)
- treetop (1.4.15)
- polyglot
- polyglot (>= 0.3.1)
- tzinfo (0.3.47)
- uglifier (1.2.7)
- execjs (>= 0.3.0)
- multi_json (~> 1.3)
- valid_email (0.0.7)
+ thread_safe (0.3.5)
+ tilt (2.0.2)
+ tzinfo (1.2.2)
+ thread_safe (~> 0.1)
+ uglifier (3.0.0)
+ execjs (>= 0.3.0, < 3)
+ valid_email (0.0.13)
activemodel
- mail
- warden (1.2.3)
+ mail (~> 2.6.1)
+ warden (1.2.6)
rack (>= 1.0)
- websocket-driver (0.5.1)
+ websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.1)
+ websocket-extensions (0.1.2)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -264,47 +297,46 @@ PLATFORMS
DEPENDENCIES
SyslogLogger (~> 2.0)
- better_errors (= 1.1.0)
+ better_errors
binding_of_caller
- bootstrap-sass (= 2.3.2.2)
+ bootstrap-sass
byebug
capybara
certificate_authority!
client_side_validations
client_side_validations-simple_form
common_languages!
- couchrest (~> 1.1.3)
- couchrest_model (~> 2.0.0)
- couchrest_session_store (= 0.3.1)
+ couchrest (~> 2.0.0.rc3)
+ couchrest_model (~> 2.1.0.beta2)
+ couchrest_session_store (~> 0.4.2)
cucumber-rails
factory_girl_rails
fake_braintree
faker
- haml-rails (= 0.4.0)
+ haml-rails
http_accept_language
i18n-missing_translations
jquery-rails
- kaminari (= 0.13.0)
+ kaminari
launchy
leap_web_billing!
leap_web_help!
minitest-stub-const
- mocha (~> 0.13.0)
+ mocha
phantomjs-binaries
poltergeist
pry
- quiet_assets
- rails (~> 3.2.21)
+ rails (~> 4.2.6)
rails-i18n
rails_warden
- rake (~> 10.4)
+ rake
rdiscount
+ responders (~> 2.0)
ruby-srp (~> 0.2.1)
- sass-rails (~> 3.2.5)
+ sass-rails
simple_form
- therubyracer (~> 0.12.2)
- thin
- uglifier (~> 1.2.7)
+ therubyracer
+ uglifier
valid_email
BUNDLED WITH
diff --git a/README.md b/README.md
index b6f3d1b..e68c117 100644
--- a/README.md
+++ b/README.md
@@ -46,20 +46,6 @@ External docs:
* Overview of the main code repositories
* Ideas for discrete, unclaimed development projects that would greatly benefit the LEAP ecosystem.
-Known problems
----------------------------
-
-* Client certificates are generated without a CSR. The problem is that
- this makes the web application extremely vulnerable to denial of
- service attacks. This is not an issue unless the provider enables the
- possibility of anonymously fetching a client certificate without
- authenticating first.
-
-* By its very nature, the user database is vulnerable to enumeration
- attacks. These are very hard to prevent, because our protocol is
- designed to allow query of a user database via proxy in order to
- provide network perspective.
-
Installation
---------------------------
@@ -69,17 +55,37 @@ these instructions:
### Install system requirements
+You'll need git, ruby (2.1.5), couchdb and bundler installed.
+On a recent debian based distribution run
+
sudo apt install git ruby couchdb bundler
-Your actual requirements might differ if you are running an older OS that defaults to ruby 1.9.
+For other operation systems please lookup the install instructions of these
+tools.
### Download source
+We host our own git repository. In order to create a local clone run
+
git clone --recursive git://leap.se/leap_web
+ cd leap_web
+
+The repo is mirrored on github and we accept pull requests there:
+
+ https://github.com/leapcode/leap_web
+
+### Pick branch (development only)
+
+We use the master branch for the stable version deployed to production.
+Development usually happens on the develop branch. So for development you
+want to run
+
+ git checkout origin/develop -b develop
+
+This will create a local branch called develop based on our develop branch.
### Install required ruby libraries
- cd leap_web
bundle --binstubs
Typically, you run ``bundle`` as a normal user and it will ask you for a
@@ -88,13 +94,13 @@ have sudo, run ``bundle`` as root.
### Installation for development purposes
-Please see `doc/DEVELOP.md` for further required steps when installing
+Please see `doc/DEVELOP.md` for details about installing
leap_web for development purposes.
-Configuration
+Configuration for Production
----------------------------
-The configuration file `config/defaults.yml` providers good defaults for
+The configuration file `config/defaults.yml` provides good defaults for
most values. You can override these defaults by creating a file
`config/config.yml`.
@@ -167,3 +173,17 @@ To run an individual test:
or
ruby -Itest certs/test/unit/client_certificate_test.rb
+Known problems
+---------------------------
+
+* Client certificates are generated without a CSR. The problem is that
+ this makes the web application extremely vulnerable to denial of
+ service attacks. This is not an issue unless the provider enables the
+ possibility of anonymously fetching a client certificate without
+ authenticating first.
+
+* By its very nature, the user database is vulnerable to enumeration
+ attacks. These are very hard to prevent, because our protocol is
+ designed to allow query of a user database via proxy in order to
+ provide network perspective.
+
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 9af373d..7888161 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -16,6 +16,7 @@
//= require bootstrap
//= require rails.validations
//= require rails.validations.simple_form
+//= require typeahead.bundle
//= require leap
//= require platform
//= require tickets
diff --git a/app/assets/javascripts/leap.js b/app/assets/javascripts/leap.js
index 94e602d..c8fbcf5 100644
--- a/app/assets/javascripts/leap.js
+++ b/app/assets/javascripts/leap.js
@@ -5,3 +5,5 @@
function alert_message(msg) {
$('#messages').append('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><span>'+msg+'</span></div>');
}
+
+ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers.bootstrap = ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers["default"];
diff --git a/app/assets/javascripts/typeahead.bundle.js b/app/assets/javascripts/typeahead.bundle.js
new file mode 100644
index 0000000..bb0c8ae
--- /dev/null
+++ b/app/assets/javascripts/typeahead.bundle.js
@@ -0,0 +1,2451 @@
+/*!
+ * typeahead.js 0.11.1
+ * https://github.com/twitter/typeahead.js
+ * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
+ */
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("bloodhound", [ "jquery" ], function(a0) {
+ return root["Bloodhound"] = factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ root["Bloodhound"] = factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var VERSION = "0.11.1";
+ var tokenizers = function() {
+ "use strict";
+ return {
+ nonword: nonword,
+ whitespace: whitespace,
+ obj: {
+ nonword: getObjTokenizer(nonword),
+ whitespace: getObjTokenizer(whitespace)
+ }
+ };
+ function whitespace(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\s+/) : [];
+ }
+ function nonword(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\W+/) : [];
+ }
+ function getObjTokenizer(tokenizer) {
+ return function setKey(keys) {
+ keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
+ return function tokenize(o) {
+ var tokens = [];
+ _.each(keys, function(k) {
+ tokens = tokens.concat(tokenizer(_.toStr(o[k])));
+ });
+ return tokens;
+ };
+ };
+ }
+ }();
+ var LruCache = function() {
+ "use strict";
+ function LruCache(maxSize) {
+ this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
+ this.reset();
+ if (this.maxSize <= 0) {
+ this.set = this.get = $.noop;
+ }
+ }
+ _.mixin(LruCache.prototype, {
+ set: function set(key, val) {
+ var tailItem = this.list.tail, node;
+ if (this.size >= this.maxSize) {
+ this.list.remove(tailItem);
+ delete this.hash[tailItem.key];
+ this.size--;
+ }
+ if (node = this.hash[key]) {
+ node.val = val;
+ this.list.moveToFront(node);
+ } else {
+ node = new Node(key, val);
+ this.list.add(node);
+ this.hash[key] = node;
+ this.size++;
+ }
+ },
+ get: function get(key) {
+ var node = this.hash[key];
+ if (node) {
+ this.list.moveToFront(node);
+ return node.val;
+ }
+ },
+ reset: function reset() {
+ this.size = 0;
+ this.hash = {};
+ this.list = new List();
+ }
+ });
+ function List() {
+ this.head = this.tail = null;
+ }
+ _.mixin(List.prototype, {
+ add: function add(node) {
+ if (this.head) {
+ node.next = this.head;
+ this.head.prev = node;
+ }
+ this.head = node;
+ this.tail = this.tail || node;
+ },
+ remove: function remove(node) {
+ node.prev ? node.prev.next = node.next : this.head = node.next;
+ node.next ? node.next.prev = node.prev : this.tail = node.prev;
+ },
+ moveToFront: function(node) {
+ this.remove(node);
+ this.add(node);
+ }
+ });
+ function Node(key, val) {
+ this.key = key;
+ this.val = val;
+ this.prev = this.next = null;
+ }
+ return LruCache;
+ }();
+ var PersistentStorage = function() {
+ "use strict";
+ var LOCAL_STORAGE;
+ try {
+ LOCAL_STORAGE = window.localStorage;
+ LOCAL_STORAGE.setItem("~~~", "!");
+ LOCAL_STORAGE.removeItem("~~~");
+ } catch (err) {
+ LOCAL_STORAGE = null;
+ }
+ function PersistentStorage(namespace, override) {
+ this.prefix = [ "__", namespace, "__" ].join("");
+ this.ttlKey = "__ttl__";
+ this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
+ this.ls = override || LOCAL_STORAGE;
+ !this.ls && this._noop();
+ }
+ _.mixin(PersistentStorage.prototype, {
+ _prefix: function(key) {
+ return this.prefix + key;
+ },
+ _ttlKey: function(key) {
+ return this._prefix(key) + this.ttlKey;
+ },
+ _noop: function() {
+ this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
+ },
+ _safeSet: function(key, val) {
+ try {
+ this.ls.setItem(key, val);
+ } catch (err) {
+ if (err.name === "QuotaExceededError") {
+ this.clear();
+ this._noop();
+ }
+ }
+ },
+ get: function(key) {
+ if (this.isExpired(key)) {
+ this.remove(key);
+ }
+ return decode(this.ls.getItem(this._prefix(key)));
+ },
+ set: function(key, val, ttl) {
+ if (_.isNumber(ttl)) {
+ this._safeSet(this._ttlKey(key), encode(now() + ttl));
+ } else {
+ this.ls.removeItem(this._ttlKey(key));
+ }
+ return this._safeSet(this._prefix(key), encode(val));
+ },
+ remove: function(key) {
+ this.ls.removeItem(this._ttlKey(key));
+ this.ls.removeItem(this._prefix(key));
+ return this;
+ },
+ clear: function() {
+ var i, keys = gatherMatchingKeys(this.keyMatcher);
+ for (i = keys.length; i--; ) {
+ this.remove(keys[i]);
+ }
+ return this;
+ },
+ isExpired: function(key) {
+ var ttl = decode(this.ls.getItem(this._ttlKey(key)));
+ return _.isNumber(ttl) && now() > ttl ? true : false;
+ }
+ });
+ return PersistentStorage;
+ function now() {
+ return new Date().getTime();
+ }
+ function encode(val) {
+ return JSON.stringify(_.isUndefined(val) ? null : val);
+ }
+ function decode(val) {
+ return $.parseJSON(val);
+ }
+ function gatherMatchingKeys(keyMatcher) {
+ var i, key, keys = [], len = LOCAL_STORAGE.length;
+ for (i = 0; i < len; i++) {
+ if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
+ keys.push(key.replace(keyMatcher, ""));
+ }
+ }
+ return keys;
+ }
+ }();
+ var Transport = function() {
+ "use strict";
+ var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
+ function Transport(o) {
+ o = o || {};
+ this.cancelled = false;
+ this.lastReq = null;
+ this._send = o.transport;
+ this._get = o.limiter ? o.limiter(this._get) : this._get;
+ this._cache = o.cache === false ? new LruCache(0) : sharedCache;
+ }
+ Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
+ maxPendingRequests = num;
+ };
+ Transport.resetCache = function resetCache() {
+ sharedCache.reset();
+ };
+ _.mixin(Transport.prototype, {
+ _fingerprint: function fingerprint(o) {
+ o = o || {};
+ return o.url + o.type + $.param(o.data || {});
+ },
+ _get: function(o, cb) {
+ var that = this, fingerprint, jqXhr;
+ fingerprint = this._fingerprint(o);
+ if (this.cancelled || fingerprint !== this.lastReq) {
+ return;
+ }
+ if (jqXhr = pendingRequests[fingerprint]) {
+ jqXhr.done(done).fail(fail);
+ } else if (pendingRequestsCount < maxPendingRequests) {
+ pendingRequestsCount++;
+ pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
+ } else {
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
+ }
+ function done(resp) {
+ cb(null, resp);
+ that._cache.set(fingerprint, resp);
+ }
+ function fail() {
+ cb(true);
+ }
+ function always() {
+ pendingRequestsCount--;
+ delete pendingRequests[fingerprint];
+ if (that.onDeckRequestArgs) {
+ that._get.apply(that, that.onDeckRequestArgs);
+ that.onDeckRequestArgs = null;
+ }
+ }
+ },
+ get: function(o, cb) {
+ var resp, fingerprint;
+ cb = cb || $.noop;
+ o = _.isString(o) ? {
+ url: o
+ } : o || {};
+ fingerprint = this._fingerprint(o);
+ this.cancelled = false;
+ this.lastReq = fingerprint;
+ if (resp = this._cache.get(fingerprint)) {
+ cb(null, resp);
+ } else {
+ this._get(o, cb);
+ }
+ },
+ cancel: function() {
+ this.cancelled = true;
+ }
+ });
+ return Transport;
+ }();
+ var SearchIndex = window.SearchIndex = function() {
+ "use strict";
+ var CHILDREN = "c", IDS = "i";
+ function SearchIndex(o) {
+ o = o || {};
+ if (!o.datumTokenizer || !o.queryTokenizer) {
+ $.error("datumTokenizer and queryTokenizer are both required");
+ }
+ this.identify = o.identify || _.stringify;
+ this.datumTokenizer = o.datumTokenizer;
+ this.queryTokenizer = o.queryTokenizer;
+ this.reset();
+ }
+ _.mixin(SearchIndex.prototype, {
+ bootstrap: function bootstrap(o) {
+ this.datums = o.datums;
+ this.trie = o.trie;
+ },
+ add: function(data) {
+ var that = this;
+ data = _.isArray(data) ? data : [ data ];
+ _.each(data, function(datum) {
+ var id, tokens;
+ that.datums[id = that.identify(datum)] = datum;
+ tokens = normalizeTokens(that.datumTokenizer(datum));
+ _.each(tokens, function(token) {
+ var node, chars, ch;
+ node = that.trie;
+ chars = token.split("");
+ while (ch = chars.shift()) {
+ node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
+ node[IDS].push(id);
+ }
+ });
+ });
+ },
+ get: function get(ids) {
+ var that = this;
+ return _.map(ids, function(id) {
+ return that.datums[id];
+ });
+ },
+ search: function search(query) {
+ var that = this, tokens, matches;
+ tokens = normalizeTokens(this.queryTokenizer(query));
+ _.each(tokens, function(token) {
+ var node, chars, ch, ids;
+ if (matches && matches.length === 0) {
+ return false;
+ }
+ node = that.trie;
+ chars = token.split("");
+ while (node && (ch = chars.shift())) {
+ node = node[CHILDREN][ch];
+ }
+ if (node && chars.length === 0) {
+ ids = node[IDS].slice(0);
+ matches = matches ? getIntersection(matches, ids) : ids;
+ } else {
+ matches = [];
+ return false;
+ }
+ });
+ return matches ? _.map(unique(matches), function(id) {
+ return that.datums[id];
+ }) : [];
+ },
+ all: function all() {
+ var values = [];
+ for (var key in this.datums) {
+ values.push(this.datums[key]);
+ }
+ return values;
+ },
+ reset: function reset() {
+ this.datums = {};
+ this.trie = newNode();
+ },
+ serialize: function serialize() {
+ return {
+ datums: this.datums,
+ trie: this.trie
+ };
+ }
+ });
+ return SearchIndex;
+ function normalizeTokens(tokens) {
+ tokens = _.filter(tokens, function(token) {
+ return !!token;
+ });
+ tokens = _.map(tokens, function(token) {
+ return token.toLowerCase();
+ });
+ return tokens;
+ }
+ function newNode() {
+ var node = {};
+ node[IDS] = [];
+ node[CHILDREN] = {};
+ return node;
+ }
+ function unique(array) {
+ var seen = {}, uniques = [];
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (!seen[array[i]]) {
+ seen[array[i]] = true;
+ uniques.push(array[i]);
+ }
+ }
+ return uniques;
+ }
+ function getIntersection(arrayA, arrayB) {
+ var ai = 0, bi = 0, intersection = [];
+ arrayA = arrayA.sort();
+ arrayB = arrayB.sort();
+ var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
+ while (ai < lenArrayA && bi < lenArrayB) {
+ if (arrayA[ai] < arrayB[bi]) {
+ ai++;
+ } else if (arrayA[ai] > arrayB[bi]) {
+ bi++;
+ } else {
+ intersection.push(arrayA[ai]);
+ ai++;
+ bi++;
+ }
+ }
+ return intersection;
+ }
+ }();
+ var Prefetch = function() {
+ "use strict";
+ var keys;
+ keys = {
+ data: "data",
+ protocol: "protocol",
+ thumbprint: "thumbprint"
+ };
+ function Prefetch(o) {
+ this.url = o.url;
+ this.ttl = o.ttl;
+ this.cache = o.cache;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = o.transport;
+ this.thumbprint = o.thumbprint;
+ this.storage = new PersistentStorage(o.cacheKey);
+ }
+ _.mixin(Prefetch.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ store: function store(data) {
+ if (!this.cache) {
+ return;
+ }
+ this.storage.set(keys.data, data, this.ttl);
+ this.storage.set(keys.protocol, location.protocol, this.ttl);
+ this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
+ },
+ fromCache: function fromCache() {
+ var stored = {}, isExpired;
+ if (!this.cache) {
+ return null;
+ }
+ stored.data = this.storage.get(keys.data);
+ stored.protocol = this.storage.get(keys.protocol);
+ stored.thumbprint = this.storage.get(keys.thumbprint);
+ isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
+ return stored.data && !isExpired ? stored.data : null;
+ },
+ fromNetwork: function(cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ settings = this.prepare(this._settings());
+ this.transport(settings).fail(onError).done(onResponse);
+ function onError() {
+ cb(true);
+ }
+ function onResponse(resp) {
+ cb(null, that.transform(resp));
+ }
+ },
+ clear: function clear() {
+ this.storage.clear();
+ return this;
+ }
+ });
+ return Prefetch;
+ }();
+ var Remote = function() {
+ "use strict";
+ function Remote(o) {
+ this.url = o.url;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = new Transport({
+ cache: o.cache,
+ limiter: o.limiter,
+ transport: o.transport
+ });
+ }
+ _.mixin(Remote.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ get: function get(query, cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ query = query || "";
+ settings = this.prepare(query, this._settings());
+ return this.transport.get(settings, onResponse);
+ function onResponse(err, resp) {
+ err ? cb([]) : cb(that.transform(resp));
+ }
+ },
+ cancelLastRequest: function cancelLastRequest() {
+ this.transport.cancel();
+ }
+ });
+ return Remote;
+ }();
+ var oParser = function() {
+ "use strict";
+ return function parse(o) {
+ var defaults, sorter;
+ defaults = {
+ initialize: true,
+ identify: _.stringify,
+ datumTokenizer: null,
+ queryTokenizer: null,
+ sufficient: 5,
+ sorter: null,
+ local: [],
+ prefetch: null,
+ remote: null
+ };
+ o = _.mixin(defaults, o || {});
+ !o.datumTokenizer && $.error("datumTokenizer is required");
+ !o.queryTokenizer && $.error("queryTokenizer is required");
+ sorter = o.sorter;
+ o.sorter = sorter ? function(x) {
+ return x.sort(sorter);
+ } : _.identity;
+ o.local = _.isFunction(o.local) ? o.local() : o.local;
+ o.prefetch = parsePrefetch(o.prefetch);
+ o.remote = parseRemote(o.remote);
+ return o;
+ };
+ function parsePrefetch(o) {
+ var defaults;
+ if (!o) {
+ return null;
+ }
+ defaults = {
+ url: null,
+ ttl: 24 * 60 * 60 * 1e3,
+ cache: true,
+ cacheKey: null,
+ thumbprint: "",
+ prepare: _.identity,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("prefetch requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.cacheKey = o.cacheKey || o.url;
+ o.thumbprint = VERSION + o.thumbprint;
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ return o;
+ }
+ function parseRemote(o) {
+ var defaults;
+ if (!o) {
+ return;
+ }
+ defaults = {
+ url: null,
+ cache: true,
+ prepare: null,
+ replace: null,
+ wildcard: null,
+ limiter: null,
+ rateLimitBy: "debounce",
+ rateLimitWait: 300,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("remote requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.prepare = toRemotePrepare(o);
+ o.limiter = toLimiter(o);
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ delete o.replace;
+ delete o.wildcard;
+ delete o.rateLimitBy;
+ delete o.rateLimitWait;
+ return o;
+ }
+ function toRemotePrepare(o) {
+ var prepare, replace, wildcard;
+ prepare = o.prepare;
+ replace = o.replace;
+ wildcard = o.wildcard;
+ if (prepare) {
+ return prepare;
+ }
+ if (replace) {
+ prepare = prepareByReplace;
+ } else if (o.wildcard) {
+ prepare = prepareByWildcard;
+ } else {
+ prepare = idenityPrepare;
+ }
+ return prepare;
+ function prepareByReplace(query, settings) {
+ settings.url = replace(settings.url, query);
+ return settings;
+ }
+ function prepareByWildcard(query, settings) {
+ settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
+ return settings;
+ }
+ function idenityPrepare(query, settings) {
+ return settings;
+ }
+ }
+ function toLimiter(o) {
+ var limiter, method, wait;
+ limiter = o.limiter;
+ method = o.rateLimitBy;
+ wait = o.rateLimitWait;
+ if (!limiter) {
+ limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
+ }
+ return limiter;
+ function debounce(wait) {
+ return function debounce(fn) {
+ return _.debounce(fn, wait);
+ };
+ }
+ function throttle(wait) {
+ return function throttle(fn) {
+ return _.throttle(fn, wait);
+ };
+ }
+ }
+ function callbackToDeferred(fn) {
+ return function wrapper(o) {
+ var deferred = $.Deferred();
+ fn(o, onSuccess, onError);
+ return deferred;
+ function onSuccess(resp) {
+ _.defer(function() {
+ deferred.resolve(resp);
+ });
+ }
+ function onError(err) {
+ _.defer(function() {
+ deferred.reject(err);
+ });
+ }
+ };
+ }
+ }();
+ var Bloodhound = function() {
+ "use strict";
+ var old;
+ old = window && window.Bloodhound;
+ function Bloodhound(o) {
+ o = oParser(o);
+ this.sorter = o.sorter;
+ this.identify = o.identify;
+ this.sufficient = o.sufficient;
+ this.local = o.local;
+ this.remote = o.remote ? new Remote(o.remote) : null;
+ this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
+ this.index = new SearchIndex({
+ identify: this.identify,
+ datumTokenizer: o.datumTokenizer,
+ queryTokenizer: o.queryTokenizer
+ });
+ o.initialize !== false && this.initialize();
+ }
+ Bloodhound.noConflict = function noConflict() {
+ window && (window.Bloodhound = old);
+ return Bloodhound;
+ };
+ Bloodhound.tokenizers = tokenizers;
+ _.mixin(Bloodhound.prototype, {
+ __ttAdapter: function ttAdapter() {
+ var that = this;
+ return this.remote ? withAsync : withoutAsync;
+ function withAsync(query, sync, async) {
+ return that.search(query, sync, async);
+ }
+ function withoutAsync(query, sync) {
+ return that.search(query, sync);
+ }
+ },
+ _loadPrefetch: function loadPrefetch() {
+ var that = this, deferred, serialized;
+ deferred = $.Deferred();
+ if (!this.prefetch) {
+ deferred.resolve();
+ } else if (serialized = this.prefetch.fromCache()) {
+ this.index.bootstrap(serialized);
+ deferred.resolve();
+ } else {
+ this.prefetch.fromNetwork(done);
+ }
+ return deferred.promise();
+ function done(err, data) {
+ if (err) {
+ return deferred.reject();
+ }
+ that.add(data);
+ that.prefetch.store(that.index.serialize());
+ deferred.resolve();
+ }
+ },
+ _initialize: function initialize() {
+ var that = this, deferred;
+ this.clear();
+ (this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
+ return this.initPromise;
+ function addLocalToIndex() {
+ that.add(that.local);
+ }
+ },
+ initialize: function initialize(force) {
+ return !this.initPromise || force ? this._initialize() : this.initPromise;
+ },
+ add: function add(data) {
+ this.index.add(data);
+ return this;
+ },
+ get: function get(ids) {
+ ids = _.isArray(ids) ? ids : [].slice.call(arguments);
+ return this.index.get(ids);
+ },
+ search: function search(query, sync, async) {
+ var that = this, local;
+ local = this.sorter(this.index.search(query));
+ sync(this.remote ? local.slice() : local);
+ if (this.remote && local.length < this.sufficient) {
+ this.remote.get(query, processRemote);
+ } else if (this.remote) {
+ this.remote.cancelLastRequest();
+ }
+ return this;
+ function processRemote(remote) {
+ var nonDuplicates = [];
+ _.each(remote, function(r) {
+ !_.some(local, function(l) {
+ return that.identify(r) === that.identify(l);
+ }) && nonDuplicates.push(r);
+ });
+ async && async(nonDuplicates);
+ }
+ },
+ all: function all() {
+ return this.index.all();
+ },
+ clear: function clear() {
+ this.index.reset();
+ return this;
+ },
+ clearPrefetchCache: function clearPrefetchCache() {
+ this.prefetch && this.prefetch.clear();
+ return this;
+ },
+ clearRemoteCache: function clearRemoteCache() {
+ Transport.resetCache();
+ return this;
+ },
+ ttAdapter: function ttAdapter() {
+ return this.__ttAdapter();
+ }
+ });
+ return Bloodhound;
+ }();
+ return Bloodhound;
+});
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("typeahead.js", [ "jquery" ], function(a0) {
+ return factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var WWW = function() {
+ "use strict";
+ var defaultClassNames = {
+ wrapper: "twitter-typeahead",
+ input: "tt-input",
+ hint: "tt-hint",
+ menu: "tt-menu",
+ dataset: "tt-dataset",
+ suggestion: "tt-suggestion",
+ selectable: "tt-selectable",
+ empty: "tt-empty",
+ open: "tt-open",
+ cursor: "tt-cursor",
+ highlight: "tt-highlight"
+ };
+ return build;
+ function build(o) {
+ var www, classes;
+ classes = _.mixin({}, defaultClassNames, o);
+ www = {
+ css: buildCss(),
+ classes: classes,
+ html: buildHtml(classes),
+ selectors: buildSelectors(classes)
+ };
+ return {
+ css: www.css,
+ html: www.html,
+ classes: www.classes,
+ selectors: www.selectors,
+ mixin: function(o) {
+ _.mixin(o, www);
+ }
+ };
+ }
+ function buildHtml(c) {
+ return {
+ wrapper: '<span class="' + c.wrapper + '"></span>',
+ menu: '<div class="' + c.menu + '"></div>'
+ };
+ }
+ function buildSelectors(classes) {
+ var selectors = {};
+ _.each(classes, function(v, k) {
+ selectors[k] = "." + v;
+ });
+ return selectors;
+ }
+ function buildCss() {
+ var css = {
+ wrapper: {
+ position: "relative",
+ display: "inline-block"
+ },
+ hint: {
+ position: "absolute",
+ top: "0",
+ left: "0",
+ borderColor: "transparent",
+ boxShadow: "none",
+ opacity: "1"
+ },
+ input: {
+ position: "relative",
+ verticalAlign: "top",
+ backgroundColor: "transparent"
+ },
+ inputWithNoHint: {
+ position: "relative",
+ verticalAlign: "top"
+ },
+ menu: {
+ position: "absolute",
+ top: "100%",
+ left: "0",
+ zIndex: "100",
+ display: "none"
+ },
+ ltr: {
+ left: "0",
+ right: "auto"
+ },
+ rtl: {
+ left: "auto",
+ right: " 0"
+ }
+ };
+ if (_.isMsie()) {
+ _.mixin(css.input, {
+ backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
+ });
+ }
+ return css;
+ }
+ }();
+ var EventBus = function() {
+ "use strict";
+ var namespace, deprecationMap;
+ namespace = "typeahead:";
+ deprecationMap = {
+ render: "rendered",
+ cursorchange: "cursorchanged",
+ select: "selected",
+ autocomplete: "autocompleted"
+ };
+ function EventBus(o) {
+ if (!o || !o.el) {
+ $.error("EventBus initialized without el");
+ }
+ this.$el = $(o.el);
+ }
+ _.mixin(EventBus.prototype, {
+ _trigger: function(type, args) {
+ var $e;
+ $e = $.Event(namespace + type);
+ (args = args || []).unshift($e);
+ this.$el.trigger.apply(this.$el, args);
+ return $e;
+ },
+ before: function(type) {
+ var args, $e;
+ args = [].slice.call(arguments, 1);
+ $e = this._trigger("before" + type, args);
+ return $e.isDefaultPrevented();
+ },
+ trigger: function(type) {
+ var deprecatedType;
+ this._trigger(type, [].slice.call(arguments, 1));
+ if (deprecatedType = deprecationMap[type]) {
+ this._trigger(deprecatedType, [].slice.call(arguments, 1));
+ }
+ }
+ });
+ return EventBus;
+ }();
+ var EventEmitter = function() {
+ "use strict";
+ var splitter = /\s+/, nextTick = getNextTick();
+ return {
+ onSync: onSync,
+ onAsync: onAsync,
+ off: off,
+ trigger: trigger
+ };
+ function on(method, types, cb, context) {
+ var type;
+ if (!cb) {
+ return this;
+ }
+ types = types.split(splitter);
+ cb = context ? bindContext(cb, context) : cb;
+ this._callbacks = this._callbacks || {};
+ while (type = types.shift()) {
+ this._callbacks[type] = this._callbacks[type] || {
+ sync: [],
+ async: []
+ };
+ this._callbacks[type][method].push(cb);
+ }
+ return this;
+ }
+ function onAsync(types, cb, context) {
+ return on.call(this, "async", types, cb, context);
+ }
+ function onSync(types, cb, context) {
+ return on.call(this, "sync", types, cb, context);
+ }
+ function off(types) {
+ var type;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ while (type = types.shift()) {
+ delete this._callbacks[type];
+ }
+ return this;
+ }
+ function trigger(types) {
+ var type, callbacks, args, syncFlush, asyncFlush;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ args = [].slice.call(arguments, 1);
+ while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
+ syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
+ asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
+ syncFlush() && nextTick(asyncFlush);
+ }
+ return this;
+ }
+ function getFlush(callbacks, context, args) {
+ return flush;
+ function flush() {
+ var cancelled;
+ for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
+ cancelled = callbacks[i].apply(context, args) === false;
+ }
+ return !cancelled;
+ }
+ }
+ function getNextTick() {
+ var nextTickFn;
+ if (window.setImmediate) {
+ nextTickFn = function nextTickSetImmediate(fn) {
+ setImmediate(function() {
+ fn();
+ });
+ };
+ } else {
+ nextTickFn = function nextTickSetTimeout(fn) {
+ setTimeout(function() {
+ fn();
+ }, 0);
+ };
+ }
+ return nextTickFn;
+ }
+ function bindContext(fn, context) {
+ return fn.bind ? fn.bind(context) : function() {
+ fn.apply(context, [].slice.call(arguments, 0));
+ };
+ }
+ }();
+ var highlight = function(doc) {
+ "use strict";
+ var defaults = {
+ node: null,
+ pattern: null,
+ tagName: "strong",
+ className: null,
+ wordsOnly: false,
+ caseSensitive: false
+ };
+ return function hightlight(o) {
+ var regex;
+ o = _.mixin({}, defaults, o);
+ if (!o.node || !o.pattern) {
+ return;
+ }
+ o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
+ regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
+ traverse(o.node, hightlightTextNode);
+ function hightlightTextNode(textNode) {
+ var match, patternNode, wrapperNode;
+ if (match = regex.exec(textNode.data)) {
+ wrapperNode = doc.createElement(o.tagName);
+ o.className && (wrapperNode.className = o.className);
+ patternNode = textNode.splitText(match.index);
+ patternNode.splitText(match[0].length);
+ wrapperNode.appendChild(patternNode.cloneNode(true));
+ textNode.parentNode.replaceChild(wrapperNode, patternNode);
+ }
+ return !!match;
+ }
+ function traverse(el, hightlightTextNode) {
+ var childNode, TEXT_NODE_TYPE = 3;
+ for (var i = 0; i < el.childNodes.length; i++) {
+ childNode = el.childNodes[i];
+ if (childNode.nodeType === TEXT_NODE_TYPE) {
+ i += hightlightTextNode(childNode) ? 1 : 0;
+ } else {
+ traverse(childNode, hightlightTextNode);
+ }
+ }
+ }
+ };
+ function getRegex(patterns, caseSensitive, wordsOnly) {
+ var escapedPatterns = [], regexStr;
+ for (var i = 0, len = patterns.length; i < len; i++) {
+ escapedPatterns.push(_.escapeRegExChars(patterns[i]));
+ }
+ regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
+ return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
+ }
+ }(window.document);
+ var Input = function() {
+ "use strict";
+ var specialKeyCodeMap;
+ specialKeyCodeMap = {
+ 9: "tab",
+ 27: "esc",
+ 37: "left",
+ 39: "right",
+ 13: "enter",
+ 38: "up",
+ 40: "down"
+ };
+ function Input(o, www) {
+ o = o || {};
+ if (!o.input) {
+ $.error("input is missing");
+ }
+ www.mixin(this);
+ this.$hint = $(o.hint);
+ this.$input = $(o.input);
+ this.query = this.$input.val();
+ this.queryWhenFocused = this.hasFocus() ? this.query : null;
+ this.$overflowHelper = buildOverflowHelper(this.$input);
+ this._checkLanguageDirection();
+ if (this.$hint.length === 0) {
+ this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
+ }
+ }
+ Input.normalizeQuery = function(str) {
+ return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
+ };
+ _.mixin(Input.prototype, EventEmitter, {
+ _onBlur: function onBlur() {
+ this.resetInputValue();
+ this.trigger("blurred");
+ },
+ _onFocus: function onFocus() {
+ this.queryWhenFocused = this.query;
+ this.trigger("focused");
+ },
+ _onKeydown: function onKeydown($e) {
+ var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
+ this._managePreventDefault(keyName, $e);
+ if (keyName && this._shouldTrigger(keyName, $e)) {
+ this.trigger(keyName + "Keyed", $e);
+ }
+ },
+ _onInput: function onInput() {
+ this._setQuery(this.getInputValue());
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ _managePreventDefault: function managePreventDefault(keyName, $e) {
+ var preventDefault;
+ switch (keyName) {
+ case "up":
+ case "down":
+ preventDefault = !withModifier($e);
+ break;
+
+ default:
+ preventDefault = false;
+ }
+ preventDefault && $e.preventDefault();
+ },
+ _shouldTrigger: function shouldTrigger(keyName, $e) {
+ var trigger;
+ switch (keyName) {
+ case "tab":
+ trigger = !withModifier($e);
+ break;
+
+ default:
+ trigger = true;
+ }
+ return trigger;
+ },
+ _checkLanguageDirection: function checkLanguageDirection() {
+ var dir = (this.$input.css("direction") || "ltr").toLowerCase();
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.$hint.attr("dir", dir);
+ this.trigger("langDirChanged", dir);
+ }
+ },
+ _setQuery: function setQuery(val, silent) {
+ var areEquivalent, hasDifferentWhitespace;
+ areEquivalent = areQueriesEquivalent(val, this.query);
+ hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false;
+ this.query = val;
+ if (!silent && !areEquivalent) {
+ this.trigger("queryChanged", this.query);
+ } else if (!silent && hasDifferentWhitespace) {
+ this.trigger("whitespaceChanged", this.query);
+ }
+ },
+ bind: function() {
+ var that = this, onBlur, onFocus, onKeydown, onInput;
+ onBlur = _.bind(this._onBlur, this);
+ onFocus = _.bind(this._onFocus, this);
+ onKeydown = _.bind(this._onKeydown, this);
+ onInput = _.bind(this._onInput, this);
+ this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
+ if (!_.isMsie() || _.isMsie() > 9) {
+ this.$input.on("input.tt", onInput);
+ } else {
+ this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
+ if (specialKeyCodeMap[$e.which || $e.keyCode]) {
+ return;
+ }
+ _.defer(_.bind(that._onInput, that, $e));
+ });
+ }
+ return this;
+ },
+ focus: function focus() {
+ this.$input.focus();
+ },
+ blur: function blur() {
+ this.$input.blur();
+ },
+ getLangDir: function getLangDir() {
+ return this.dir;
+ },
+ getQuery: function getQuery() {
+ return this.query || "";
+ },
+ setQuery: function setQuery(val, silent) {
+ this.setInputValue(val);
+ this._setQuery(val, silent);
+ },
+ hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
+ return this.query !== this.queryWhenFocused;
+ },
+ getInputValue: function getInputValue() {
+ return this.$input.val();
+ },
+ setInputValue: function setInputValue(value) {
+ this.$input.val(value);
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ resetInputValue: function resetInputValue() {
+ this.setInputValue(this.query);
+ },
+ getHint: function getHint() {
+ return this.$hint.val();
+ },
+ setHint: function setHint(value) {
+ this.$hint.val(value);
+ },
+ clearHint: function clearHint() {
+ this.setHint("");
+ },
+ clearHintIfInvalid: function clearHintIfInvalid() {
+ var val, hint, valIsPrefixOfHint, isValid;
+ val = this.getInputValue();
+ hint = this.getHint();
+ valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
+ isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
+ !isValid && this.clearHint();
+ },
+ hasFocus: function hasFocus() {
+ return this.$input.is(":focus");
+ },
+ hasOverflow: function hasOverflow() {
+ var constraint = this.$input.width() - 2;
+ this.$overflowHelper.text(this.getInputValue());
+ return this.$overflowHelper.width() >= constraint;
+ },
+ isCursorAtEnd: function() {
+ var valueLength, selectionStart, range;
+ valueLength = this.$input.val().length;
+ selectionStart = this.$input[0].selectionStart;
+ if (_.isNumber(selectionStart)) {
+ return selectionStart === valueLength;
+ } else if (document.selection) {
+ range = document.selection.createRange();
+ range.moveStart("character", -valueLength);
+ return valueLength === range.text.length;
+ }
+ return true;
+ },
+ destroy: function destroy() {
+ this.$hint.off(".tt");
+ this.$input.off(".tt");
+ this.$overflowHelper.remove();
+ this.$hint = this.$input = this.$overflowHelper = $("<div>");
+ }
+ });
+ return Input;
+ function buildOverflowHelper($input) {
+ return $('<pre aria-hidden="true"></pre>').css({
+ position: "absolute",
+ visibility: "hidden",
+ whiteSpace: "pre",
+ fontFamily: $input.css("font-family"),
+ fontSize: $input.css("font-size"),
+ fontStyle: $input.css("font-style"),
+ fontVariant: $input.css("font-variant"),
+ fontWeight: $input.css("font-weight"),
+ wordSpacing: $input.css("word-spacing"),
+ letterSpacing: $input.css("letter-spacing"),
+ textIndent: $input.css("text-indent"),
+ textRendering: $input.css("text-rendering"),
+ textTransform: $input.css("text-transform")
+ }).insertAfter($input);
+ }
+ function areQueriesEquivalent(a, b) {
+ return Input.normalizeQuery(a) === Input.normalizeQuery(b);
+ }
+ function withModifier($e) {
+ return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
+ }
+ }();
+ var Dataset = function() {
+ "use strict";
+ var keys, nameGenerator;
+ keys = {
+ val: "tt-selectable-display",
+ obj: "tt-selectable-object"
+ };
+ nameGenerator = _.getIdGenerator();
+ function Dataset(o, www) {
+ o = o || {};
+ o.templates = o.templates || {};
+ o.templates.notFound = o.templates.notFound || o.templates.empty;
+ if (!o.source) {
+ $.error("missing source");
+ }
+ if (!o.node) {
+ $.error("missing node");
+ }
+ if (o.name && !isValidName(o.name)) {
+ $.error("invalid dataset name: " + o.name);
+ }
+ www.mixin(this);
+ this.highlight = !!o.highlight;
+ this.name = o.name || nameGenerator();
+ this.limit = o.limit || 5;
+ this.displayFn = getDisplayFn(o.display || o.displayKey);
+ this.templates = getTemplates(o.templates, this.displayFn);
+ this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
+ this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
+ this._resetLastSuggestion();
+ this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
+ }
+ Dataset.extractData = function extractData(el) {
+ var $el = $(el);
+ if ($el.data(keys.obj)) {
+ return {
+ val: $el.data(keys.val) || "",
+ obj: $el.data(keys.obj) || null
+ };
+ }
+ return null;
+ };
+ _.mixin(Dataset.prototype, EventEmitter, {
+ _overwrite: function overwrite(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (this.async && this.templates.pending) {
+ this._renderPending(query);
+ } else if (!this.async && this.templates.notFound) {
+ this._renderNotFound(query);
+ } else {
+ this._empty();
+ }
+ this.trigger("rendered", this.name, suggestions, false);
+ },
+ _append: function append(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length && this.$lastSuggestion.length) {
+ this._appendSuggestions(query, suggestions);
+ } else if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (!this.$lastSuggestion.length && this.templates.notFound) {
+ this._renderNotFound(query);
+ }
+ this.trigger("rendered", this.name, suggestions, true);
+ },
+ _renderSuggestions: function renderSuggestions(query, suggestions) {
+ var $fragment;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ this.$lastSuggestion = $fragment.children().last();
+ this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions));
+ },
+ _appendSuggestions: function appendSuggestions(query, suggestions) {
+ var $fragment, $lastSuggestion;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ $lastSuggestion = $fragment.children().last();
+ this.$lastSuggestion.after($fragment);
+ this.$lastSuggestion = $lastSuggestion;
+ },
+ _renderPending: function renderPending(query) {
+ var template = this.templates.pending;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _renderNotFound: function renderNotFound(query) {
+ var template = this.templates.notFound;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _empty: function empty() {
+ this.$el.empty();
+ this._resetLastSuggestion();
+ },
+ _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
+ var that = this, fragment;
+ fragment = document.createDocumentFragment();
+ _.each(suggestions, function getSuggestionNode(suggestion) {
+ var $el, context;
+ context = that._injectQuery(query, suggestion);
+ $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
+ fragment.appendChild($el[0]);
+ });
+ this.highlight && highlight({
+ className: this.classes.highlight,
+ node: fragment,
+ pattern: query
+ });
+ return $(fragment);
+ },
+ _getFooter: function getFooter(query, suggestions) {
+ return this.templates.footer ? this.templates.footer({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _getHeader: function getHeader(query, suggestions) {
+ return this.templates.header ? this.templates.header({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _resetLastSuggestion: function resetLastSuggestion() {
+ this.$lastSuggestion = $();
+ },
+ _injectQuery: function injectQuery(query, obj) {
+ return _.isObject(obj) ? _.mixin({
+ _query: query
+ }, obj) : obj;
+ },
+ update: function update(query) {
+ var that = this, canceled = false, syncCalled = false, rendered = 0;
+ this.cancel();
+ this.cancel = function cancel() {
+ canceled = true;
+ that.cancel = $.noop;
+ that.async && that.trigger("asyncCanceled", query);
+ };
+ this.source(query, sync, async);
+ !syncCalled && sync([]);
+ function sync(suggestions) {
+ if (syncCalled) {
+ return;
+ }
+ syncCalled = true;
+ suggestions = (suggestions || []).slice(0, that.limit);
+ rendered = suggestions.length;
+ that._overwrite(query, suggestions);
+ if (rendered < that.limit && that.async) {
+ that.trigger("asyncRequested", query);
+ }
+ }
+ function async(suggestions) {
+ suggestions = suggestions || [];
+ if (!canceled && rendered < that.limit) {
+ that.cancel = $.noop;
+ rendered += suggestions.length;
+ that._append(query, suggestions.slice(0, that.limit - rendered));
+ that.async && that.trigger("asyncReceived", query);
+ }
+ }
+ },
+ cancel: $.noop,
+ clear: function clear() {
+ this._empty();
+ this.cancel();
+ this.trigger("cleared");
+ },
+ isEmpty: function isEmpty() {
+ return this.$el.is(":empty");
+ },
+ destroy: function destroy() {
+ this.$el = $("<div>");
+ }
+ });
+ return Dataset;
+ function getDisplayFn(display) {
+ display = display || _.stringify;
+ return _.isFunction(display) ? display : displayFn;
+ function displayFn(obj) {
+ return obj[display];
+ }
+ }
+ function getTemplates(templates, displayFn) {
+ return {
+ notFound: templates.notFound && _.templatify(templates.notFound),
+ pending: templates.pending && _.templatify(templates.pending),
+ header: templates.header && _.templatify(templates.header),
+ footer: templates.footer && _.templatify(templates.footer),
+ suggestion: templates.suggestion || suggestionTemplate
+ };
+ function suggestionTemplate(context) {
+ return $("<div>").text(displayFn(context));
+ }
+ }
+ function isValidName(str) {
+ return /^[_a-zA-Z0-9-]+$/.test(str);
+ }
+ }();
+ var Menu = function() {
+ "use strict";
+ function Menu(o, www) {
+ var that = this;
+ o = o || {};
+ if (!o.node) {
+ $.error("node is required");
+ }
+ www.mixin(this);
+ this.$node = $(o.node);
+ this.query = null;
+ this.datasets = _.map(o.datasets, initializeDataset);
+ function initializeDataset(oDataset) {
+ var node = that.$node.find(oDataset.node).first();
+ oDataset.node = node.length ? node : $("<div>").appendTo(that.$node);
+ return new Dataset(oDataset, www);
+ }
+ }
+ _.mixin(Menu.prototype, EventEmitter, {
+ _onSelectableClick: function onSelectableClick($e) {
+ this.trigger("selectableClicked", $($e.currentTarget));
+ },
+ _onRendered: function onRendered(type, dataset, suggestions, async) {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetRendered", dataset, suggestions, async);
+ },
+ _onCleared: function onCleared() {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetCleared");
+ },
+ _propagate: function propagate() {
+ this.trigger.apply(this, arguments);
+ },
+ _allDatasetsEmpty: function allDatasetsEmpty() {
+ return _.every(this.datasets, isDatasetEmpty);
+ function isDatasetEmpty(dataset) {
+ return dataset.isEmpty();
+ }
+ },
+ _getSelectables: function getSelectables() {
+ return this.$node.find(this.selectors.selectable);
+ },
+ _removeCursor: function _removeCursor() {
+ var $selectable = this.getActiveSelectable();
+ $selectable && $selectable.removeClass(this.classes.cursor);
+ },
+ _ensureVisible: function ensureVisible($el) {
+ var elTop, elBottom, nodeScrollTop, nodeHeight;
+ elTop = $el.position().top;
+ elBottom = elTop + $el.outerHeight(true);
+ nodeScrollTop = this.$node.scrollTop();
+ nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10);
+ if (elTop < 0) {
+ this.$node.scrollTop(nodeScrollTop + elTop);
+ } else if (nodeHeight < elBottom) {
+ this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
+ }
+ },
+ bind: function() {
+ var that = this, onSelectableClick;
+ onSelectableClick = _.bind(this._onSelectableClick, this);
+ this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
+ _.each(this.datasets, function(dataset) {
+ dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ return this.$node.hasClass(this.classes.open);
+ },
+ open: function open() {
+ this.$node.addClass(this.classes.open);
+ },
+ close: function close() {
+ this.$node.removeClass(this.classes.open);
+ this._removeCursor();
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.attr("dir", dir);
+ },
+ selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
+ var $selectables, $oldCursor, oldIndex, newIndex;
+ $oldCursor = this.getActiveSelectable();
+ $selectables = this._getSelectables();
+ oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
+ newIndex = oldIndex + delta;
+ newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
+ newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
+ return newIndex === -1 ? null : $selectables.eq(newIndex);
+ },
+ setCursor: function setCursor($selectable) {
+ this._removeCursor();
+ if ($selectable = $selectable && $selectable.first()) {
+ $selectable.addClass(this.classes.cursor);
+ this._ensureVisible($selectable);
+ }
+ },
+ getSelectableData: function getSelectableData($el) {
+ return $el && $el.length ? Dataset.extractData($el) : null;
+ },
+ getActiveSelectable: function getActiveSelectable() {
+ var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
+ return $selectable.length ? $selectable : null;
+ },
+ getTopSelectable: function getTopSelectable() {
+ var $selectable = this._getSelectables().first();
+ return $selectable.length ? $selectable : null;
+ },
+ update: function update(query) {
+ var isValidUpdate = query !== this.query;
+ if (isValidUpdate) {
+ this.query = query;
+ _.each(this.datasets, updateDataset);
+ }
+ return isValidUpdate;
+ function updateDataset(dataset) {
+ dataset.update(query);
+ }
+ },
+ empty: function empty() {
+ _.each(this.datasets, clearDataset);
+ this.query = null;
+ this.$node.addClass(this.classes.empty);
+ function clearDataset(dataset) {
+ dataset.clear();
+ }
+ },
+ destroy: function destroy() {
+ this.$node.off(".tt");
+ this.$node = $("<div>");
+ _.each(this.datasets, destroyDataset);
+ function destroyDataset(dataset) {
+ dataset.destroy();
+ }
+ }
+ });
+ return Menu;
+ }();
+ var DefaultMenu = function() {
+ "use strict";
+ var s = Menu.prototype;
+ function DefaultMenu() {
+ Menu.apply(this, [].slice.call(arguments, 0));
+ }
+ _.mixin(DefaultMenu.prototype, Menu.prototype, {
+ open: function open() {
+ !this._allDatasetsEmpty() && this._show();
+ return s.open.apply(this, [].slice.call(arguments, 0));
+ },
+ close: function close() {
+ this._hide();
+ return s.close.apply(this, [].slice.call(arguments, 0));
+ },
+ _onRendered: function onRendered() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onRendered.apply(this, [].slice.call(arguments, 0));
+ },
+ _onCleared: function onCleared() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onCleared.apply(this, [].slice.call(arguments, 0));
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl);
+ return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
+ },
+ _hide: function hide() {
+ this.$node.hide();
+ },
+ _show: function show() {
+ this.$node.css("display", "block");
+ }
+ });
+ return DefaultMenu;
+ }();
+ var Typeahead = function() {
+ "use strict";
+ function Typeahead(o, www) {
+ var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged;
+ o = o || {};
+ if (!o.input) {
+ $.error("missing input");
+ }
+ if (!o.menu) {
+ $.error("missing menu");
+ }
+ if (!o.eventBus) {
+ $.error("missing event bus");
+ }
+ www.mixin(this);
+ this.eventBus = o.eventBus;
+ this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
+ this.input = o.input;
+ this.menu = o.menu;
+ this.enabled = true;
+ this.active = false;
+ this.input.hasFocus() && this.activate();
+ this.dir = this.input.getLangDir();
+ this._hacks();
+ this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this);
+ onFocused = c(this, "activate", "open", "_onFocused");
+ onBlurred = c(this, "deactivate", "_onBlurred");
+ onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed");
+ onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed");
+ onEscKeyed = c(this, "isActive", "_onEscKeyed");
+ onUpKeyed = c(this, "isActive", "open", "_onUpKeyed");
+ onDownKeyed = c(this, "isActive", "open", "_onDownKeyed");
+ onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed");
+ onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed");
+ onQueryChanged = c(this, "_openIfActive", "_onQueryChanged");
+ onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged");
+ this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this);
+ }
+ _.mixin(Typeahead.prototype, {
+ _hacks: function hacks() {
+ var $input, $menu;
+ $input = this.input.$input || $("<div>");
+ $menu = this.menu.$node || $("<div>");
+ $input.on("blur.tt", function($e) {
+ var active, isActive, hasActive;
+ active = document.activeElement;
+ isActive = $menu.is(active);
+ hasActive = $menu.has(active).length > 0;
+ if (_.isMsie() && (isActive || hasActive)) {
+ $e.preventDefault();
+ $e.stopImmediatePropagation();
+ _.defer(function() {
+ $input.focus();
+ });
+ }
+ });
+ $menu.on("mousedown.tt", function($e) {
+ $e.preventDefault();
+ });
+ },
+ _onSelectableClicked: function onSelectableClicked(type, $el) {
+ this.select($el);
+ },
+ _onDatasetCleared: function onDatasetCleared() {
+ this._updateHint();
+ },
+ _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
+ this._updateHint();
+ this.eventBus.trigger("render", suggestions, async, dataset);
+ },
+ _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
+ this.eventBus.trigger("asyncrequest", query, dataset);
+ },
+ _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
+ this.eventBus.trigger("asynccancel", query, dataset);
+ },
+ _onAsyncReceived: function onAsyncReceived(type, dataset, query) {
+ this.eventBus.trigger("asyncreceive", query, dataset);
+ },
+ _onFocused: function onFocused() {
+ this._minLengthMet() && this.menu.update(this.input.getQuery());
+ },
+ _onBlurred: function onBlurred() {
+ if (this.input.hasQueryChangedSinceLastFocus()) {
+ this.eventBus.trigger("change", this.input.getQuery());
+ }
+ },
+ _onEnterKeyed: function onEnterKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ }
+ },
+ _onTabKeyed: function onTabKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ } else if ($selectable = this.menu.getTopSelectable()) {
+ this.autocomplete($selectable) && $e.preventDefault();
+ }
+ },
+ _onEscKeyed: function onEscKeyed() {
+ this.close();
+ },
+ _onUpKeyed: function onUpKeyed() {
+ this.moveCursor(-1);
+ },
+ _onDownKeyed: function onDownKeyed() {
+ this.moveCursor(+1);
+ },
+ _onLeftKeyed: function onLeftKeyed() {
+ if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onRightKeyed: function onRightKeyed() {
+ if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onQueryChanged: function onQueryChanged(e, query) {
+ this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
+ },
+ _onWhitespaceChanged: function onWhitespaceChanged() {
+ this._updateHint();
+ },
+ _onLangDirChanged: function onLangDirChanged(e, dir) {
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.menu.setLanguageDirection(dir);
+ }
+ },
+ _openIfActive: function openIfActive() {
+ this.isActive() && this.open();
+ },
+ _minLengthMet: function minLengthMet(query) {
+ query = _.isString(query) ? query : this.input.getQuery() || "";
+ return query.length >= this.minLength;
+ },
+ _updateHint: function updateHint() {
+ var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
+ $selectable = this.menu.getTopSelectable();
+ data = this.menu.getSelectableData($selectable);
+ val = this.input.getInputValue();
+ if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
+ query = Input.normalizeQuery(val);
+ escapedQuery = _.escapeRegExChars(query);
+ frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
+ match = frontMatchRegEx.exec(data.val);
+ match && this.input.setHint(val + match[1]);
+ } else {
+ this.input.clearHint();
+ }
+ },
+ isEnabled: function isEnabled() {
+ return this.enabled;
+ },
+ enable: function enable() {
+ this.enabled = true;
+ },
+ disable: function disable() {
+ this.enabled = false;
+ },
+ isActive: function isActive() {
+ return this.active;
+ },
+ activate: function activate() {
+ if (this.isActive()) {
+ return true;
+ } else if (!this.isEnabled() || this.eventBus.before("active")) {
+ return false;
+ } else {
+ this.active = true;
+ this.eventBus.trigger("active");
+ return true;
+ }
+ },
+ deactivate: function deactivate() {
+ if (!this.isActive()) {
+ return true;
+ } else if (this.eventBus.before("idle")) {
+ return false;
+ } else {
+ this.active = false;
+ this.close();
+ this.eventBus.trigger("idle");
+ return true;
+ }
+ },
+ isOpen: function isOpen() {
+ return this.menu.isOpen();
+ },
+ open: function open() {
+ if (!this.isOpen() && !this.eventBus.before("open")) {
+ this.menu.open();
+ this._updateHint();
+ this.eventBus.trigger("open");
+ }
+ return this.isOpen();
+ },
+ close: function close() {
+ if (this.isOpen() && !this.eventBus.before("close")) {
+ this.menu.close();
+ this.input.clearHint();
+ this.input.resetInputValue();
+ this.eventBus.trigger("close");
+ }
+ return !this.isOpen();
+ },
+ setVal: function setVal(val) {
+ this.input.setQuery(_.toStr(val));
+ },
+ getVal: function getVal() {
+ return this.input.getQuery();
+ },
+ select: function select($selectable) {
+ var data = this.menu.getSelectableData($selectable);
+ if (data && !this.eventBus.before("select", data.obj)) {
+ this.input.setQuery(data.val, true);
+ this.eventBus.trigger("select", data.obj);
+ this.close();
+ return true;
+ }
+ return false;
+ },
+ autocomplete: function autocomplete($selectable) {
+ var query, data, isValid;
+ query = this.input.getQuery();
+ data = this.menu.getSelectableData($selectable);
+ isValid = data && query !== data.val;
+ if (isValid && !this.eventBus.before("autocomplete", data.obj)) {
+ this.input.setQuery(data.val);
+ this.eventBus.trigger("autocomplete", data.obj);
+ return true;
+ }
+ return false;
+ },
+ moveCursor: function moveCursor(delta) {
+ var query, $candidate, data, payload, cancelMove;
+ query = this.input.getQuery();
+ $candidate = this.menu.selectableRelativeToCursor(delta);
+ data = this.menu.getSelectableData($candidate);
+ payload = data ? data.obj : null;
+ cancelMove = this._minLengthMet() && this.menu.update(query);
+ if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
+ this.menu.setCursor($candidate);
+ if (data) {
+ this.input.setInputValue(data.val);
+ } else {
+ this.input.resetInputValue();
+ this._updateHint();
+ }
+ this.eventBus.trigger("cursorchange", payload);
+ return true;
+ }
+ return false;
+ },
+ destroy: function destroy() {
+ this.input.destroy();
+ this.menu.destroy();
+ }
+ });
+ return Typeahead;
+ function c(ctx) {
+ var methods = [].slice.call(arguments, 1);
+ return function() {
+ var args = [].slice.call(arguments);
+ _.each(methods, function(method) {
+ return ctx[method].apply(ctx, args);
+ });
+ };
+ }
+ }();
+ (function() {
+ "use strict";
+ var old, keys, methods;
+ old = $.fn.typeahead;
+ keys = {
+ www: "tt-www",
+ attrs: "tt-attrs",
+ typeahead: "tt-typeahead"
+ };
+ methods = {
+ initialize: function initialize(o, datasets) {
+ var www;
+ datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
+ o = o || {};
+ www = WWW(o.classNames);
+ return this.each(attach);
+ function attach() {
+ var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor;
+ _.each(datasets, function(d) {
+ d.highlight = !!o.highlight;
+ });
+ $input = $(this);
+ $wrapper = $(www.html.wrapper);
+ $hint = $elOrNull(o.hint);
+ $menu = $elOrNull(o.menu);
+ defaultHint = o.hint !== false && !$hint;
+ defaultMenu = o.menu !== false && !$menu;
+ defaultHint && ($hint = buildHintFromInput($input, www));
+ defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
+ $hint && $hint.val("");
+ $input = prepInput($input, www);
+ if (defaultHint || defaultMenu) {
+ $wrapper.css(www.css.wrapper);
+ $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
+ $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null);
+ }
+ MenuConstructor = defaultMenu ? DefaultMenu : Menu;
+ eventBus = new EventBus({
+ el: $input
+ });
+ input = new Input({
+ hint: $hint,
+ input: $input
+ }, www);
+ menu = new MenuConstructor({
+ node: $menu,
+ datasets: datasets
+ }, www);
+ typeahead = new Typeahead({
+ input: input,
+ menu: menu,
+ eventBus: eventBus,
+ minLength: o.minLength
+ }, www);
+ $input.data(keys.www, www);
+ $input.data(keys.typeahead, typeahead);
+ }
+ },
+ isEnabled: function isEnabled() {
+ var enabled;
+ ttEach(this.first(), function(t) {
+ enabled = t.isEnabled();
+ });
+ return enabled;
+ },
+ enable: function enable() {
+ ttEach(this, function(t) {
+ t.enable();
+ });
+ return this;
+ },
+ disable: function disable() {
+ ttEach(this, function(t) {
+ t.disable();
+ });
+ return this;
+ },
+ isActive: function isActive() {
+ var active;
+ ttEach(this.first(), function(t) {
+ active = t.isActive();
+ });
+ return active;
+ },
+ activate: function activate() {
+ ttEach(this, function(t) {
+ t.activate();
+ });
+ return this;
+ },
+ deactivate: function deactivate() {
+ ttEach(this, function(t) {
+ t.deactivate();
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ var open;
+ ttEach(this.first(), function(t) {
+ open = t.isOpen();
+ });
+ return open;
+ },
+ open: function open() {
+ ttEach(this, function(t) {
+ t.open();
+ });
+ return this;
+ },
+ close: function close() {
+ ttEach(this, function(t) {
+ t.close();
+ });
+ return this;
+ },
+ select: function select(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.select($el);
+ });
+ return success;
+ },
+ autocomplete: function autocomplete(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.autocomplete($el);
+ });
+ return success;
+ },
+ moveCursor: function moveCursoe(delta) {
+ var success = false;
+ ttEach(this.first(), function(t) {
+ success = t.moveCursor(delta);
+ });
+ return success;
+ },
+ val: function val(newVal) {
+ var query;
+ if (!arguments.length) {
+ ttEach(this.first(), function(t) {
+ query = t.getVal();
+ });
+ return query;
+ } else {
+ ttEach(this, function(t) {
+ t.setVal(newVal);
+ });
+ return this;
+ }
+ },
+ destroy: function destroy() {
+ ttEach(this, function(typeahead, $input) {
+ revert($input);
+ typeahead.destroy();
+ });
+ return this;
+ }
+ };
+ $.fn.typeahead = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, [].slice.call(arguments, 1));
+ } else {
+ return methods.initialize.apply(this, arguments);
+ }
+ };
+ $.fn.typeahead.noConflict = function noConflict() {
+ $.fn.typeahead = old;
+ return this;
+ };
+ function ttEach($els, fn) {
+ $els.each(function() {
+ var $input = $(this), typeahead;
+ (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
+ });
+ }
+ function buildHintFromInput($input, www) {
+ return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({
+ autocomplete: "off",
+ spellcheck: "false",
+ tabindex: -1
+ });
+ }
+ function prepInput($input, www) {
+ $input.data(keys.attrs, {
+ dir: $input.attr("dir"),
+ autocomplete: $input.attr("autocomplete"),
+ spellcheck: $input.attr("spellcheck"),
+ style: $input.attr("style")
+ });
+ $input.addClass(www.classes.input).attr({
+ autocomplete: "off",
+ spellcheck: false
+ });
+ try {
+ !$input.attr("dir") && $input.attr("dir", "auto");
+ } catch (e) {}
+ return $input;
+ }
+ function getBackgroundStyles($el) {
+ return {
+ backgroundAttachment: $el.css("background-attachment"),
+ backgroundClip: $el.css("background-clip"),
+ backgroundColor: $el.css("background-color"),
+ backgroundImage: $el.css("background-image"),
+ backgroundOrigin: $el.css("background-origin"),
+ backgroundPosition: $el.css("background-position"),
+ backgroundRepeat: $el.css("background-repeat"),
+ backgroundSize: $el.css("background-size")
+ };
+ }
+ function revert($input) {
+ var www, $wrapper;
+ www = $input.data(keys.www);
+ $wrapper = $input.parent().filter(www.selectors.wrapper);
+ _.each($input.data(keys.attrs), function(val, key) {
+ _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
+ });
+ $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input);
+ if ($wrapper.length) {
+ $input.detach().insertAfter($wrapper);
+ $wrapper.remove();
+ }
+ }
+ function $elOrNull(obj) {
+ var isValid, $el;
+ isValid = _.isJQuery(obj) || _.isElement(obj);
+ $el = isValid ? $(obj).first() : [];
+ return $el.length ? $el : null;
+ }
+ })();
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js
index 70d78fd..3dae526 100644
--- a/app/assets/javascripts/users.js
+++ b/app/assets/javascripts/users.js
@@ -6,7 +6,7 @@
"error_tag":"span",
"wrapper_error_class":"error",
"wrapper_tag":"div",
- "wrapper_class":"control-group"
+ "wrapper_class":"form-group"
}
//
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 9cd3a55..856a559 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,10 +9,8 @@
//
// import bootstrap.
//
+@import "bootstrap-sprockets";
@import "bootstrap";
-@import "bootstrap-responsive";
-// backport bootstrap 3.2 features
-@import "backport";
//
// LEAP web app specific overrides
diff --git a/app/assets/stylesheets/backport.scss b/app/assets/stylesheets/backport.scss
deleted file mode 100644
index cadb035..0000000
--- a/app/assets/stylesheets/backport.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// Backporting styles from bootstrap 3.2
-//
-
-
-// List options
-
-// Unstyled keeps list items block level, just removes default browser padding and list-style
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-
-// Inline turns list items into inline-block
-.list-inline {
- @extend .list-unstyled;
- margin-left: -5px;
-
- > li {
- display: inline-block;
- padding-left: 5px;
- padding-right: 5px;
- }
-}
diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss
index abbfc88..8c4d702 100644
--- a/app/assets/stylesheets/leap.scss
+++ b/app/assets/stylesheets/leap.scss
@@ -57,7 +57,6 @@
[class*="-icon-"] {
display: inline-block;
- @include ie7-restore-right-whitespace();
vertical-align: middle;
background-repeat: no-repeat;
margin-top: 1px;
@@ -92,8 +91,8 @@
//
input.large {
- font-size: $baseFontSize * 1.25;
- line-height: $baseLineHeight * 1.5;
+ font-size: $font-size-base * 1.25;
+ line-height: $line-height-base * 1.5;
}
.p {
@@ -120,9 +119,9 @@ input, textarea {
// like a label, but with no background
.label-clear {
- background-color: $white;
+ background-color: $body-bg;
text-shadow: none;
- color: $black;
+ color: $gray-base;
}
// force a black icon, even if bootstrap thinks differently
@@ -188,7 +187,7 @@ input, textarea {
font-weight: bold;
}
a {
- color: $textColor;
+ color: $text-color;
}
}
@@ -199,7 +198,7 @@ input, textarea {
}
.download {
a.btn {
- width: 15em;
+ width: 18em;
font-weight: bold;
small {
font-weight: normal;
@@ -228,9 +227,9 @@ input, textarea {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
li.active {
a, a:hover {
- background-color: $linkColor;
- color: $white;
- border-color: darken($linkColor, 0%);
+ background-color: $link-color;
+ color: $body-bg;
+ border-color: darken($link-color, 0%);
cursor: pointer;
}
}
@@ -265,7 +264,7 @@ $footer-height: 100px;
$footer-border-width: 1px;
$footer-gutter: 20px; // vertical gap above footer
$footer-combined: $footer-height + $footer-border-width + $footer-gutter;
-$footer-color: $grayLighter !default;
+$footer-color: $gray-lighter !default;
html, body {
height: 100%;
@@ -313,7 +312,7 @@ html, body {
// border-top: $footer-border-width solid darken($footer-color, 10%);
background-color: $footer-color;
a {
- color: $black;
+ color: $gray-base;
margin: 0 5px;
}
}
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
new file mode 100644
index 0000000..ee7cca4
--- /dev/null
+++ b/app/controllers/account_controller.rb
@@ -0,0 +1,17 @@
+class AccountController < ApplicationController
+
+ before_filter :require_registration_allowed
+ before_filter :redirect_if_logged_in
+
+ def new
+ @user = User.new
+ end
+
+ protected
+
+ def require_registration_allowed
+ unless APP_CONFIG[:allow_registration]
+ redirect_to home_path
+ end
+ end
+end
diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/api/certs_controller.rb
index ffa6e35..46a84d3 100644
--- a/app/controllers/v1/certs_controller.rb
+++ b/app/controllers/api/certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::CertsController < ApiController
+class Api::CertsController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
before_filter :require_enabled
diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/api/configs_controller.rb
index 4a6f455..0f9b8a6 100644
--- a/app/controllers/v1/configs_controller.rb
+++ b/app/controllers/api/configs_controller.rb
@@ -1,17 +1,15 @@
-class V1::ConfigsController < ApiController
+class Api::ConfigsController < ApiController
include ControllerExtension::JsonFile
before_filter :require_login, :unless => :anonymous_access_allowed?
before_filter :sanitize_id, only: :show
- before_filter :lookup_file, only: :show
- before_filter :fetch_file, only: :show
def index
render json: {services: service_paths}
end
def show
- send_file
+ send_file lookup_file
end
protected
@@ -23,7 +21,11 @@ class V1::ConfigsController < ApiController
}
def service_paths
- Hash[SERVICE_IDS.map{|k,v| [k,"/1/configs/#{v}.json"] } ]
+ Hash[SERVICE_IDS.map{|k,v| [k,"/#{api_version}/configs/#{v}.json"] } ]
+ end
+
+ def api_version
+ ["1", "2"].include?(params[:version]) ? params[:version] : "2"
end
def sanitize_id
@@ -34,6 +36,6 @@ class V1::ConfigsController < ApiController
def lookup_file
path = APP_CONFIG[:config_file_paths][@id]
not_found if path.blank?
- @filename = Rails.root.join path
+ Rails.root.join path
end
end
diff --git a/app/controllers/v1/identities_controller.rb b/app/controllers/api/identities_controller.rb
index 4efd1f5..ab2ac00 100644
--- a/app/controllers/v1/identities_controller.rb
+++ b/app/controllers/api/identities_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class IdentitiesController < ApiController
before_filter :token_authenticate
before_filter :require_monitor
diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/api/messages_controller.rb
index c0ca0c7..a69a40a 100644
--- a/app/controllers/v1/messages_controller.rb
+++ b/app/controllers/api/messages_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class MessagesController < ApiController
before_filter :require_login
diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/api/services_controller.rb
index 523eb44..da2774b 100644
--- a/app/controllers/v1/services_controller.rb
+++ b/app/controllers/api/services_controller.rb
@@ -1,4 +1,4 @@
-class V1::ServicesController < ApiController
+class Api::ServicesController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/api/sessions_controller.rb
index a343d9b..c8deb7a 100644
--- a/app/controllers/v1/sessions_controller.rb
+++ b/app/controllers/api/sessions_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class SessionsController < ApiController
before_filter :require_login, only: :destroy
diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/api/smtp_certs_controller.rb
index 5760645..d9eab7d 100644
--- a/app/controllers/v1/smtp_certs_controller.rb
+++ b/app/controllers/api/smtp_certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::SmtpCertsController < ApiController
+class Api::SmtpCertsController < ApiController
before_filter :require_login
before_filter :require_email_account
diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/api/users_controller.rb
index 6640d10..c79a729 100644
--- a/app/controllers/v1/users_controller.rb
+++ b/app/controllers/api/users_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class UsersController < ApiController
include ControllerExtension::FetchUser
@@ -50,8 +50,7 @@ module V1
end
def destroy
- destroy_identity = current_user.is_monitor? || params[:identities] == "destroy"
- @user.account.destroy(destroy_identity)
+ @user.account.destroy(release_handles)
if @user == current_user
logout
end
@@ -60,6 +59,10 @@ module V1
private
+ def release_handles
+ current_user.is_monitor? || params[:identities] == "destroy"
+ end
+
# tester auth can only create test users.
def create_test_account
if User::is_test?(params[:user][:login])
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 079dc18..2af2f29 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
before_filter :no_frame_header
before_filter :language_header
rescue_from StandardError, :with => :default_error_handler
- rescue_from RestClient::Exception, :with => :default_error_handler
+ rescue_from CouchRest::Exception, :with => :default_error_handler
ActiveSupport.run_load_hooks(:application_controller, self)
diff --git a/app/controllers/controller_extension/fetch_user.rb b/app/controllers/controller_extension/fetch_user.rb
index 97f92fa..632291d 100644
--- a/app/controllers/controller_extension/fetch_user.rb
+++ b/app/controllers/controller_extension/fetch_user.rb
@@ -22,7 +22,7 @@ module ControllerExtension::FetchUser
@user = User.find(params[:user_id] || params[:id])
if current_user.is_admin? || current_user.is_monitor?
if @user.nil?
- not_found(t(:no_such_thing, :thing => 'user'), users_url)
+ not_found(t(:no_such_user), users_url)
elsif current_user.is_monitor?
access_denied unless @user.is_test?
end
diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb
index 6be919a..df9cf55 100644
--- a/app/controllers/controller_extension/json_file.rb
+++ b/app/controllers/controller_extension/json_file.rb
@@ -4,20 +4,25 @@ module ControllerExtension::JsonFile
protected
- def send_file
- if stale?(:last_modified => @file.mtime)
- response.content_type = 'application/json'
- render :text => @file.read
+ def send_file(filename)
+ file = fetch_file(filename)
+ if file.present?
+ send_file_or_cache_hit(file)
+ else
+ not_found
end
end
- def fetch_file
- if File.exists?(@filename)
- @file = File.new(@filename)
- else
- not_found
+ def send_file_or_cache_hit(file)
+ if stale?(:last_modified => file.mtime)
+ response.content_type = 'application/json'
+ render :text => file.read
end
end
+ def fetch_file(filename)
+ File.new(filename) if File.exist?(filename)
+ end
+
end
diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb
index c78e006..46e7cd2 100644
--- a/app/controllers/static_config_controller.rb
+++ b/app/controllers/static_config_controller.rb
@@ -5,13 +5,9 @@ class StaticConfigController < ActionController::Base
include ControllerExtension::JsonFile
before_filter :set_minimum_client_version
- before_filter :set_filename
- before_filter :fetch_file
-
- PROVIDER_JSON = Rails.root.join('config', 'provider', 'provider.json')
def provider
- send_file
+ send_file provider_json
end
protected
@@ -23,7 +19,8 @@ class StaticConfigController < ActionController::Base
APP_CONFIG[:minimum_client_version].to_s
end
- def set_filename
- @filename = PROVIDER_JSON
+ def provider_json
+ Rails.root.join APP_CONFIG[:config_file_paths]['provider']
end
+
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 1404b0e..4d198b9 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -5,11 +5,9 @@
class UsersController < ApplicationController
include ControllerExtension::FetchUser
- before_filter :require_login, :except => [:new]
- before_filter :redirect_if_logged_in, :only => [:new]
+ before_filter :require_login
before_filter :require_admin, :only => [:index, :deactivate, :enable]
- before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable]
- before_filter :require_registration_allowed, only: :new
+ before_filter :fetch_user, :only => [:show, :edit, :destroy, :deactivate, :enable]
respond_to :html
@@ -27,25 +25,12 @@ class UsersController < ApplicationController
@users = @users.limit(100)
end
- def new
- @user = User.new
- end
-
def show
end
def edit
end
- ## added so updating service level works, but not sure we will actually want this. also not sure that this is place to prevent user from updating own effective service level, but here as placeholder:
- def update
- @user.update_attributes(params[:user]) unless (!admin? and params[:user][:effective_service_level])
- if @user.valid?
- flash[:notice] = I18n.t(:changes_saved)
- end
- respond_with @user, :location => edit_user_path(@user)
- end
-
def deactivate
@user.account.disable
flash[:notice] = I18n.t("actions.user_disabled_message", username: @user.username)
@@ -73,10 +58,11 @@ class UsersController < ApplicationController
protected
- def require_registration_allowed
- unless APP_CONFIG[:allow_registration]
- redirect_to home_path
+ def user_params
+ if admin?
+ params.require(:user).permit(:effective_service_level)
+ else
+ params.require(:user).permit(:password, :password_confirmation)
end
end
-
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6de5e1b..920186d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -19,7 +19,8 @@ module ApplicationHelper
# http://twitter.github.io/bootstrap/base-css.html#icons
#
def icon(name, color=nil)
- "<i class=\"icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ content_tag :span, '',
+ class: "glyphicon glyphicon-#{name} #{color_class(color)}"
end
def big_icon(name, color=nil)
diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb
index ddb063e..b74e1d7 100644
--- a/app/helpers/link_helper.rb
+++ b/app/helpers/link_helper.rb
@@ -39,6 +39,10 @@ module LinkHelper
def btn(*args, &block)
html_options = extract_html_options!(args, &block)
type = Array(html_options.delete(:type))
+ btn_opts = [:default, :primary, :success, :info, :warning, :danger, :link]
+ if (type & btn_opts).blank?
+ type << :default
+ end
type.map! {|t| "btn-#{t}"}
html_options[:class] = concat_classes(html_options[:class], 'btn', type)
args[0] = t(args[0]) if args[0].is_a?(Symbol)
diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb
index 2639246..1df840c 100644
--- a/app/helpers/navigation_helper.rb
+++ b/app/helpers/navigation_helper.rb
@@ -66,9 +66,9 @@ module NavigationHelper
end
def extract_icon!(options)
- icon = options.delete(:icon)
- if icon.present?
- content_tag(:i, '', class: "icon-#{icon}")
+ name = options.delete(:icon)
+ if name.present?
+ icon name
else
""
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 7310250..d722caa 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -69,15 +69,13 @@ class Account
@user.refresh_identity
end
- def destroy(destroy_identity=false)
+ def destroy(release_handles=false)
return unless @user
if !@user.is_tmp?
- if destroy_identity == false
- @user.identities.each do |id|
+ @user.identities.each do |id|
+ if release_handles == false
id.orphan!
- end
- else
- @user.identities.each do |id|
+ else
id.destroy
end
end
diff --git a/app/models/api_monitor_user.rb b/app/models/api_monitor_user.rb
new file mode 100644
index 0000000..d0fe411
--- /dev/null
+++ b/app/models/api_monitor_user.rb
@@ -0,0 +1,11 @@
+#
+# A user that has limited admin access, to be used
+# for running monitor tests against a live production
+# installation.
+#
+class ApiMonitorUser < ApiUser
+ def is_monitor?
+ true
+ end
+end
+
diff --git a/app/models/api_user.rb b/app/models/api_user.rb
index 2efe1cb..c70cccb 100644
--- a/app/models/api_user.rb
+++ b/app/models/api_user.rb
@@ -3,21 +3,10 @@ class ApiUser < AnonymousUser
end
#
-# A user that has limited admin access, to be used
-# for running monitor tests against a live production
-# installation.
-#
-class ApiMonitorUser < ApiUser
- def is_monitor?
- true
- end
-end
-
-#
# Not yet supported:
#
#class ApiAdminUser < ApiUser
# def is_admin?
# true
# end
-#end \ No newline at end of file
+#end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index f987e4e..92f8f7a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,3 +1,5 @@
+require 'login_format_validation'
+require 'local_email'
#
# Identity states:
#
diff --git a/app/models/token.rb b/app/models/token.rb
index b398fcb..8ac32b8 100644
--- a/app/models/token.rb
+++ b/app/models/token.rb
@@ -59,8 +59,8 @@ class Token < CouchRest::Model::Base
# So let's make sure we don't crash if they disappeared
def destroy_with_rescue
destroy_without_rescue
- rescue RestClient::ResourceNotFound # do nothing it's gone already
- rescue RestClient::Conflict # do nothing - it's been updated - #7670
+ rescue CouchRest::NotFound
+ rescue CouchRest::Conflict # do nothing - it's been updated - #7670
end
alias_method_chain :destroy, :rescue
diff --git a/app/models/user.rb b/app/models/user.rb
index e3246ad..6541305 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,3 +1,6 @@
+require 'login_format_validation'
+require 'local_email'
+
class User < CouchRest::Model::Base
include LoginFormatValidation
diff --git a/app/views/users/new.html.haml b/app/views/account/new.html.haml
index bc0b1af..d40259e 100644
--- a/app/views/users/new.html.haml
+++ b/app/views/account/new.html.haml
@@ -1,18 +1,18 @@
-#
-# This form is handled entirely by javascript
-# Please take care when changing element ids.
--#
--# The form is hidden when no js is available
+-#
+-# The form is hidden when no js is available
-# to prevent submission in the clear.
-#
- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true}
-.span1
-.span9
+.col-md-1
+.col-md-9
%h2=t :signup
.lead=t :signup_info
- = render :partial => 'warnings'
+ = render "sessions/warnings"
= simple_form_for(@user, form_options) do |f|
= f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username }
= f.input :password, :label => t(:password), :required => false, :validate => true, :input_html => { :id => :srp_password }
diff --git a/app/views/v1/sessions/new.json.erb b/app/views/api/sessions/new.json.erb
index 36154b8..36154b8 100644
--- a/app/views/v1/sessions/new.json.erb
+++ b/app/views/api/sessions/new.json.erb
diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml
index 81ebf67..eb663c0 100644
--- a/app/views/common/_action_buttons.html.haml
+++ b/app/views/common/_action_buttons.html.haml
@@ -1,14 +1,14 @@
.home-buttons
- .row-fluid.second
- .login.span4
+ .row.second
+ .login.col-md-4
%span.link= btn icon('ok-sign') + t(:login), login_path
%span.info= t(:login_info, default: "")
- if APP_CONFIG[:allow_registration]
- .signup.span4
+ .signup.col-md-4
%span.link= btn icon('user') + t(:signup), signup_path
%span.info= t(:signup_info, default: "")
- else
- .signup.span4
- .help.span4
+ .signup.col-md-4
+ .help.col-md-4
%span.link= btn icon('question-sign') + t(:get_help), new_ticket_path
%span.info= t(:support_info, default: "")
diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml
index 9c26860..1278230 100644
--- a/app/views/common/_download_button.html.haml
+++ b/app/views/common/_download_button.html.haml
@@ -1,8 +1,8 @@
.home-buttons
- .row-fluid.first
- .span2
- .download.span8
+ .row.first
+ .col-md-2
+ .download.col-md-8
= btn client_download_url, type: [:large, :primary] do
= big_icon('download')
= t(:download_bitmask)
- .span2
+ .col-md-2
diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml
index fc6348e..cfe3734 100644
--- a/app/views/common/_home_page_buttons.html.haml
+++ b/app/views/common/_home_page_buttons.html.haml
@@ -1,6 +1,6 @@
= render 'common/download_button'
- if local_assigns[:divider]
- .row-fluid
- .span12
+ .row
+ .col-md-12
= render local_assigns[:divider]
= render 'common/action_buttons'
diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml
index d96a1e0..67e4533 100644
--- a/app/views/home/_content.html.haml
+++ b/app/views/home/_content.html.haml
@@ -1,12 +1,12 @@
-.row-fluid
+.row
%h1= t(:welcome, :provider => APP_CONFIG[:domain])
.p=t(:welcome_message_html)
-.row-fluid
+.row
= home_page_buttons
- if Rails.env == 'development'
- .row-fluid
+ .row
%hr
%p
= link_to "make donation", new_payment_path if APP_CONFIG[:payment].present?
diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml
index d5c2fa3..8e0bfbc 100644
--- a/app/views/layouts/_content.html.haml
+++ b/app/views/layouts/_content.html.haml
@@ -7,13 +7,13 @@
- else
- content = yield
-- if @show_navigation
- .span2
+- if @show_navigation && @user
+ .col-md-2
= render 'layouts/navigation'
- .span10
+ .col-md-10
= render 'layouts/messages'
= content
- else
- .span12
+ .col-md-12
= render 'layouts/messages'
= content
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 0aeda8b..e242a5f 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -15,13 +15,13 @@
#main
.container-fluid
- if @show_navigation && !admin?
- .row-fluid
+ .row
%h1= t(:user_control_panel)
- if logged_in?
- .row-fluid
- .span12
+ .row
+ .col-md-12
= render 'layouts/header'
- .row-fluid
+ .row
= render 'layouts/content'
#push
-# #push is used for sticky footer in bootstrap 2. remove when upgrading to bootstrap 3
diff --git a/app/views/users/_warnings.html.haml b/app/views/sessions/_warnings.html.haml
index baf80a4..baf80a4 100644
--- a/app/views/users/_warnings.html.haml
+++ b/app/views/sessions/_warnings.html.haml
diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml
index bb7e4bd..6695123 100644
--- a/app/views/sessions/new.html.haml
+++ b/app/views/sessions/new.html.haml
@@ -1,8 +1,8 @@
-.span1
-.span9
+.col-md-1
+.col-md-9
%h2=t :login
.lead=t :login_info
- = render :partial => 'users/warnings'
+ = render 'warnings'
= simple_form_for [:api, @session], validate: true, html: { id: :new_session, class: 'form-horizontal hidden js-show', style: "display:none;" } do |f|
= f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username }
= f.input :password, :required => false, :input_html => { :id => :srp_password }
diff --git a/app/views/users/_change_password.html.haml b/app/views/users/_change_password.html.haml
index 425e3ee..64a4d0a 100644
--- a/app/views/users/_change_password.html.haml
+++ b/app/views/users/_change_password.html.haml
@@ -15,7 +15,7 @@
= f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username}
= f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password }
= f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation }
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn btn-primary'
diff --git a/app/views/users/_change_pgp_key.html.haml b/app/views/users/_change_pgp_key.html.haml
index e465125..af3eb5e 100644
--- a/app/views/users/_change_pgp_key.html.haml
+++ b/app/views/users/_change_pgp_key.html.haml
@@ -8,6 +8,6 @@
= simple_form_for [:api, @user], form_options do |f|
%legend= t(:advanced_options)
= f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml
index 42315a2..32ea8c0 100644
--- a/app/views/users/_change_service_level.html.haml
+++ b/app/views/users/_change_service_level.html.haml
@@ -1,8 +1,13 @@
--# TODO: probably won't want here, but here for now. Also, we will need way to ensure payment if they pick a non-free plan.
--#
--# SERVICE LEVEL
--#
-- if APP_CONFIG[:service_levels]
+:ruby
+ # DISABLED! this form points to a route that does not exist.
+ # It's a draft for implementing service levels.
+ # TODO: probably won't want here, but here for now.
+ # We will need way to ensure payment for a non-free plan.
+ #
+ # SERVICE LEVEL
+ #
+ #
+- if APP_CONFIG[:service_levels] && false
- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_service_level', :data => {token: session[:token]}}, :validate => true}
= simple_form_for @user, form_options do |f|
%legend= t(:service_level)
@@ -13,6 +18,6 @@
%p
= t(:effective_service_level)
= f.select :effective_service_level_code, ServiceLevel.select_options, :selected => @user.effective_service_level.id
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/app/views/users/_contact_email.html.haml b/app/views/users/_contact_email.html.haml
index ad768b7..1627ff6 100644
--- a/app/views/users/_contact_email.html.haml
+++ b/app/views/users/_contact_email.html.haml
@@ -4,6 +4,6 @@
= f.input :contact_email, :label => false
-# %p= t(:public_key)
-# = f.text_area :contact_email_key, {:class => "full-width", :rows => 4}
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index da8e467..3bdf952 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,6 +1,6 @@
= render 'overview'
.container-fluid
- .row-fluid
+ .row
%h4 To use bitmask services:
= btn client_download_url, type: "primary" do
%i.icon-arrow-down.icon-white
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..66e9889
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..5191e69
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..1724048
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..acdb2c1
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/config/application.rb b/config/application.rb
index b13c7d9..0e00356 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -4,20 +4,16 @@ require File.expand_path('../boot', __FILE__)
# require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
-require "active_resource/railtie"
require "sprockets/railtie"
require "rails/test_unit/railtie"
-if defined?(Bundler)
- # If you precompile assets before deploying to production, use this line
- Bundler.require(*Rails.groups(:assets => %w(development test)))
- # If you want your assets lazily compiled in production, use this line
- # Bundler.require(:default, :assets, Rails.env)
-end
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
APP_CONFIG = ["defaults.yml", "config.yml"].inject({}) {|config, file|
filepath = File.expand_path(file, File.dirname(__FILE__))
- if File.exists?(filepath) && settings = YAML.load_file(filepath)[Rails.env]
+ if File.exist?(filepath) && settings = YAML.load_file(filepath)[Rails.env]
config.merge(settings)
else
config
@@ -30,16 +26,6 @@ module LeapWeb
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
- # Custom directories with classes and modules you want to be autoloadable.
- # config.autoload_paths += %W(#{config.root}/extras)
-
- # Only load the plugins named here, in the order given (default is alphabetical).
- # :all can be used as a placeholder for all plugins not explicitly named.
- # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
-
- # Activate observers that should always be running.
- # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
-
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
@@ -48,48 +34,18 @@ module LeapWeb
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
- # Configure the default encoding used in templates for Ruby 1.9.
- config.encoding = "utf-8"
-
- # Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters += [:password]
-
if APP_CONFIG[:logfile].present?
config.logger = Logger.new(APP_CONFIG[:logfile])
end
- # Enable escaping HTML in JSON.
- config.active_support.escape_html_entities_in_json = true
-
- # Use SQL instead of Active Record's schema dumper when creating the database.
- # This is necessary if your schema can't be completely dumped by the schema dumper,
- # like if you have constraints or database-specific column types
- # config.active_record.schema_format = :sql
-
- # Enforce whitelist mode for mass assignment.
- # This will create an empty whitelist of attributes available for mass-assignment for all models
- # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
- # parameters by using an attr_accessible or attr_protected declaration.
- # config.active_record.whitelist_attributes = true
-
- ##
- ## ASSETS
- ##
-
- # Enable the asset pipeline
- config.assets.enabled = true
- config.assets.initialize_on_precompile = true # don't change this (see customization.rb)
-
- # Version of your assets, change this if you want to expire all your assets
- config.assets.version = '1.0'
-
- # Set to false in order to see asset requests in the log
- config.quiet_assets = true
-
##
## CUSTOMIZATION
## see initializers/customization.rb
##
+
+ # don't change this (see customization.rb)
+ config.assets.initialize_on_precompile = true
+
if APP_CONFIG["customization_directory"]
custom_view_path = (Pathname.new(APP_CONFIG["customization_directory"]).relative_path_from(Rails.root) + 'views').to_s
else
diff --git a/config/boot.rb b/config/boot.rb
index 4489e58..6b750f0 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,6 +1,3 @@
-require 'rubygems'
-
-# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/defaults.yml b/config/defaults.yml
index 844adaa..7e2ea58 100644
--- a/config/defaults.yml
+++ b/config/defaults.yml
@@ -87,6 +87,7 @@ common: &common
soledad-service: 'public/1/config/soledad-service.json'
eip-service: 'public/1/config/eip-service.json'
smtp-service: 'public/1/config/smtp-service.json'
+ provider: 'config/provider/provider.json'
mailer:
from_address: 'noreply'
diff --git a/config/environment.rb b/config/environment.rb
index fe16a54..00a613f 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,5 @@
-# Load the rails application
+# Load the rails application.
require File.expand_path('../application', __FILE__)
-# Initialize the rails application
-LeapWeb::Application.initialize!
+# Initialize the rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3c0ff0f..4112c6d 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,38 +1,58 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ ##
+ # DEFAULT:
+ #
+ # rails default settings in their natural order
+ ##
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
- # Log error messages when you accidentally call methods on nil.
- config.whiny_nils = true
+ # Do not eager load code on boot.
+ config.eager_load = false
- # Show full error reports and disable caching
+ # Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
- # Don't care if the mailer can't send
+ # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
- # Print deprecation notices to the Rails logger
+ # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
- # Only use best-standards-support built into browsers
- config.action_dispatch.best_standards_support = :builtin
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
- # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found)
- config.i18n.fallbacks = true
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
- # Do not compress assets
- config.assets.compress = false
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
# If set to true, each asset file is loaded separately. If you are not
# debugging js or css, it is much faster to set this to false.
config.assets.debug = false
+ ##
+ # CUSTOM:
+ #
+ # our own settings
+ ##
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation can not be found)
+ config.i18n.fallbacks = true
+
# super hacky, but otherwise getting certificate error, and doesn't seem dangerous in development mode:
OpenSSL::SSL.send(:remove_const, "VERIFY_PEER")
OpenSSL::SSL.const_set("VERIFY_PEER", OpenSSL::SSL::VERIFY_NONE)
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 7acca75..acdfc1d 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,37 +1,56 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
- # Code is not reloaded between requests
+ # Code is not reloaded between requests.
config.cache_classes = true
- # Full error reports are disabled and caching is turned on
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
- # Disable Rails's static asset server (Apache or nginx will already do this)
- config.serve_static_assets = false
- # Compress JavaScripts and CSS
- config.assets.compress = true
+ # Dble Rack::Cache to put a simple HTTP cache in front of your application
+ # Add `rack-cache` to your Gemfile before enabling this.
+ # For large-scale production use, consider using a caching reverse proxy like
+ # NGINX, varnish or squid.
+ # config.action_dispatch.rack_cache = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
- # Don't fallback to assets pipeline if a precompiled asset is missed
+ #Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # Generate digests for assets URLs
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
config.assets.digest = true
- # Defaults to nil and saved in location specified by config.assets.prefix
- # config.assets.manifest = YOUR_PATH
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
- # Specifies the header that your server uses for sending files
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = APP_CONFIG[:force_ssl]
- # See everything in the log (default is :info)
- # config.log_level = :debug
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ # config.log_tags = [ :subdomain, :uuid ]
# Use syslog if no file has been specified
if APP_CONFIG[:logfile].blank?
@@ -39,26 +58,23 @@ LeapWeb::Application.configure do
config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new('webapp'))
end
- # Use a different cache store in production
+ # Use a different cache store in production.
# config.cache_store = :mem_cache_store
- # Enable serving of images, stylesheets, and JavaScripts from an asset server
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = "http://assets.example.com"
- # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
- # config.assets.precompile += %w( search.js )
-
- # Disable delivery errors, bad email addresses will be ignored
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
- # Enable threaded mode
- # config.threadsafe!
-
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found)
+ # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1a38df7..0ba4fbd 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,5 +1,7 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ config.assets.debug = true
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
@@ -7,29 +9,36 @@ LeapWeb::Application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
- # Configure static asset server for tests with Cache-Control for performance
- config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
- # Log error messages when you accidentally call methods on nil
- config.whiny_nils = true
+ # Configure static asset server for tests with Cache-Control for performance
+ config.serve_static_files = true
+ config.static_cache_control = 'public, max-age=3600'
- # Show full error reports and disable caching
+ # Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
- # Raise exceptions instead of rendering exception templates
+ # Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
- # Disable request forgery protection in test environment
- config.action_controller.allow_forgery_protection = false
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ # Randomize the order test cases are executed.
+ config.active_support.test_order = :random
- # Print deprecation notices to the stderr
+ # Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/config/initializers/client_side_validations.rb b/config/initializers/client_side_validations.rb
index 252aded..f7a658a 100644
--- a/config/initializers/client_side_validations.rb
+++ b/config/initializers/client_side_validations.rb
@@ -1,9 +1,16 @@
# ClientSideValidations Initializer
-# Uncomment to disable uniqueness validator, possible security issue
-ClientSideValidations::Config.disabled_validators = [:uniqueness]
+# Disabled validators. The uniqueness validator is disabled by default for security issues. Enable it on your own responsibility!
+# ClientSideValidations::Config.disabled_validators = [:uniqueness]
+
+# Uncomment to validate number format with current I18n locale
+# ClientSideValidations::Config.number_format_with_locale = true
# Uncomment the following block if you want each input field to have the validation messages attached.
+#
+# Note: client_side_validation requires the error to be encapsulated within
+# <label for="#{instance.send(:tag_id)}" class="message"></label>
+
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
unless html_tag =~ /^<label/
%{<div class="field_with_errors">#{html_tag}<label for="#{instance.send(:tag_id)}" class="message">#{instance.error_message.first}</label></div>}.html_safe
@@ -11,4 +18,3 @@ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
%{<div class="field_with_errors">#{html_tag}</div>}.html_safe
end
end
-
diff --git a/config/initializers/customization.rb b/config/initializers/customization.rb
index aaa2939..9f537e9 100644
--- a/config/initializers/customization.rb
+++ b/config/initializers/customization.rb
@@ -26,7 +26,7 @@ Rails.application.config.assets.paths.unshift "#{customization_directory}/styles
#
# Copy files to public
#
-if !defined?(RAKE) && Dir.exists?("#{customization_directory}/public")
+if !defined?(RAKE) && Dir.exist?("#{customization_directory}/public")
require 'fileutils'
FileUtils.cp_r("#{customization_directory}/public/.", "#{Rails.root}/public", :preserve => true)
end
diff --git a/config/initializers/error_constants.rb b/config/initializers/error_constants.rb
index fdd3624..bf2dad4 100644
--- a/config/initializers/error_constants.rb
+++ b/config/initializers/error_constants.rb
@@ -1,3 +1,8 @@
require 'ruby-srp'
WRONG_PASSWORD = SRP::WrongPassword
+
+# In case we use a different ORM at some point
+VALIDATION_FAILED = CouchRest::Model::Errors::Validations
+RECORD_NOT_FOUND = CouchRest::Model::DocumentNotFound
+RESOURCE_NOT_FOUDN = CouchRest::NotFound
diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb
index c949f5e..720174f 100644
--- a/config/initializers/simple_form_bootstrap.rb
+++ b/config/initializers/simple_form_bootstrap.rb
@@ -1,17 +1,17 @@
# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
- config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
+ config.wrappers :bootstrap, :tag => 'div', :class => 'form-group', :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
- b.use :label
- b.wrapper :tag => 'div', :class => 'controls' do |ba|
+ b.use :label, :class => 'col-sm-2'
+ b.wrapper :tag => 'div', :class => 'col-sm-10' do |ba|
ba.use :input
ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' }
end
end
- config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
+ config.wrappers :prepend, :tag => 'div', :class => "form-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
@@ -24,7 +24,7 @@ SimpleForm.setup do |config|
end
end
- config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
+ config.wrappers :append, :tag => 'div', :class => "form-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
@@ -38,7 +38,7 @@ SimpleForm.setup do |config|
end
#
- # when you don't want any bootstrap "control-group" or "controls" wrappers.
+ # when you don't want any bootstrap "form-group" or "controls" wrappers.
#
config.wrappers :none, :tag => 'div', :error_class => 'error' do |b|
b.use :html5
diff --git a/config/initializers/validations.rb b/config/initializers/validations.rb
deleted file mode 100644
index e8acfbe..0000000
--- a/config/initializers/validations.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# In case we use a different ORM at some point
-VALIDATION_FAILED = CouchRest::Model::Errors::Validations
-RECORD_NOT_FOUND = CouchRest::Model::DocumentNotFound
-RESOURCE_NOT_FOUND = RestClient::ResourceNotFound
diff --git a/config/locales/en/generic.en.yml b/config/locales/en/generic.en.yml
index be62a40..617116f 100644
--- a/config/locales/en/generic.en.yml
+++ b/config/locales/en/generic.en.yml
@@ -21,7 +21,7 @@ en:
example_email: 'user@domain.org'
- no_such_thing: "No such %{thing}."
+ no_such_user: "No such user."
create_thing: "Create %{thing}"
overview: "Overview"
diff --git a/config/routes.rb b/config/routes.rb
index e370aa4..b152c9c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,8 +10,8 @@ LeapWeb::Application.routes.draw do
# HTTP Error Handling
# instead of the default error pages use the errors controller and views
#
- match '/404' => 'errors#not_found'
- match '/500' => 'errors#server_error'
+ match '/404' => 'errors#not_found', via: [:get, :post]
+ match '/500' => 'errors#server_error', via: [:get, :post]
scope "(:locale)", :locale => CommonLanguages.match_available, :controller => 'pages', :action => 'show' do
get 'privacy-policy', :as => 'privacy_policy'
@@ -24,10 +24,10 @@ LeapWeb::Application.routes.draw do
get '/provider.json' => 'static_config#provider'
- namespace "api", { module: "v1",
- path: "/1/",
- defaults: {format: 'json'},
- :constraints => { :id => /[^\/]+(?=\.json\z)|[^\/]+/ }
+ namespace "api", { module: "api",
+ path: "/:version/",
+ defaults: {version: '2', format: 'json'},
+ :constraints => { :id => /[^\/]+(?=\.json\z)|[^\/]+/, :version => /[12]/ }
} do
resources :sessions, :only => [:new, :create, :update]
delete "logout" => "sessions#destroy", :as => "logout"
@@ -44,8 +44,8 @@ LeapWeb::Application.routes.draw do
get "login" => "sessions#new", :as => "login"
delete "logout" => "sessions#destroy", :as => "logout"
- get "signup" => "users#new", :as => "signup"
- resources :users, :except => [:create, :update] do
+ get "signup" => "account#new", :as => "signup"
+ resources :users, :except => [:new, :create, :update] do
# resource :email_settings, :only => [:edit, :update]
# resources :email_aliases, :only => [:destroy], :id => /.*/
post 'deactivate', on: :member
diff --git a/doc/DEPLOY.md b/doc/DEPLOY.md
index 33d5598..4d59701 100644
--- a/doc/DEPLOY.md
+++ b/doc/DEPLOY.md
@@ -1,5 +1,10 @@
# Deployment #
+LEAP Web is provisioned and run as part of the overall [LEAP platform](https://leap.se/en/docs/platform).
+We strongly recomment using the whole Platform and following its instructions.
+If you want to directly deploy the webapp never the less these instructions are
+for you.
+
These instructions are targeting a Debian GNU/Linux system. You might need to
change the commands to match your own needs.
@@ -10,9 +15,9 @@ change the commands to match your own needs.
The following packages need to be installed:
* git
-* ruby1.9
-* rubygems1.9
+* ruby (2.1.5)
* couchdb (if you want to use a local couch)
+* bundler
### Setup Capistrano ###
diff --git a/doc/DEVELOP.md b/doc/DEVELOP.md
index cdd0867..97ecd25 100644
--- a/doc/DEVELOP.md
+++ b/doc/DEVELOP.md
@@ -1,5 +1,19 @@
# Development #
+## Branches
+
+We use the 'master' branch to hold the version currently deployed to the
+production servers. Only hotfixes are applied here.
+
+Most of development happens based upon the 'develop' branch. So unless
+you are investigating a specific issue that occured in production you
+probably want to base your changes on 'develop':
+```
+git checkout origin/develop -b my-new-feature
+```
+This will create a new branch called 'my-new-feature' based on the develop
+branch from the origin remote.
+
## Setting up the local CouchDB
CouchDB operates in Admin Party by default, meaning there are no access
@@ -7,17 +21,24 @@ control checks. This is handy for local development. However, there is
the risk that running tests with Couch in Admin Party yields false
results.
-You have two options:
+We recommend keeping the default CouchDB configuration locally and testing
+the more complex setup with access control in Continuous Integration.
-1) Use Admin Party and accept the risk
-2) Stop Admin Party by creating user accounts & security docs by running the
-following script:
+Please see .travis.yml for the configuration of our CI runs.
- test/travis/setup_couch.sh
+In order to prepare you local couch for development run
+```
+bin/rake db:rotate
+bin/rake db:migrate
+```
-### Database configuration
+### Customized database configuration (advanced)
+
+If you want to stop Admin Party mode you need to create user accounts &
+security docs. You can use the following script as a guideline:
+ test/travis/setup_couch.sh
-Copy & adapt the default database configuration:
+Afterwards copy & adapt the default database configuration:
```
mv config/couchdb.example.yml config/couchdb.yml
@@ -37,14 +58,14 @@ Some tips on modifying the views:
## Engines ##
-Leap Web contains some. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory.
+We use engines to separate optional functionality from the core. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory.
Currently Leap Web includes 2 Engines:
* [support](https://github.com/leapcode/leap_web/blob/master/engines/support) - Help ticket management
* [billing](https://github.com/leapcode/leap_web/blob/master/engines/billing) - Billing System
-## Creating a new engine ##
+## Creating a new engine (advanced) ##
If you want to add functionality to the webapp but keep it easy to remove you might consider adding an engine. This only makes sense if your engine really is a plugin - so no other pieces of code depend on it.
@@ -99,7 +120,7 @@ For example:
visit robot_path(@robot, :locale => nil)
end
-## Debugging
+## Debugging Production (advanced)
Sometimes bugs only show up when deployed to the live production server. Debugging can be tricky,
because the open source mod_passenger does not support debugger. You can't just run
diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md
index f3db006..0e2957d 100644
--- a/doc/TROUBLESHOOT.md
+++ b/doc/TROUBLESHOOT.md
@@ -13,15 +13,19 @@ Here are some less common issues you might run into when installing Leap Web.
Make sure bundler is installed. `gem list bundler` should list `bundler`.
You also need to be able to access the `bundler` executable in your PATH.
-## Outdated version of rubygems ##
+## Incompatible ruby version ##
-### Error Messages ###
+### Detecting the problem ###
+The rubyversion we use for development and testing is noted in the file
+
+ .ruby-version
-`bundler requires rubygems >= 1.3.6`
+It should match what `ruby --version` prints.
### Solution ###
-`gem update --system` will install the latest rubygems
+Install the matching ruby version. For some operation systems this may require
+the use of rbenv or rvm.
## Missing development tools ##
@@ -42,5 +46,7 @@ Some gem dependencies might not compile because they lack the needed c libraries
### Solution ###
Install the libraries in question including their development files.
+Usually the missing library is mentioned in the error message. Searching the
+internet for similar errors is a good starting point aswell.
diff --git a/engines/billing/config/routes.rb b/engines/billing/config/routes.rb
index 357c55b..12a6778 100644
--- a/engines/billing/config/routes.rb
+++ b/engines/billing/config/routes.rb
@@ -2,26 +2,23 @@ Rails.application.routes.draw do
scope "(:locale)", :locale => CommonLanguages.match_available do
- get 'payments/new' => 'payments#new', :as => :new_payment
- post 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
- # match 'payments/new' => 'payments#new', :as => :new_payment
- # match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
- #resources :users do
- # resources :payments, :only => [:new, :confirm]
- # resources :subscriptions, :only => [:index, :destroy]
- #end
- resources :subscriptions, :only => [:index, :show] do
- member do
- post 'subscribe'
- delete 'unsubscribe'
+ get 'payments/new' => 'payments#new', :as => :new_payment
+ post 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ # match 'payments/new' => 'payments#new', :as => :new_payment
+ # match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ #resources :users do
+ # resources :payments, :only => [:new, :confirm]
+ # resources :subscriptions, :only => [:index, :destroy]
+ #end
+ resources :subscriptions, :only => [:index, :show] do
+ member do
+ post 'subscribe'
+ delete 'unsubscribe'
+ end
end
- end
-
- resources :customer, :only => [:new, :edit]
- match 'customer/confirm/' => 'customer#confirm', :as => :confirm_customer
- match 'customer/show/:id' => 'customer#show', :as => :show_customer
+ resources :customer, :only => [:new, :edit]
- match 'billing_admin' => 'billing_admin#show', :as => :billing_admin
+ get 'billing_admin' => 'billing_admin#show'
end
end
diff --git a/engines/support/app/helpers/auto_tickets_path_helper.rb b/engines/support/app/helpers/auto_tickets_path_helper.rb
index bc98a0a..17fc003 100644
--- a/engines/support/app/helpers/auto_tickets_path_helper.rb
+++ b/engines/support/app/helpers/auto_tickets_path_helper.rb
@@ -4,7 +4,8 @@
# (1) include the user in the path if appropriate.
# (2) retain the sort params, if appropriate.
#
-# Tickets views with a user_id are limited to that user. For admins, they don't need a user_id for any ticket action.
+# Tickets views with a user_id are limited to that user.
+# Admins don't need a user_id for any ticket action.
#
# This is available both to the views and the tickets_controller.
#
@@ -13,7 +14,6 @@ module AutoTicketsPathHelper
protected
def auto_tickets_path(options={})
- return unless options.class == Hash
options = ticket_view_options.merge options
if @user.is_a? User
user_tickets_path(@user, options)
@@ -22,9 +22,9 @@ module AutoTicketsPathHelper
end
end
- def auto_ticket_path(ticket, options={})
+ def auto_ticket_path(ticket)
return unless ticket.persisted?
- options = ticket_view_options.merge options
+ options = ticket_view_options
if @user.is_a? User
user_ticket_path(@user, ticket, options)
else
@@ -32,10 +32,9 @@ module AutoTicketsPathHelper
end
end
- def auto_new_ticket_path(options={})
- return unless options.class == Hash
- options = ticket_view_options.merge options
- if @user
+ def auto_new_ticket_path
+ options = ticket_view_options
+ if @user.is_a? User
new_user_ticket_path(@user, options)
else
new_ticket_path(options)
diff --git a/engines/support/app/models/account_extension/tickets.rb b/engines/support/app/models/account_extension/tickets.rb
deleted file mode 100644
index f38d5fd..0000000
--- a/engines/support/app/models/account_extension/tickets.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module AccountExtension::Tickets
- extend ActiveSupport::Concern
-
- def destroy_with_tickets(destroy_identities=false)
- Ticket.destroy_all_from(self.user)
- destroy_without_tickets(destroy_identities)
- end
-
- included do
- alias_method_chain :destroy, :tickets
- end
-
-end
diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb
index b1bdf8d..025e2ab 100644
--- a/engines/support/app/models/ticket.rb
+++ b/engines/support/app/models/ticket.rb
@@ -37,9 +37,11 @@ class Ticket < CouchRest::Model::Base
# email can be nil, "", or valid address.
# validation provided by 'valid_email' gem.
+ # mx validation depends on network availability and is disabled in test
+ # and development environment
validates :email, :allow_blank => true,
:email => true,
- :mx_with_fallback => true
+ :mx_with_fallback => Rails.env.production?
def self.search(options = {})
@selection = TicketSelection.new(options)
@@ -50,7 +52,7 @@ class Ticket < CouchRest::Model::Base
self.by_created_by.key(user.id).each do |ticket|
ticket.destroy
end
- rescue RestClient::ResourceNotFound
+ rescue RESOURCE_NOT_FOUND
# silently ignore if design docs are not yet created
end
diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml
index cd1dbe4..8d64256 100644
--- a/engines/support/app/views/tickets/_edit_form.html.haml
+++ b/engines/support/app/views/tickets/_edit_form.html.haml
@@ -34,12 +34,12 @@
= simple_form_for @ticket do |f|
= hidden_ticket_fields
= f.input :subject, input_html: {:class => 'large full-width'}
- .row-fluid
- .span4
+ .row
+ .col-md-4
= f.input :is_open, as: :select, collection: [:true, :false], include_blank: false
- .span4
+ .col-md-4
= f.input :email
- .span4
+ .col-md-4
= f.input :regarding_user, label: (Ticket.human_attribute_name(:regarding_user) + " " + regarding_user_link).html_safe
= f.button :loading
- if admin?
diff --git a/engines/support/app/views/tickets/edit.html.haml b/engines/support/app/views/tickets/edit.html.haml
index 03bda7d..cdc5e16 100644
--- a/engines/support/app/views/tickets/edit.html.haml
+++ b/engines/support/app/views/tickets/edit.html.haml
@@ -1,4 +1,3 @@
-- @show_navigation = params[:user_id].present?
- @comment = TicketComment.new
.ticket
diff --git a/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml
index d107ce2..56c7012 100644
--- a/engines/support/app/views/tickets/index.html.haml
+++ b/engines/support/app/views/tickets/index.html.haml
@@ -1,5 +1,3 @@
-- @show_navigation = params[:user_id].present?
-
= render 'tickets/tabs'
= table @tickets, %w(subject created updated voices)
= paginate @tickets
diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml
index d3580f9..d0b0f89 100644
--- a/engines/support/app/views/tickets/new.html.haml
+++ b/engines/support/app/views/tickets/new.html.haml
@@ -1,10 +1,5 @@
-- @show_navigation = params[:user_id].present?
-
= render 'tickets/tabs'
-- user = @user if admin?
-- user ||= current_user
-
= simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f|
= hidden_ticket_fields
= f.input :subject
diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml
index 99afa2a..a625870 100644
--- a/engines/support/app/views/tickets/show.html.haml
+++ b/engines/support/app/views/tickets/show.html.haml
@@ -1,5 +1,3 @@
-- @show_navigation = params[:user_id].present?
-
.ticket
= render 'tickets/edit_form'
= render 'tickets/comments'
diff --git a/engines/support/config/initializers/account_lifecycle.rb b/engines/support/config/initializers/account_lifecycle.rb
index d9f04c1..9060757 100644
--- a/engines/support/config/initializers/account_lifecycle.rb
+++ b/engines/support/config/initializers/account_lifecycle.rb
@@ -1,3 +1,5 @@
+require 'account_extension/tickets'
+
ActiveSupport.on_load(:account) do
include AccountExtension::Tickets
end
diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml
index 83af2c4..3b509d5 100644
--- a/engines/support/config/locales/en.yml
+++ b/engines/support/config/locales/en.yml
@@ -92,7 +92,7 @@ en:
# mouse over hints for the given fields
hints:
ticket:
- email: "Provide an email address in order to notified when this ticket is updated."
+ email: "Provide an email address in order to be notified when this ticket is updated."
# these will fallback to translations in "simple_form.hints.defaults"
# placeholders inside the fields before anything was typed
#placeholders:
diff --git a/engines/support/config/routes.rb b/engines/support/config/routes.rb
index 81bdf9a..5647477 100644
--- a/engines/support/config/routes.rb
+++ b/engines/support/config/routes.rb
@@ -3,8 +3,8 @@ Rails.application.routes.draw do
resources :tickets, except: :edit do
member do
- put 'open'
- put 'close'
+ patch 'open'
+ patch 'close'
end
end
diff --git a/engines/support/lib/account_extension/tickets.rb b/engines/support/lib/account_extension/tickets.rb
new file mode 100644
index 0000000..63f4873
--- /dev/null
+++ b/engines/support/lib/account_extension/tickets.rb
@@ -0,0 +1,15 @@
+module AccountExtension
+ module Tickets
+ extend ActiveSupport::Concern
+
+ def destroy_with_tickets(destroy_identities=false)
+ Ticket.destroy_all_from(self.user)
+ destroy_without_tickets(destroy_identities)
+ end
+
+ included do
+ alias_method_chain :destroy, :tickets
+ end
+
+ end
+end
diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb
index a7a2011..5c2b346 100644
--- a/engines/support/test/functional/tickets_controller_test.rb
+++ b/engines/support/test/functional/tickets_controller_test.rb
@@ -35,6 +35,12 @@ class TicketsControllerTest < ActionController::TestCase
assert_response :success
end
+ test "should get new despite invalid user_id" do
+ get :new, user_id: :bla
+ assert_equal Ticket, assigns(:ticket).class
+ assert_response :success
+ end
+
test "unauthenticated tickets are visible" do
ticket = find_record :ticket, :created_by => nil
get :show, :id => ticket.id
diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb
index 00f9a6b..6abb3d3 100644
--- a/engines/support/test/integration/create_ticket_test.rb
+++ b/engines/support/test/integration/create_ticket_test.rb
@@ -29,7 +29,7 @@ class CreateTicketTest < BrowserIntegrationTest
fill_in 'Description', with: 'description of the problem goes here'
click_on 'Submit Ticket'
assert page.has_content?("is invalid")
- assert_equal 'invalid data', find_field('Email').value
+ assert_equal 'invaliddata', find_field('Email').value
assert_equal 'some user', find_field('Regarding User').value
end
diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb
index 7b5281f..373f06c 100644
--- a/engines/support/test/unit/ticket_test.rb
+++ b/engines/support/test/unit/ticket_test.rb
@@ -8,12 +8,12 @@ class TicketTest < ActiveSupport::TestCase
test "ticket with default attribs is valid" do
t = FactoryGirl.build :ticket
- assert t.valid?
+ assert t.valid?, t.errors.full_messages.to_sentence
end
test "ticket without email is valid" do
t = FactoryGirl.build :ticket, email: ""
- assert t.valid?
+ assert t.valid?, t.errors.full_messages.to_sentence
end
test "ticket validates email format" do
@@ -63,7 +63,7 @@ class TicketTest < ActiveSupport::TestCase
test "find tickets user commented on" do
# clear old tickets just in case
- # this will cause RestClient::ResourceNotFound errors if there are multiple copies of the same ticket returned
+ # this will cause RESOURCE_NOT_FOUND errors if there are multiple copies of the same ticket returned
Ticket.by_includes_post_by.key('123').each {|t| t.destroy}
# TODO: the by_includes_post_by view is only used for tests. Maybe we should get rid of it and change the test to including ordering?
diff --git a/features/1/anonymous.feature b/features/1/anonymous.feature
new file mode 100644
index 0000000..73a6d3f
--- /dev/null
+++ b/features/1/anonymous.feature
@@ -0,0 +1,34 @@
+@config
+Feature: Anonymous access to EIP
+
+ A provider may choose to allow anonymous access to EIP.
+ In this case some endpoints that would normally require authentication
+ will be available without authentication.
+
+ Background:
+ Given "allow_anonymous_certs" is enabled in the config
+ And I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+
+ Scenario: Fetch configs when anonymous certs are allowed
+ When I send a GET request to "/1/configs.json"
+ Then the response status should be "200"
+
+ Scenario: Fetch EIP config when anonymous certs are allowed
+ Given there is a config for the eip
+ When I send a GET request to "/1/configs/eip-service.json"
+ Then the response status should be "200"
+
+ Scenario: Fetch service description
+ When I send a GET request to "/1/service.json"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "name": "anonymous",
+ "description": "anonymous access to the VPN",
+ "eip_rate_limit": false
+ }
+ """
+
diff --git a/features/1/authentication.feature b/features/1/authentication.feature
new file mode 100644
index 0000000..52b562f
--- /dev/null
+++ b/features/1/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/1/config.feature b/features/1/config.feature
new file mode 100644
index 0000000..ff04e9d
--- /dev/null
+++ b/features/1/config.feature
@@ -0,0 +1,58 @@
+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 there is a config for the provider
+ When I send a GET request to "/provider.json"
+ Then the response status should be "200"
+ And the response should be that config
+
+ 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"
+ }
+ }
+ """
+
+ Scenario: Attempt to fetch an invalid config
+ When I send a GET request to "/1/configs/non-existing.json"
+ Then the response status should be "403"
+
+ # I am not sure what this test is about, that config is not
+ # actually missing.
+ #Scenario: Attempt to fetch a config that is missing on the server
+ # When I send a GET request to "/1/configs/eip-service.json"
+ # Then the response status should be "404"
+
+ @tempfile, @config
+ Scenario: Attempt to fetch the EIP config
+ Given there is a config for the eip
+ When I send a GET request to "/1/configs/eip-service.json"
+ Then the response status should be "200"
+ And the response should be that config
+
diff --git a/features/1/service.feature b/features/1/service.feature
new file mode 100644
index 0000000..ea49c74
--- /dev/null
+++ b/features/1/service.feature
@@ -0,0 +1,33 @@
+Feature: Get service description for current user
+
+ The LEAP provider can offer different services and their availability may
+ depend upon a users service level - so wether they are paying or not.
+
+ The /1/service endpoint allows the client to find out about the services
+ available to the authenticated user.
+
+ Background:
+ Given I authenticated
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+ | Authorization | Token token="MY_AUTH_TOKEN" |
+
+ Scenario: Get service settings
+ When I send a GET request to "/1/service"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "name": "free",
+ "description": "free account, with rate limited VPN",
+ "eip_rate_limit": true,
+ "storage": 100,
+ "services": [
+ "eip"
+ ]
+ }
+ """
+
+
+
diff --git a/features/1/unauthenticated.feature b/features/1/unauthenticated.feature
new file mode 100644
index 0000000..aea7117
--- /dev/null
+++ b/features/1/unauthenticated.feature
@@ -0,0 +1,31 @@
+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 there is a config for the provider
+ When I send a GET request to "/provider.json"
+ Then the response status should be "200"
+ And the response should be that config
+
+ Scenario: Authentication required response
+ 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"
+
+ Scenario: Authentication required for all other API endpoints (incomplete)
+ Given I am not logged in
+ When I send requests to these endpoints:
+ | GET | /1/configs |
+ | GET | /1/configs/config_id.json |
+ | GET | /1/service |
+ | DELETE | /1/logout |
+ Then they should require authentication
diff --git a/features/anonymous.feature b/features/anonymous.feature
index 73a6d3f..d6b3ce2 100644
--- a/features/anonymous.feature
+++ b/features/anonymous.feature
@@ -5,23 +5,23 @@ Feature: Anonymous access to EIP
In this case some endpoints that would normally require authentication
will be available without authentication.
- Background:
+ Background:
Given "allow_anonymous_certs" is enabled in the config
And I set headers:
| Accept | application/json |
| Content-Type | application/json |
Scenario: Fetch configs when anonymous certs are allowed
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "200"
Scenario: Fetch EIP config when anonymous certs are allowed
Given there is a config for the eip
- When I send a GET request to "/1/configs/eip-service.json"
+ When I send a GET request to "/2/configs/eip-service.json"
Then the response status should be "200"
Scenario: Fetch service description
- When I send a GET request to "/1/service.json"
+ When I send a GET request to "/2/service.json"
Then the response status should be "200"
And the response should be:
"""
diff --git a/features/authentication.feature b/features/authentication.feature
index 52b562f..806e2b7 100644
--- a/features/authentication.feature
+++ b/features/authentication.feature
@@ -13,12 +13,12 @@ Feature: Authentication
Given I authenticated
And I set headers:
| Authorization | Token token="MY_AUTH_TOKEN" |
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/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"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "401"
diff --git a/features/config.feature b/features/config.feature
index ff04e9d..bd627de 100644
--- a/features/config.feature
+++ b/features/config.feature
@@ -4,7 +4,7 @@ Feature: Download Provider Configuration
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`.
+ More detailed settings of the services are available after authentication. You can get a list of the available settings from `/2/configs.json`.
Background:
Given I authenticated
@@ -26,33 +26,33 @@ Feature: Download Provider Configuration
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"
+ When I send a GET request to "/2/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"
+ "soledad": "/2/configs/soledad-service.json",
+ "eip": "/2/configs/eip-service.json",
+ "smtp": "/2/configs/smtp-service.json"
}
}
"""
Scenario: Attempt to fetch an invalid config
- When I send a GET request to "/1/configs/non-existing.json"
+ When I send a GET request to "/2/configs/non-existing.json"
Then the response status should be "403"
# I am not sure what this test is about, that config is not
# actually missing.
#Scenario: Attempt to fetch a config that is missing on the server
- # When I send a GET request to "/1/configs/eip-service.json"
+ # When I send a GET request to "/2/configs/eip-service.json"
# Then the response status should be "404"
@tempfile, @config
Scenario: Attempt to fetch the EIP config
Given there is a config for the eip
- When I send a GET request to "/1/configs/eip-service.json"
+ When I send a GET request to "/2/configs/eip-service.json"
Then the response status should be "200"
And the response should be that config
diff --git a/features/service.feature b/features/service.feature
index ea49c74..6244f6c 100644
--- a/features/service.feature
+++ b/features/service.feature
@@ -3,7 +3,7 @@ Feature: Get service description for current user
The LEAP provider can offer different services and their availability may
depend upon a users service level - so wether they are paying or not.
- The /1/service endpoint allows the client to find out about the services
+ The /2/service endpoint allows the client to find out about the services
available to the authenticated user.
Background:
@@ -14,7 +14,7 @@ Feature: Get service description for current user
| Authorization | Token token="MY_AUTH_TOKEN" |
Scenario: Get service settings
- When I send a GET request to "/1/service"
+ When I send a GET request to "/2/service"
Then the response status should be "200"
And the response should be:
"""
diff --git a/features/step_definitions/config_steps.rb b/features/step_definitions/config_steps.rb
index 1fc67f5..a635d06 100644
--- a/features/step_definitions/config_steps.rb
+++ b/features/step_definitions/config_steps.rb
@@ -4,12 +4,9 @@ Given /there is a config for the (.*)$/ do |config|
@tempfile = Tempfile.new("#{config}.json")
@tempfile.write @dummy_config
@tempfile.close
- if config == 'provider'
- StaticConfigController::PROVIDER_JSON = @tempfile.path
- else
- @orig_config ||= APP_CONFIG.dup
- APP_CONFIG[:config_file_paths].merge! "#{config}-service" => @tempfile.path
- end
+ @orig_config ||= APP_CONFIG.dup
+ config = "#{config}-service" unless config == 'provider'
+ APP_CONFIG[:config_file_paths].merge! config => @tempfile.path
end
# use with @config
diff --git a/features/support/hooks.rb b/features/support/hooks.rb
index 256e5d8..4ddc77e 100644
--- a/features/support/hooks.rb
+++ b/features/support/hooks.rb
@@ -13,9 +13,9 @@ end
After do |scenario|
if scenario.failed?
logfile_path = Rails.root + 'tmp'
- logfile_path += "#{scenario.title.gsub(/\s/, '_')}.log"
+ logfile_path += "#{scenario.name.gsub(/\s/, '_')}.log"
File.open(logfile_path, 'w') do |test_log|
- test_log.puts scenario.title
+ test_log.puts scenario.name
test_log.puts "========================="
test_log.puts `tail log/test.log -n 200`
end
diff --git a/features/unauthenticated.feature b/features/unauthenticated.feature
index aea7117..b4b0f55 100644
--- a/features/unauthenticated.feature
+++ b/features/unauthenticated.feature
@@ -16,7 +16,7 @@ Feature: Unauthenticated API endpoints
And the response should be that config
Scenario: Authentication required response
- When I send a GET request to "/1/configs"
+ When I send a GET request to "/2/configs"
Then the response status should be "401"
And the response should have "error" with "not_authorized_login"
And the response should have "message"
@@ -24,8 +24,8 @@ Feature: Unauthenticated API endpoints
Scenario: Authentication required for all other API endpoints (incomplete)
Given I am not logged in
When I send requests to these endpoints:
- | GET | /1/configs |
- | GET | /1/configs/config_id.json |
- | GET | /1/service |
- | DELETE | /1/logout |
+ | GET | /2/configs |
+ | GET | /2/configs/config_id.json |
+ | GET | /2/service |
+ | DELETE | /2/logout |
Then they should require authentication
diff --git a/app/models/email.rb b/lib/email.rb
index 4090275..4090275 100644
--- a/app/models/email.rb
+++ b/lib/email.rb
diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb
index cc041e0..4578926 100644
--- a/lib/extensions/couchrest.rb
+++ b/lib/extensions/couchrest.rb
@@ -69,7 +69,7 @@ module CouchRest
def prepare_directory(dir = '')
dir = Rails.root + 'tmp' + 'designs' + dir
- Dir.mkdir(dir) unless Dir.exists?(dir)
+ Dir.mkdir(dir) unless Dir.exist?(dir)
return dir
end
@@ -81,7 +81,7 @@ module CouchRest
class ModelRailtie
config.action_dispatch.rescue_responses.merge!(
'CouchRest::Model::DocumentNotFound' => :not_found,
- 'RestClient::ResourceNotFound' => :not_found
+ 'CouchRest::NotFound' => :not_found
)
end
end
diff --git a/lib/gemfile_tools.rb b/lib/gemfile_tools.rb
index dce2448..d1c00dc 100644
--- a/lib/gemfile_tools.rb
+++ b/lib/gemfile_tools.rb
@@ -68,7 +68,7 @@ def local_config
empty_hash.default_proc = proc{|h, k| h.key?(k.to_s) ? h[k.to_s] : nil}
["defaults.yml", "config.yml"].inject(empty_hash.dup) {|config, file|
filepath = File.join(File.expand_path("../../config", __FILE__), file)
- if File.exists?(filepath)
+ if File.exist?(filepath)
new_config = YAML.load_file(filepath)
['development', 'test','production'].each do |env|
config[env] ||= empty_hash.dup
@@ -87,7 +87,7 @@ end
# or nil if not actually a gem directory
#
def gem_info(gem_dir)
- if Dir.exists?(gem_dir)
+ if Dir.exist?(gem_dir)
gemspec = Dir["#{gem_dir}/*.gemspec"]
if gemspec.any?
gem_name = File.basename(gemspec.first).sub(/\.gemspec$/,'')
diff --git a/app/models/local_email.rb b/lib/local_email.rb
index ded7baf..7c592e1 100644
--- a/app/models/local_email.rb
+++ b/lib/local_email.rb
@@ -1,3 +1,4 @@
+require 'email'
class LocalEmail < Email
BLACKLIST_FROM_RFC2142 = [
diff --git a/app/models/login_format_validation.rb b/lib/login_format_validation.rb
index c1fcf70..c1fcf70 100644
--- a/app/models/login_format_validation.rb
+++ b/lib/login_format_validation.rb
diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake
index 6ffbb23..1034211 100644
--- a/lib/tasks/i18n.rake
+++ b/lib/tasks/i18n.rake
@@ -63,7 +63,7 @@ namespace :i18n do
desc "pull translations from transifex"
task :download do
Dir.chdir('config/') do
- if !File.exists?('transifex.netrc')
+ if !File.exist?('transifex.netrc')
puts "In order to download translations, you need a config/transifex.netrc file."
puts "For example:"
puts "machine www.transifex.com login yourusername password yourpassword"
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d96b625..9859729 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -1,10 +1,15 @@
namespace :test do
- [:units, :functionals, :integration].each do |type|
- Rails::SubTestTask.new(type => "test:prepare") do |t|
- t.libs << "test"
- subdir = type.to_s.singularize
- t.pattern = "engines/*/test/#{subdir}/**/*_test.rb"
+ namespace :engines do
+ [:units, :functionals, :integration].each do |type|
+ desc "Test engine #{type}"
+ Rails::TestTask.new(type => "test:prepare") do |t|
+ t.libs << "test"
+ subdir = type.to_s.singularize
+ t.pattern = "engines/*/test/#{subdir}/**/*_test.rb"
+ end
+ Rake::Task["test:#{type}"].enhance ["test:engines:#{type}"]
+ Rake::Task["test"].enhance ["test:engines:#{type}"]
end
end
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
new file mode 100644
index 0000000..f5f1446
--- /dev/null
+++ b/test/functional/account_controller_test.rb
@@ -0,0 +1,26 @@
+require 'test_helper'
+
+class AccountControllerTest < ActionController::TestCase
+
+ test "should get new" do
+ get :new
+ assert_equal User, assigns(:user).class
+ assert_response :success
+ end
+
+ test "new should redirect logged in users" do
+ login
+ get :new
+ assert_response :redirect
+ assert_redirected_to home_path
+ end
+
+ test "new redirects if registration is closed" do
+ with_config(allow_registration: false) do
+ get :new
+ assert_response :redirect
+ assert_redirected_to home_path
+ end
+ end
+end
+
diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/api/certs_controller_test.rb
index 04c1c86..f23b4c8 100644
--- a/test/functional/v1/certs_controller_test.rb
+++ b/test/functional/api/certs_controller_test.rb
@@ -1,11 +1,11 @@
-require_relative '../../test_helper'
+require 'test_helper'
-class V1::CertsControllerTest < ActionController::TestCase
+class Api::CertsControllerTest < ApiControllerTest
test "create unlimited cert without login" do
with_config allow_anonymous_certs: true do
cert = expect_cert('UNLIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -15,7 +15,7 @@ class V1::CertsControllerTest < ActionController::TestCase
with_config allow_limited_certs: true do
login
cert = expect_cert('LIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -23,14 +23,14 @@ class V1::CertsControllerTest < ActionController::TestCase
test "fail to create cert when disabled" do
login :enabled? => false
- post :create
+ api_post :create
assert_access_denied
end
test "create unlimited cert" do
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert('UNLIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -38,13 +38,13 @@ class V1::CertsControllerTest < ActionController::TestCase
test "GET still works as an alias" do
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert('UNLIMITED')
- get :show
+ api_get :show
assert_response :success
assert_equal cert.to_s, @response.body
end
test "redirect if no eip service offered" do
- post :create
+ api_post :create
assert_response :redirect
end
diff --git a/test/functional/v1/identities_controller_test.rb b/test/functional/api/identities_controller_test.rb
index 6410c44..57345c8 100644
--- a/test/functional/v1/identities_controller_test.rb
+++ b/test/functional/api/identities_controller_test.rb
@@ -1,15 +1,15 @@
require_relative '../../test_helper'
-class V1::IdentitiesControllerTest < ActionController::TestCase
+class Api::IdentitiesControllerTest < ApiControllerTest
test "api monitor can fetch identity" do
monitor_auth do
identity = FactoryGirl.create :identity
- get :show, :id => identity.address, :format => 'json'
+ api_get :show, :id => identity.address, :format => 'json'
assert_response :success
assert_equal identity, assigns(:identity)
- get :show, :id => "blahblahblah", :format => 'json'
+ api_get :show, :id => "blahblahblah", :format => 'json'
assert_response :not_found
end
end
@@ -17,7 +17,7 @@ class V1::IdentitiesControllerTest < ActionController::TestCase
test "anonymous cannot fetch identity" do
identity = FactoryGirl.create :identity
- get :show, :id => identity.address, :format => 'json'
+ api_get :show, :id => identity.address, :format => 'json'
assert_response :forbidden
end
diff --git a/test/functional/v1/messages_controller_test.rb b/test/functional/api/messages_controller_test.rb
index 67f34a1..e586980 100644
--- a/test/functional/v1/messages_controller_test.rb
+++ b/test/functional/api/messages_controller_test.rb
@@ -1,6 +1,6 @@
require 'test_helper'
-class V1::MessagesControllerTest < ActionController::TestCase
+class Api::MessagesControllerTest < ApiControllerTest
setup do
@user = FactoryGirl.build(:user)
@@ -13,9 +13,8 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get the motd" do
with_config("customization_directory" => Rails.root+'test/files') do
login @user
- get :index, :locale => 'es'
+ api_get :index, :locale => 'es'
body = JSON.parse(response.body)
- p body
message1 = "<p>\"This\" is a <strong>very</strong> fine message. <a href=\"https://bitmask.net\">https://bitmask.net</a></p>\n"
assert_equal 2, body.size, 'there should be two messages'
assert_equal message1, body.first["text"], 'first message text should match files/motd/1.en.md'
@@ -25,7 +24,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get localized motd" do
with_config("customization_directory" => Rails.root+'test/files') do
login @user
- get :index, :locale => 'de'
+ api_get :index, :locale => 'de'
body = JSON.parse(response.body)
message1 = "<p>Dies ist eine sehr feine Nachricht. <a href=\"https://bitmask.net\">https://bitmask.net</a></p>\n"
assert_equal message1, body.first["text"], 'first message text should match files/motd/1.de.md'
@@ -34,7 +33,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get empty motd" do
login @user
- get :index
+ api_get :index
assert_equal "[]", response.body, "motd response should be empty if no motd directory exists"
end
@@ -59,7 +58,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get messages for user" do
login @user
- get :index
+ api_get :index
assert response.body.include? @message.text
assert response.body.include? @message.id
end
@@ -79,7 +78,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
login @user
put :update, :id => @message.id
@message.reload
- get :index
+ api_get :index
assert !(response.body.include? @message.text)
assert !(response.body.include? @message.id)
end
@@ -92,7 +91,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
end
test "fails if not authenticated" do
- get :index, :format => :json
+ api_get :index, :format => :json
assert_login_required
end
=end
diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/api/services_controller_test.rb
index 039eb27..cb85edf 100644
--- a/test/functional/v1/services_controller_test.rb
+++ b/test/functional/api/services_controller_test.rb
@@ -1,16 +1,16 @@
require 'test_helper'
-class V1::ServicesControllerTest < ActionController::TestCase
+class Api::ServicesControllerTest < ApiControllerTest
test "anonymous user gets login required service info" do
- get :show, format: :json
+ api_get :show, format: :json
assert_json_response error: 'not_authorized_login',
message: 'Please log in to perform that action.'
end
test "anonymous user gets vpn service info" do
with_config allow_anonymous_certs: true do
- get :show, format: :json
+ api_get :show, format: :json
assert_json_response name: 'anonymous',
eip_rate_limit: false,
description: 'anonymous access to the VPN'
@@ -19,7 +19,7 @@ class V1::ServicesControllerTest < ActionController::TestCase
test "user can see their service info" do
login
- get :show, format: :json
+ api_get :show, format: :json
default_level = APP_CONFIG[:default_service_level]
assert_json_response APP_CONFIG[:service_levels][default_level]
end
diff --git a/test/functional/v1/sessions_controller_test.rb b/test/functional/api/sessions_controller_test.rb
index 8bb6acd..03a1ef9 100644
--- a/test/functional/v1/sessions_controller_test.rb
+++ b/test/functional/api/sessions_controller_test.rb
@@ -3,7 +3,7 @@ require 'test_helper'
# This is a simple controller unit test.
# We're stubbing out both warden and srp.
# There's an integration test testing the full rack stack and srp
-class V1::SessionsControllerTest < ActionController::TestCase
+class Api::SessionsControllerTest < ApiControllerTest
setup do
@request.env['HTTP_HOST'] = 'api.lvh.me'
@@ -12,7 +12,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
end
test "renders json" do
- get :new, :format => :json
+ api_get :new, :format => :json
assert_response :success
assert_json_error nil
end
@@ -22,7 +22,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
strategy = stub :message => {:field => :translate_me}
request.env['warden'].stubs(:winning_strategy).returns(strategy)
I18n.expects(:t).with(:translate_me).at_least_once.returns("translation stub")
- get :new, :format => :json
+ api_get :new, :format => :json
assert_response 422
assert_json_error :field => "translation stub"
end
@@ -33,7 +33,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
request.env['warden'].expects(:authenticate!)
# make sure we don't get a template missing error:
@controller.stubs(:render)
- post :create, :login => @user.login, 'A' => @client_hex
+ api_post :create, :login => @user.login, 'A' => @client_hex
end
test "should authenticate" do
@@ -42,7 +42,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
handshake = stub(:to_hash => {h: "ash"})
session[:handshake] = handshake
- post :update, :id => @user.login, :client_auth => @client_hex
+ api_post :update, :id => @user.login, :client_auth => @client_hex
assert_nil session[:handshake]
assert_response :success
@@ -55,7 +55,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
test "destroy should logout" do
login
expect_logout
- delete :destroy
+ api_delete :destroy
assert_response 204
end
diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/api/smtp_certs_controller_test.rb
index 1b03995..393f090 100644
--- a/test/functional/v1/smtp_certs_controller_test.rb
+++ b/test/functional/api/smtp_certs_controller_test.rb
@@ -1,17 +1,17 @@
require 'test_helper'
-class V1::SmtpCertsControllerTest < ActionController::TestCase
+class Api::SmtpCertsControllerTest < ApiControllerTest
test "no smtp cert without login" do
with_config allow_anonymous_certs: true do
- post :create
+ api_post :create
assert_login_required
end
end
test "require service level with email" do
login
- post :create
+ api_post :create
assert_access_denied
end
@@ -19,14 +19,14 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert(@current_user.email_address)
cert.expects(:fingerprint).returns('fingerprint')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
test "fail to create cert when disabled" do
login :enabled? => false
- post :create
+ api_post :create
assert_access_denied
end
diff --git a/test/functional/token_auth_test.rb b/test/functional/api/token_auth_test.rb
index 53d5fb3..c7f91c7 100644
--- a/test/functional/token_auth_test.rb
+++ b/test/functional/api/token_auth_test.rb
@@ -3,15 +3,15 @@
# via static configured tokens.
#
-require_relative '../test_helper'
+require 'test_helper'
-class TokenAuthTest < ActionController::TestCase
- tests V1::ConfigsController
+class Api::TokenAuthTest < ApiControllerTest
+ tests Api::ConfigsController
def test_login_via_api_token
with_config(:allow_anonymous_certs => false) do
monitor_auth do
- get :index
+ api_get :index
assert assigns(:token), 'should have authenticated via api token'
assert assigns(:token).is_a? ApiToken
assert @controller.send(:current_user).is_a? ApiMonitorUser
@@ -26,10 +26,10 @@ class TokenAuthTest < ActionController::TestCase
with_config(new_config) do
monitor_auth do
request.env['REMOTE_ADDR'] = "1.1.1.1"
- get :index
+ api_get :index
assert_nil assigns(:token), "should not be able to auth with api token when ip restriction doesn't allow it"
request.env['REMOTE_ADDR'] = allowed
- get :index
+ api_get :index
assert assigns(:token), "should have authenticated via api token"
end
end
diff --git a/test/functional/v1/users_controller_test.rb b/test/functional/api/users_controller_test.rb
index df59c4d..32afd0a 100644
--- a/test/functional/v1/users_controller_test.rb
+++ b/test/functional/api/users_controller_test.rb
@@ -1,6 +1,6 @@
-require_relative '../../test_helper'
+require 'test_helper'
-class V1::UsersControllerTest < ActionController::TestCase
+class Api::UsersControllerTest < ApiControllerTest
test "user can change settings" do
user = find_record :user
@@ -10,11 +10,11 @@ class V1::UsersControllerTest < ActionController::TestCase
Account.expects(:new).with(user).returns(account_settings)
login user
- put :update, :user => changed_attribs, :id => user.id, :format => :json
+ api_put :update, :user => changed_attribs, :id => user.id, :format => :json
assert_equal user, assigns[:user]
assert_response 204
- assert_equal " ", @response.body
+ assert @response.body.blank?, "Response should be blank"
end
test "admin can update user" do
@@ -25,7 +25,7 @@ class V1::UsersControllerTest < ActionController::TestCase
Account.expects(:new).with(user).returns(account_settings)
login :is_admin? => true
- put :update, :user => changed_attribs, :id => user.id, :format => :json
+ api_put :update, :user => changed_attribs, :id => user.id, :format => :json
assert_equal user, assigns[:user]
assert_response 204
@@ -34,7 +34,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "user cannot update other user" do
user = find_record :user
login
- put :update, id: user.id,
+ api_put :update, id: user.id,
user: record_attributes_for(:user_with_settings),
:format => :json
assert_access_denied
@@ -45,7 +45,7 @@ class V1::UsersControllerTest < ActionController::TestCase
user = User.new(user_attribs)
Account.expects(:create).with(user_attribs).returns(user)
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_nil session[:user_id]
assert_json_response user
@@ -59,7 +59,7 @@ class V1::UsersControllerTest < ActionController::TestCase
assert !user.valid?
Account.expects(:create).with(user_attribs).returns(user)
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_json_error user.errors.messages
assert_response 422
@@ -67,7 +67,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "admin can autocomplete users" do
login :is_admin? => true
- get :index, :query => 'a', :format => :json
+ api_get :index, :query => 'a', :format => :json
assert_response :success
assert assigns(:users)
@@ -76,7 +76,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "create returns forbidden if registration is closed" do
user_attribs = record_attributes_for :user
with_config(allow_registration: false) do
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :forbidden
end
end
@@ -84,20 +84,20 @@ class V1::UsersControllerTest < ActionController::TestCase
test "admin can show user" do
user = FactoryGirl.create :user
login :is_admin? => true
- get :show, :id => 0, :login => user.login, :format => :json
+ api_get :show, :id => 0, :login => user.login, :format => :json
assert_response :success
assert_json_response user
- get :show, :id => user.id, :format => :json
+ api_get :show, :id => user.id, :format => :json
assert_response :success
assert_json_response user
- get :show, :id => "0", :format => :json
+ api_get :show, :id => "0", :format => :json
assert_response :not_found
end
test "normal users cannot show user" do
user = find_record :user
login
- get :show, :id => 0, :login => user.login, :format => :json
+ api_get :show, :id => 0, :login => user.login, :format => :json
assert_access_denied
end
@@ -106,9 +106,9 @@ class V1::UsersControllerTest < ActionController::TestCase
with_config(allow_registration: false, invite_required: true) do
monitor_auth do
user_attribs = record_attributes_for :test_user
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :success
- delete :destroy, :id => assigns(:user).id, :format => :json
+ api_delete :destroy, :id => assigns(:user).id, :format => :json
assert_response :success
end
end
@@ -117,17 +117,17 @@ class V1::UsersControllerTest < ActionController::TestCase
test "api monitor auth cannot create normal users" do
monitor_auth do
user_attribs = record_attributes_for :user
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :forbidden
end
end
- test "api monitor auth cannot delete normal users" do
- post :create, :user => record_attributes_for(:user), :format => :json
+ test "api monitor auth cannot api_delete normal users" do
+ api_post :create, :user => record_attributes_for(:user), :format => :json
assert_response :success
normal_user_id = assigns(:user).id
monitor_auth do
- delete :destroy, :id => normal_user_id, :format => :json
+ api_delete :destroy, :id => normal_user_id, :format => :json
assert_response :forbidden
end
end
diff --git a/test/functional/static_config_controller_test.rb b/test/functional/static_config_controller_test.rb
index 9c2cfef..7027bf8 100644
--- a/test/functional/static_config_controller_test.rb
+++ b/test/functional/static_config_controller_test.rb
@@ -1,7 +1,7 @@
require 'test_helper'
# use minitest for stubbing, rather than bloated mocha
-require 'minitest/stub_const'
+require 'minitest/mock'
class StaticConfigControllerTest < ActionController::TestCase
@@ -9,7 +9,7 @@ class StaticConfigControllerTest < ActionController::TestCase
end
def test_provider_success
- StaticConfigController.stub_const(:PROVIDER_JSON, file_path('provider.json')) do
+ @controller.stub(:provider_json, file_path('provider.json')) do
get :provider, format: :json
assert_equal 'application/json', @response.content_type
assert_response :success
@@ -17,7 +17,7 @@ class StaticConfigControllerTest < ActionController::TestCase
end
def test_provider_not_modified
- StaticConfigController.stub_const(:PROVIDER_JSON, file_path('provider.json')) do
+ @controller.stub(:provider_json, file_path('provider.json')) do
request.env["HTTP_IF_MODIFIED_SINCE"] = File.mtime(file_path('provider.json')).rfc2822()
get :provider, format: :json
assert_response 304
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index 7b24098..2794422 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -1,20 +1,7 @@
-require_relative '../test_helper'
+require 'test_helper'
class UsersControllerTest < ActionController::TestCase
- test "should get new" do
- get :new
- assert_equal User, assigns(:user).class
- assert_response :success
- end
-
- test "new should redirect logged in users" do
- login
- get :new
- assert_response :redirect
- assert_redirected_to home_path
- end
-
test "failed show without login" do
user = find_record :user
get :show, :id => user.id
@@ -67,8 +54,8 @@ class UsersControllerTest < ActionController::TestCase
nonid = 'thisisnotanexistinguserid'
login :is_admin? => true
get :show, :id => nonid
+ assert_error_response :no_such_user
assert_response :redirect
- assert_equal({:alert => "No such user."}, flash.to_hash)
assert_redirected_to users_path
end
@@ -163,11 +150,4 @@ class UsersControllerTest < ActionController::TestCase
assert !assigns(:user).enabled?
end
- test "new redirects if registration is closed" do
- with_config(allow_registration: false) do
- get :new
- assert_response :redirect
- assert_redirected_to home_path
- end
- end
end
diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb
index 772901d..289d3c6 100644
--- a/test/integration/api/cert_test.rb
+++ b/test/integration/api/cert_test.rb
@@ -5,7 +5,7 @@ class CertTest < ApiIntegrationTest
test "retrieve eip cert" do
login
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -14,13 +14,13 @@ class CertTest < ApiIntegrationTest
end
test "fetching certs requires login by default" do
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_login_required
end
test "retrieve anonymous eip cert" do
with_config allow_anonymous_certs: true do
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -28,4 +28,9 @@ class CertTest < ApiIntegrationTest
assert_response_includes "END CERTIFICATE"
end
end
+
+ def cert_url
+ "/#{api_version}/cert"
+ end
+
end
diff --git a/test/integration/api/signup_test.rb b/test/integration/api/signup_test.rb
index 05a0abe..2e515c1 100644
--- a/test/integration/api/signup_test.rb
+++ b/test/integration/api/signup_test.rb
@@ -1,4 +1,4 @@
-require_relative '../../test_helper'
+require 'test_helper'
require_relative 'srp_test'
class SignupTest < SrpTest
diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb
index 681d509..53382c1 100644
--- a/test/integration/api/smtp_cert_test.rb
+++ b/test/integration/api/smtp_cert_test.rb
@@ -11,7 +11,7 @@ class SmtpCertTest < ApiIntegrationTest
test "retrieve smtp cert" do
@user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -22,7 +22,7 @@ class SmtpCertTest < ApiIntegrationTest
test "cert and key" do
@user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
cert = OpenSSL::X509::Certificate.new(get_response.body)
key = OpenSSL::PKey::RSA.new(get_response.body)
@@ -34,7 +34,7 @@ class SmtpCertTest < ApiIntegrationTest
test "fingerprint is stored with identity" do
@user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
cert = OpenSSL::X509::Certificate.new(get_response.body)
fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':')
@@ -48,14 +48,18 @@ class SmtpCertTest < ApiIntegrationTest
test "fetching smtp certs requires email account" do
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_access_denied
end
test "no anonymous smtp certs" do
with_config allow_anonymous_certs: true do
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_login_required
end
end
+
+ def smtp_cert_url
+ "/#{api_version}/smtp_cert"
+ end
end
diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb
index 463abcd..b9605f9 100644
--- a/test/integration/api/srp_test.rb
+++ b/test/integration/api/srp_test.rb
@@ -14,7 +14,7 @@ class SrpTest < RackTest
# this test wraps the api and implements the interface the ruby-srp client.
def handshake(login, aa)
- post "http://api.lvh.me:3000/1/sessions.json",
+ post api_url("sessions.json"),
:login => login,
'A' => aa,
:format => :json
@@ -27,7 +27,7 @@ class SrpTest < RackTest
end
def validate(m)
- put "http://api.lvh.me:3000/1/sessions/" + @login + '.json',
+ put api_url("sessions/#{@login}.json"),
:client_auth => m,
:format => :json
return JSON.parse(last_response.body)
@@ -39,7 +39,7 @@ class SrpTest < RackTest
def register_user(login = "integration_test", password = 'srp, verify me!', invite_code = @testcode.invite_code)
cleanup_user(login)
- post 'http://api.lvh.me:3000/1/users.json',
+ post api_url('users.json'),
user_params(login: login, password: password, invite_code: invite_code)
assert(@user = User.find_by_login(login), 'user should have been created: %s' % last_response_errors)
@login = login
@@ -47,7 +47,7 @@ class SrpTest < RackTest
end
def update_user(params)
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json',
+ put api_url("users/#{@user.id}.json"),
user_params(params),
auth_headers
end
@@ -68,7 +68,7 @@ class SrpTest < RackTest
end
def logout(params=nil, headers=nil)
- delete "http://api.lvh.me:3000/1/logout.json",
+ delete api_url("logout.json"),
params || {format: :json},
headers || auth_headers
end
@@ -112,4 +112,12 @@ class SrpTest < RackTest
rescue
""
end
+
+ def api_url(path)
+ "http://api.lvh.me:3000/#{api_version}/#{path}"
+ end
+
+ def api_version
+ 2
+ end
end
diff --git a/test/integration/api/token_test.rb b/test/integration/api/token_auth_test.rb
index dafbfb7..7b20b00 100644
--- a/test/integration/api/token_test.rb
+++ b/test/integration/api/token_auth_test.rb
@@ -1,7 +1,7 @@
-require_relative '../../test_helper'
+require 'test_helper'
require_relative 'srp_test'
-class TokenTest < SrpTest
+class TokenAuthTest < SrpTest
setup do
register_user
diff --git a/test/integration/api/update_account_test.rb b/test/integration/api/update_account_test.rb
index 16bbb8c..1492006 100644
--- a/test/integration/api/update_account_test.rb
+++ b/test/integration/api/update_account_test.rb
@@ -14,7 +14,7 @@ class UpdateAccountTest < SrpTest
test "require token" do
authenticate
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json',
+ put "http://api.lvh.me:3000/2/users/" + @user.id + '.json',
user_params(password: "No! Verify me instead.")
assert_login_required
end
diff --git a/test/integration/browser/account_livecycle_test.rb b/test/integration/browser/account_livecycle_test.rb
new file mode 100644
index 0000000..85dbf13
--- /dev/null
+++ b/test/integration/browser/account_livecycle_test.rb
@@ -0,0 +1,114 @@
+require 'test_helper'
+
+class AccountLivecycleTest < BrowserIntegrationTest
+
+ teardown do
+ Identity.destroy_all_orphaned
+ end
+
+ test "signup successfully when invited" do
+ username, password = submit_signup
+ assert page.has_content?("Welcome #{username}")
+ click_on 'Log Out'
+ assert page.has_content?("Log In")
+ assert_equal '/', current_path
+ assert user = User.find_by_login(username)
+ user.account.destroy
+ end
+
+ test "signup successfully without invitation" do
+ with_config invite_required: false do
+
+ username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
+ password ||= SecureRandom.base64
+
+ visit '/signup'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ fill_in 'Password confirmation', with: password
+ click_on 'Sign Up'
+
+ assert page.has_content?("Welcome #{username}")
+ end
+ end
+
+ test "signup with username ending in dot json" do
+ username = Faker::Internet.user_name + '.json'
+ submit_signup username
+ assert page.has_content?("Welcome #{username}")
+ end
+
+ test "signup with reserved username" do
+ username = 'certmaster'
+ submit_signup username
+ assert page.has_content?("is reserved.")
+ end
+
+ test "successful login" do
+ username, password = submit_signup
+ click_on 'Log Out'
+ attempt_login(username, password)
+ assert page.has_content?("Welcome #{username}")
+ within('.sidenav li.active') do
+ assert page.has_content?("Overview")
+ end
+ User.find_by_login(username).account.destroy
+ end
+
+ test "failed login" do
+ visit '/'
+ attempt_login("username", "wrong password")
+ assert_invalid_login(page)
+ end
+
+ test "account destruction" do
+ username, password = submit_signup
+
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ assert page.has_content?(I18n.t('account_destroyed'))
+ assert_equal 1, Identity.by_address.key("#{username}@test.me").count
+ attempt_login(username, password)
+ assert_invalid_login(page)
+ end
+
+ test "handle blocked after account destruction" do
+ username, password = submit_signup
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ submit_signup(username)
+ assert page.has_content?('has already been taken')
+ end
+
+ test "change pgp key" do
+ with_config user_actions: ['change_pgp_key'] do
+ pgp_key = FactoryGirl.build :pgp_key
+ login
+ click_on "Account Settings"
+ within('#update_pgp_key') do
+ fill_in 'Public key', with: pgp_key
+ click_on 'Save'
+ end
+ page.assert_selector 'input[value="Saving..."]'
+ # at some point we're done:
+ page.assert_no_selector 'input[value="Saving..."]'
+ assert page.has_field? 'Public key', with: pgp_key.to_s
+ @user.reload
+ assert_equal pgp_key, @user.public_key
+ end
+ end
+
+ def attempt_login(username, password)
+ click_on 'Log In'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ click_on 'Log In'
+ end
+
+ def assert_invalid_login(page)
+ assert page.has_selector? '.btn-primary.disabled'
+ assert page.has_content? I18n.t(:invalid_user_pass)
+ assert page.has_no_selector? '.btn-primary.disabled'
+ end
+
+end
diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_livecycle_test.rb.orig
index 50adb23..d1f800b 100644
--- a/test/integration/browser/account_test.rb
+++ b/test/integration/browser/account_livecycle_test.rb.orig
@@ -1,6 +1,6 @@
require 'test_helper'
-class AccountTest < BrowserIntegrationTest
+class AccountLivecycleTest < BrowserIntegrationTest
teardown do
Identity.destroy_all_orphaned
@@ -80,24 +80,6 @@ class AccountTest < BrowserIntegrationTest
assert page.has_content?('has already been taken')
end
- test "default user actions" do
- login
- click_on "Account Settings"
- assert page.has_content? I18n.t('destroy_my_account')
- assert page.has_no_css? '#update_login_and_password'
- assert page.has_no_css? '#update_pgp_key'
- end
-
- test "default admin actions" do
- login
- with_config admins: [@user.login] do
- click_on "Account Settings"
- assert page.has_content? I18n.t('destroy_my_account')
- assert page.has_no_css? '#update_login_and_password'
- assert page.has_css? '#update_pgp_key'
- end
- end
-
test "change pgp key" do
with_config user_actions: ['change_pgp_key'] do
pgp_key = FactoryGirl.build :pgp_key
@@ -116,6 +98,8 @@ class AccountTest < BrowserIntegrationTest
end
end
+<<<<<<< HEAD:test/integration/browser/account_livecycle_test.rb
+=======
# trying to seed an invalid A for srp login
test "detects attempt to circumvent SRP" do
@@ -133,7 +117,7 @@ class AccountTest < BrowserIntegrationTest
end
test "reports internal server errors" do
- V1::UsersController.any_instance.stubs(:create).raises
+ Api::UsersController.any_instance.stubs(:create).raises
submit_signup
assert page.has_content?("server failed")
end
@@ -152,6 +136,7 @@ class AccountTest < BrowserIntegrationTest
assert page.has_no_content?("Password")
end
+>>>>>>> api: allow version bumping - bump to 2:test/integration/browser/account_test.rb
def attempt_login(username, password)
click_on 'Log In'
fill_in 'Username', with: username
@@ -165,12 +150,4 @@ class AccountTest < BrowserIntegrationTest
assert page.has_no_selector? '.btn-primary.disabled'
end
- def inject_malicious_js
- page.execute_script <<-EOJS
- var calc = new srp.Calculate();
- calc.A = function(_a) {return "00";};
- calc.S = calc.A;
- srp.session = new srp.Session(null, calc);
- EOJS
- end
end
diff --git a/test/integration/browser/admin_test.rb b/test/integration/browser/admin_test.rb
index 902c981..0b43c29 100644
--- a/test/integration/browser/admin_test.rb
+++ b/test/integration/browser/admin_test.rb
@@ -2,6 +2,24 @@ require 'test_helper'
class AdminTest < BrowserIntegrationTest
+ test "default user actions" do
+ login
+ click_on "Account Settings"
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_no_css? '#update_pgp_key'
+ end
+
+ test "default admin actions" do
+ login
+ with_config admins: [@user.login] do
+ click_on "Account Settings"
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_css? '#update_pgp_key'
+ end
+ end
+
test "clear blocked handle" do
id = FactoryGirl.create :identity
submit_signup(id.login)
diff --git a/test/integration/browser/password_validation_test.rb b/test/integration/browser/password_validation_test.rb
index 45eb0bf..51fcc5d 100644
--- a/test/integration/browser/password_validation_test.rb
+++ b/test/integration/browser/password_validation_test.rb
@@ -5,26 +5,26 @@ class PasswordValidationTest < BrowserIntegrationTest
test "password confirmation is validated" do
username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
password ||= SecureRandom.base64
- visit '/users/new'
+ visit '/signup'
fill_in 'Username', with: username
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password + "-typo"
click_on 'Sign Up'
assert page.has_content? "does not match."
- assert_equal '/users/new', current_path
+ assert_equal '/signup', current_path
assert page.has_selector? ".error #srp_password_confirmation"
end
test "password needs to be at least 8 chars long" do
username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
password ||= SecureRandom.base64[0,7]
- visit '/users/new'
+ visit '/signup'
fill_in 'Username', with: username
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password
click_on 'Sign Up'
assert page.has_content? "needs to be at least 8 characters long"
- assert_equal '/users/new', current_path
+ assert_equal '/signup', current_path
assert page.has_selector? ".error #srp_password"
end
end
diff --git a/test/integration/browser/security_test.rb b/test/integration/browser/security_test.rb
new file mode 100644
index 0000000..825d50b
--- /dev/null
+++ b/test/integration/browser/security_test.rb
@@ -0,0 +1,52 @@
+require 'test_helper'
+
+class SecurityTest < BrowserIntegrationTest
+
+ teardown do
+ Identity.destroy_all_orphaned
+ end
+
+ # trying to seed an invalid A for srp login
+ test "detects attempt to circumvent SRP" do
+ InviteCodeValidator.any_instance.stubs(:validate)
+
+ user = FactoryGirl.create :user
+ visit '/login'
+ fill_in 'Username', with: user.login
+ fill_in 'Password', with: "password"
+ inject_malicious_js
+ click_on 'Log In'
+ assert page.has_content?("Invalid random key")
+ assert page.has_no_content?("Welcome")
+ user.destroy
+ end
+
+ test "reports internal server errors" do
+ Api::UsersController.any_instance.stubs(:create).raises
+ submit_signup
+ assert page.has_content?("server failed")
+ end
+
+ test "does not render signup form without js" do
+ Capybara.current_driver = :rack_test # no js
+ visit '/signup'
+ assert page.has_no_content?("Username")
+ assert page.has_no_content?("Password")
+ end
+
+ test "does not render login form without js" do
+ Capybara.current_driver = :rack_test # no js
+ visit '/login'
+ assert page.has_no_content?("Username")
+ assert page.has_no_content?("Password")
+ end
+
+ def inject_malicious_js
+ page.execute_script <<-EOJS
+ var calc = new srp.Calculate();
+ calc.A = function(_a) {return "00";};
+ calc.S = calc.A;
+ srp.session = new srp.Session(null, calc);
+ EOJS
+ end
+end
diff --git a/test/integration/locale_path_test.rb b/test/integration/locale_path_test.rb
index 738e7f5..22293dc 100644
--- a/test/integration/locale_path_test.rb
+++ b/test/integration/locale_path_test.rb
@@ -21,6 +21,11 @@ require 'test_helper'
#
class LocalePathTest < ActionDispatch::IntegrationTest
+
+ teardown do
+ I18n.locale = 'en'
+ end
+
test "redirect if accept-language is not default locale" do
get_via_redirect '/', {}, 'HTTP_ACCEPT_LANGUAGE' => 'de'
assert_equal '/de', path
@@ -55,4 +60,4 @@ class LocalePathTest < ActionDispatch::IntegrationTest
@controller.send(:default_url_options)
end
-end \ No newline at end of file
+end
diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb
deleted file mode 100644
index eec8c0e..0000000
--- a/test/integration/navigation_test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'test_helper'
-
-class NavigationTest < ActionDispatch::IntegrationTest
-
- # test "the truth" do
- # assert true
- # end
-end
-
diff --git a/test/leap_web_users_test.rb b/test/leap_web_users_test.rb
deleted file mode 100644
index f142e54..0000000
--- a/test/leap_web_users_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class LeapWebUsersTest < ActiveSupport::TestCase
- test "module exists" do
- assert_kind_of Module, LeapWebUsers
- end
-end
diff --git a/test/performance/browsing_test.rb b/test/performance/browsing_test.rb
deleted file mode 100644
index 3fea27b..0000000
--- a/test/performance/browsing_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
- # :output => 'tmp/performance', :formats => [:flat] }
-
- def test_homepage
- get '/'
- end
-end
diff --git a/test/support/api_controller_test.rb b/test/support/api_controller_test.rb
new file mode 100644
index 0000000..06cb46a
--- /dev/null
+++ b/test/support/api_controller_test.rb
@@ -0,0 +1,29 @@
+class ApiControllerTest < ActionController::TestCase
+
+ def api_get(*args)
+ get *add_api_defaults(args)
+ end
+
+ def api_post(*args)
+ post *add_api_defaults(args)
+ end
+
+ def api_delete(*args)
+ delete *add_api_defaults(args)
+ end
+
+ def api_put(*args)
+ put *add_api_defaults(args)
+ end
+
+ def add_api_defaults(args)
+ add_defaults args, version: '2'
+ end
+
+ def add_defaults(args, defaults)
+ opts = args.extract_options!
+ opts.reverse_merge! defaults
+ args << opts
+ args
+ end
+end
diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb
index 3b3481b..cea480c 100644
--- a/test/support/api_integration_test.rb
+++ b/test/support/api_integration_test.rb
@@ -3,6 +3,10 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest
DUMMY_TOKEN = Token.new
RACK_ENV = {'HTTP_AUTHORIZATION' => %Q(Token token="#{DUMMY_TOKEN.to_s}")}
+ def api_version
+ 2
+ end
+
setup do
@testcode = InviteCode.new
@testcode.save!
diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb
index 7724fb4..6a22642 100644
--- a/test/support/assert_responses.rb
+++ b/test/support/assert_responses.rb
@@ -71,21 +71,24 @@ module AssertResponses
end
def assert_login_required
- assert_error_response :not_authorized_login, :unauthorized
+ assert_error_response :not_authorized_login,
+ status: :unauthorized
end
def assert_access_denied
- assert_error_response :not_authorized, :forbidden
+ assert_error_response :not_authorized,
+ status: :forbidden
end
- def assert_error_response(key, status=nil)
- message = I18n.t(key)
+ def assert_error_response(key, options = {})
+ status=options.delete :status
+ message = I18n.t(key, options)
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)
+ assert_equal({'alert' => message}, flash.to_hash)
end
end
diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb
index 1deb8fa..1f5e3d2 100644
--- a/test/support/browser_integration_test.rb
+++ b/test/support/browser_integration_test.rb
@@ -3,10 +3,11 @@
#
# Use this class for capybara based integration tests for the ui.
#
+require 'capybara/rails'
class BrowserIntegrationTest < ActionDispatch::IntegrationTest
# let's use dom_id inorder to identify sections
- include ActionController::RecordIdentifier
+ include ActionView::RecordIdentifier
CONFIG_RU = (Rails.root + 'config.ru').to_s
OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first
@@ -28,7 +29,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
Capybara.app_host = 'http://lvh.me:3003'
Capybara.server_port = 3003
Capybara.javascript_driver = :poltergeist
- Capybara.default_wait_time = 5
+ Capybara.default_max_wait_time = 5
# Make the Capybara DSL available
include Capybara::DSL
@@ -46,32 +47,17 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
end
def submit_signup(username = nil, password = nil)
-
- with_config invite_required: true do
-
- username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
- password ||= SecureRandom.base64
- visit '/users/new'
- fill_in 'Username', with: username
- fill_in 'Password', with: password
+ username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
+ password ||= SecureRandom.base64
+ visit '/signup'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ if APP_CONFIG[:invite_required]
fill_in 'Invite code', with: @testcode.invite_code
- fill_in 'Password confirmation', with: password
- click_on 'Sign Up'
- return username, password
- end
-
- with_config invite_required: false do
-
- username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
- password ||= SecureRandom.base64
- visit '/users/new'
- fill_in 'Username', with: username
- fill_in 'Password', with: password
- fill_in 'Password confirmation', with: password
- click_on 'Sign Up'
- return username, password
end
-
+ fill_in 'Password confirmation', with: password
+ click_on 'Sign Up'
+ return username, password
end
# currently this only works for tests with poltergeist.
@@ -101,7 +87,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
File.open(logfile_path, 'w') do |test_log|
test_log.puts self.class.name
test_log.puts "========================="
- test_log.puts __name__
+ test_log.puts name
test_log.puts Time.now
test_log.puts current_path
test_log.puts page.status_code
diff --git a/test/support/record_assertions.rb b/test/support/record_assertions.rb
new file mode 100644
index 0000000..30b947f
--- /dev/null
+++ b/test/support/record_assertions.rb
@@ -0,0 +1,10 @@
+module RecordAssertions
+
+ def assert_error(record, options)
+ options.each do |k, v|
+ errors = record.errors[k]
+ assert_equal I18n.t("errors.messages.#{v}"), errors.first
+ end
+ end
+
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index dfc6627..a06f710 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,5 +1,5 @@
ENV["RAILS_ENV"] = "test"
-require File.expand_path('../../config/environment', __FILE__)
+require_relative '../config/environment'
require 'rails/test_help'
require 'mocha/setup'
@@ -16,11 +16,11 @@ class ActiveSupport::TestCase
protected
def logfile_path
- Rails.root + 'tmp' + "#{self.class.name.underscore}.#{__name__}.log"
+ Rails.root + 'tmp' + "#{self.class.name.underscore}.#{name}.log"
end
def screenshot_path
- Rails.root + 'tmp' + "#{self.class.name.underscore}.#{__name__}.png"
+ Rails.root + 'tmp' + "#{self.class.name.underscore}.#{name}.png"
end
def file_path(name)
diff --git a/test/unit/email_test.rb b/test/unit/email_test.rb
index e858bd5..739b43e 100644
--- a/test/unit/email_test.rb
+++ b/test/unit/email_test.rb
@@ -1,4 +1,5 @@
require 'test_helper'
+require 'email'
class EmailTest < ActiveSupport::TestCase
diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb
index 9d4bc90..e9173af 100644
--- a/test/unit/identity_test.rb
+++ b/test/unit/identity_test.rb
@@ -2,6 +2,7 @@ require_relative '../test_helper'
class IdentityTest < ActiveSupport::TestCase
include StubRecordHelper
+ include RecordAssertions
setup do
@user = find_record :user
@@ -22,7 +23,7 @@ class IdentityTest < ActiveSupport::TestCase
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]
+ assert_error @id, destination: :blank
end
test "disabled identity requires no destination" do
@@ -62,7 +63,7 @@ class IdentityTest < ActiveSupport::TestCase
@id = Identity.create_for @user, address: alias_name, destination: forward_address
dup = Identity.build_for @user, address: alias_name, destination: forward_address
assert !dup.valid?
- assert_equal ["has already been taken"], dup.errors[:destination]
+ assert_error dup, destination: :taken
end
test "validates availability" do
@@ -70,7 +71,7 @@ class IdentityTest < ActiveSupport::TestCase
@id = Identity.create_for @user, address: alias_name, destination: forward_address
taken = Identity.build_for other_user, address: alias_name
assert !taken.valid?
- assert_equal ["has already been taken"], taken.errors[:address]
+ assert_error taken, address: :taken
end
test "setting and getting pgp key" do
@@ -133,7 +134,7 @@ class IdentityTest < ActiveSupport::TestCase
other_user = find_record :user
taken = Identity.build_for other_user, address: @id.address
assert !taken.valid?
- assert_equal ["has already been taken"], taken.errors[:address]
+ assert_error taken, address: :taken
end
test "destroy all orphaned identities" do
diff --git a/test/unit/invite_code_validator_test.rb b/test/unit/invite_code_validator_test.rb
index 62eeae6..934ba2e 100644
--- a/test/unit/invite_code_validator_test.rb
+++ b/test/unit/invite_code_validator_test.rb
@@ -3,9 +3,9 @@ require 'test_helper'
class InviteCodeValidatorTest < ActiveSupport::TestCase
test "user should not be created with invalid invite code" do
with_config invite_required: true do
- invalid_user = FactoryGirl.build(:user)
+ invalid_user = FactoryGirl.build(:user)
- assert !invalid_user.valid?
+ assert !invalid_user.valid?
end
end
@@ -30,7 +30,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
test "Invite count >= invite max uses is not accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.new
user_code.invite_count = 1
@@ -46,7 +46,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "Invite count < invite max uses is accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.create
user_code.save
@@ -60,7 +60,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "Invite count 0 is accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.create
@@ -73,7 +73,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "There is an error message if the invite code does not exist" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user = FactoryGirl.build :user
user.invite_code = "wrongcode"
@@ -83,4 +83,4 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
assert_equal ["This is not a valid code"], user.errors[:invite_code]
end
-end \ No newline at end of file
+end
diff --git a/test/unit/tmp_user_test.rb b/test/unit/temporary_user_test.rb
index 1dea5f9..38ccd67 100644
--- a/test/unit/tmp_user_test.rb
+++ b/test/unit/temporary_user_test.rb
@@ -1,6 +1,6 @@
require 'test_helper'
-class TmpUserTest < ActiveSupport::TestCase
+class TemporaryUserTest < ActiveSupport::TestCase
setup do
InviteCodeValidator.any_instance.stubs(:validate)