From 53808b073f539ba2b442738b6abf97228488e311 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 09:12:37 +0200 Subject: moving all of core into toplevel, tests fail. --- Gemfile | 10 +- Gemfile.lock | 20 +-- app/assets/javascripts/leap.js | 7 + app/assets/javascripts/platform.js | 93 ++++++++++++++ app/helpers/core_helper.rb | 13 ++ app/helpers/download_helper.rb | 33 +++++ app/helpers/navigation_helper.rb | 82 ++++++++++++ app/views/common/_download_for_os.html.haml | 17 +++ app/views/common/_home_page_buttons.html.haml | 23 ++++ app/views/kaminari/_first_page.html.haml | 9 ++ app/views/kaminari/_gap.html.haml | 8 ++ app/views/kaminari/_last_page.html.haml | 9 ++ app/views/kaminari/_next_page.html.haml | 12 ++ app/views/kaminari/_page.html.haml | 14 ++ app/views/kaminari/_paginator.html.haml | 19 +++ app/views/kaminari/_prev_page.html.haml | 12 ++ billing/leap_web_billing.gemspec | 1 - billing/lib/leap_web_billing/engine.rb | 3 - certs/leap_web_certs.gemspec | 1 - certs/lib/leap_web_certs/engine.rb | 2 - config/initializers/couchrest_model.rb | 3 + config/initializers/simple_form.rb | 142 +++++++++++++++++++++ config/initializers/simple_form_bootstrap.rb | 57 +++++++++ config/initializers/validations.rb | 4 + config/locales/en.yml | 42 +++++- config/locales/simple_form.en.yml | 26 ++++ core/Gemfile | 17 --- core/Rakefile | 44 ------- core/Readme.md | 6 - core/app/assets/javascripts/leap.js | 7 - core/app/assets/javascripts/platform.js | 93 -------------- core/app/helpers/core_helper.rb | 13 -- core/app/helpers/download_helper.rb | 33 ----- core/app/helpers/navigation_helper.rb | 82 ------------ core/app/views/common/_download_for_os.html.haml | 17 --- core/app/views/common/_home_page_buttons.html.haml | 23 ---- core/app/views/kaminari/_first_page.html.haml | 9 -- core/app/views/kaminari/_gap.html.haml | 8 -- core/app/views/kaminari/_last_page.html.haml | 9 -- core/app/views/kaminari/_next_page.html.haml | 12 -- core/app/views/kaminari/_page.html.haml | 14 -- core/app/views/kaminari/_paginator.html.haml | 19 --- core/app/views/kaminari/_prev_page.html.haml | 12 -- core/config/initializers/backtrace_silencers.rb | 7 - core/config/initializers/couchrest_model.rb | 3 - core/config/initializers/inflections.rb | 15 --- core/config/initializers/mime_types.rb | 5 - core/config/initializers/simple_form.rb | 142 --------------------- core/config/initializers/simple_form_bootstrap.rb | 57 --------- core/config/initializers/validations.rb | 4 - core/config/initializers/wrap_parameters.rb | 10 -- core/config/locales/en.yml | 41 ------ core/config/locales/simple_form.en.yml | 26 ---- core/config/routes.rb | 2 - core/leap_web_core.gemspec | 25 ---- core/lib/extensions/couchrest.rb | 95 -------------- core/lib/extensions/testing.rb | 48 ------- core/lib/leap_web_core.rb | 14 -- core/lib/leap_web_core/dependencies.rb | 40 ------ core/lib/leap_web_core/engine.rb | 9 -- core/lib/leap_web_core/ui_dependencies.rb | 11 -- core/lib/tasks/leap_web_core_tasks.rake | 25 ---- core/script/rails | 8 -- core/test/support/browser_integration_test.rb | 81 ------------ core/test/support/rack_test.rb | 37 ------ core/test/support/with_config_helper.rb | 16 --- help/leap_web_help.gemspec | 1 - help/lib/leap_web_help/engine.rb | 4 - lib/extensions/couchrest.rb | 95 ++++++++++++++ lib/extensions/testing.rb | 48 +++++++ lib/leap_web_core.rb | 14 ++ lib/leap_web_core/dependencies.rb | 40 ++++++ lib/leap_web_core/engine.rb | 9 ++ lib/leap_web_core/ui_dependencies.rb | 11 ++ lib/tasks/leap_web_core_tasks.rake | 25 ++++ test/support/browser_integration_test.rb | 81 ++++++++++++ test/support/rack_test.rb | 37 ++++++ test/support/with_config_helper.rb | 16 +++ users/leap_web_users.gemspec | 2 - users/lib/leap_web_users/engine.rb | 2 - 80 files changed, 1011 insertions(+), 1175 deletions(-) create mode 100644 app/assets/javascripts/leap.js create mode 100644 app/assets/javascripts/platform.js create mode 100644 app/helpers/core_helper.rb create mode 100644 app/helpers/download_helper.rb create mode 100644 app/helpers/navigation_helper.rb create mode 100644 app/views/common/_download_for_os.html.haml create mode 100644 app/views/common/_home_page_buttons.html.haml create mode 100644 app/views/kaminari/_first_page.html.haml create mode 100644 app/views/kaminari/_gap.html.haml create mode 100644 app/views/kaminari/_last_page.html.haml create mode 100644 app/views/kaminari/_next_page.html.haml create mode 100644 app/views/kaminari/_page.html.haml create mode 100644 app/views/kaminari/_paginator.html.haml create mode 100644 app/views/kaminari/_prev_page.html.haml create mode 100644 config/initializers/couchrest_model.rb create mode 100644 config/initializers/simple_form.rb create mode 100644 config/initializers/simple_form_bootstrap.rb create mode 100644 config/initializers/validations.rb create mode 100644 config/locales/simple_form.en.yml delete mode 100644 core/Gemfile delete mode 100644 core/Rakefile delete mode 100644 core/Readme.md delete mode 100644 core/app/assets/javascripts/leap.js delete mode 100644 core/app/assets/javascripts/platform.js delete mode 100644 core/app/helpers/core_helper.rb delete mode 100644 core/app/helpers/download_helper.rb delete mode 100644 core/app/helpers/navigation_helper.rb delete mode 100644 core/app/views/common/_download_for_os.html.haml delete mode 100644 core/app/views/common/_home_page_buttons.html.haml delete mode 100644 core/app/views/kaminari/_first_page.html.haml delete mode 100644 core/app/views/kaminari/_gap.html.haml delete mode 100644 core/app/views/kaminari/_last_page.html.haml delete mode 100644 core/app/views/kaminari/_next_page.html.haml delete mode 100644 core/app/views/kaminari/_page.html.haml delete mode 100644 core/app/views/kaminari/_paginator.html.haml delete mode 100644 core/app/views/kaminari/_prev_page.html.haml delete mode 100644 core/config/initializers/backtrace_silencers.rb delete mode 100644 core/config/initializers/couchrest_model.rb delete mode 100644 core/config/initializers/inflections.rb delete mode 100644 core/config/initializers/mime_types.rb delete mode 100644 core/config/initializers/simple_form.rb delete mode 100644 core/config/initializers/simple_form_bootstrap.rb delete mode 100644 core/config/initializers/validations.rb delete mode 100644 core/config/initializers/wrap_parameters.rb delete mode 100644 core/config/locales/en.yml delete mode 100644 core/config/locales/simple_form.en.yml delete mode 100644 core/config/routes.rb delete mode 100644 core/leap_web_core.gemspec delete mode 100644 core/lib/extensions/couchrest.rb delete mode 100644 core/lib/extensions/testing.rb delete mode 100644 core/lib/leap_web_core.rb delete mode 100644 core/lib/leap_web_core/dependencies.rb delete mode 100644 core/lib/leap_web_core/engine.rb delete mode 100644 core/lib/leap_web_core/ui_dependencies.rb delete mode 100644 core/lib/tasks/leap_web_core_tasks.rake delete mode 100755 core/script/rails delete mode 100644 core/test/support/browser_integration_test.rb delete mode 100644 core/test/support/rack_test.rb delete mode 100644 core/test/support/with_config_helper.rb create mode 100644 lib/extensions/couchrest.rb create mode 100644 lib/extensions/testing.rb create mode 100644 lib/leap_web_core.rb create mode 100644 lib/leap_web_core/dependencies.rb create mode 100644 lib/leap_web_core/engine.rb create mode 100644 lib/leap_web_core/ui_dependencies.rb create mode 100644 lib/tasks/leap_web_core_tasks.rake create mode 100644 test/support/browser_integration_test.rb create mode 100644 test/support/rack_test.rb create mode 100644 test/support/with_config_helper.rb diff --git a/Gemfile b/Gemfile index 1d6c432..7a82157 100644 --- a/Gemfile +++ b/Gemfile @@ -3,10 +3,12 @@ source 'https://rubygems.org' eval(File.read(File.dirname(__FILE__) + '/common_dependencies.rb')) eval(File.read(File.dirname(__FILE__) + '/ui_dependencies.rb')) -# EITHER fetch all of the leap_web gems in one go -# gem 'leap_web' -# OR use the local versions for development instead: -gem "leap_web_core", :path => 'core' +gem "rails", "~> 3.2.11" +gem "couchrest", "~> 1.1.3" +gem "couchrest_model", "~> 2.0.0" +gem "couchrest_session_store", "~> 0.2.4" +gem "json" + gem 'leap_web_users', :path => 'users' gem 'leap_web_certs', :path => 'certs' gem 'leap_web_help', :path => 'help' diff --git a/Gemfile.lock b/Gemfile.lock index 0c7486f..6c43a9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,36 +10,22 @@ PATH specs: leap_web_billing (0.5.0) braintree - leap_web_core (= 0.5.0) PATH remote: certs specs: leap_web_certs (0.5.0) certificate_authority (>= 0.2.0) - leap_web_core (= 0.5.0) - -PATH - remote: core - specs: - leap_web_core (0.5.0) - couchrest (~> 1.1.3) - couchrest_model (~> 2.0.0) - couchrest_session_store (~> 0.2.4) - json - rails (~> 3.2.11) PATH remote: help specs: leap_web_help (0.5.0) - leap_web_core (= 0.5.0) PATH remote: users specs: leap_web_users (0.5.0) - leap_web_core (= 0.5.0) rails_warden ruby-srp (~> 0.2.1) @@ -271,6 +257,9 @@ DEPENDENCIES client_side_validations client_side_validations-simple_form coffee-rails (~> 3.2.2) + couchrest (~> 1.1.3) + couchrest_model (~> 2.0.0) + couchrest_session_store (~> 0.2.4) debugger factory_girl_rails fake_braintree @@ -279,11 +268,11 @@ DEPENDENCIES haml-rails (~> 0.3.4) http_accept_language jquery-rails + json kaminari (= 0.13.0) launchy leap_web_billing! leap_web_certs! - leap_web_core! leap_web_help! leap_web_users! minitest-stub-const @@ -291,6 +280,7 @@ DEPENDENCIES phantomjs-binaries poltergeist quiet_assets + rails (~> 3.2.11) rails-i18n rdiscount sass-rails (~> 3.2.5) diff --git a/app/assets/javascripts/leap.js b/app/assets/javascripts/leap.js new file mode 100644 index 0000000..94e602d --- /dev/null +++ b/app/assets/javascripts/leap.js @@ -0,0 +1,7 @@ + +// +// add a bootstrap alert to the page via javascript. +// +function alert_message(msg) { + $('#messages').append('
×'+msg+'
'); +} diff --git a/app/assets/javascripts/platform.js b/app/assets/javascripts/platform.js new file mode 100644 index 0000000..108c162 --- /dev/null +++ b/app/assets/javascripts/platform.js @@ -0,0 +1,93 @@ +/* Inspired by mozillas platform detection: + https://github.com/mozilla/bedrock/tree/master/media/js/base +*/ + (function () { + 'use strict'; + function getPlatform() { + var ua = navigator.userAgent, + pf = navigator.platform; + if (/Win(16|9[x58]|NT( [1234]| 5\.0| [^0-9]|[^ -]|$))/.test(ua) || + /Windows ([MC]E|9[x58]|3\.1|4\.10|NT( [1234]| 5\.0| [^0-9]|[^ ]|$))/.test(ua) || + /Windows_95/.test(ua)) { + /** + * Officially unsupported platforms are Windows 95, 98, ME, NT 4.x, 2000 + * These regular expressions match: + * - Win16 + * - Win9x + * - Win95 + * - Win98 + * - WinNT (not followed by version or followed by version <= 5) + * - Windows ME + * - Windows CE + * - Windows 9x + * - Windows 95 + * - Windows 98 + * - Windows 3.1 + * - Windows 4.10 + * - Windows NT (not followed by version or followed by version <= 5) + * - Windows_95 + */ + return 'oldwin'; + } + if (ua.indexOf("MSIE 6.0") !== -1 && + ua.indexOf("Windows NT 5.1") !== -1 && + ua.indexOf("SV1") === -1) { + // Windows XP SP1 + return 'oldwin'; + } + if (pf.indexOf("Win32") !== -1 || + pf.indexOf("Win64") !== -1) { + return 'windows'; + } + if (/android/i.test(ua)) { + return 'android'; + } + if (/armv[6-7]l/.test(pf)) { + return 'android'; + } + if (pf.indexOf("Linux") !== -1) { + return 'linux'; + //if (pf.indexOf("64") !== -1) { + // return 'linux64'; + //} else { + // return 'linux32'; + //} + } + if (pf.indexOf("MacPPC") !== -1) { + return 'oldmac'; + } + if (/Mac OS X 10.[0-5]/.test(ua)) { + return 'oldmac'; + } + if (pf.indexOf('iPhone') !== -1 || + pf.indexOf('iPad') !== -1 || + pf.indexOf('iPod') !== -1 ) { + return 'ios'; + } + if (ua.indexOf("Mac OS X") !== -1) { + return 'osx'; + } + if (ua.indexOf("MSIE 5.2") !== -1) { + return 'oldmac'; + } + if (pf.indexOf("Mac") !== -1) { + return 'oldmac'; + } + if (navigator.platform === '' && + navigator.userAgent.indexOf("Firefox") !== -1 && + navigator.userAgent.indexOf("Mobile") !== -1) { + return 'fxos'; + } + + return 'other'; + } + (function () { + // Immediately set the platform classname on the html-element + // to avoid lots of flickering + var h = document.documentElement; + window.site = { + platform : getPlatform() + }; + h.className = window.site.platform; + })(); + })(); diff --git a/app/helpers/core_helper.rb b/app/helpers/core_helper.rb new file mode 100644 index 0000000..a6c7479 --- /dev/null +++ b/app/helpers/core_helper.rb @@ -0,0 +1,13 @@ +# +# Misc. helpers needed throughout. +# +module CoreHelper + + # + # insert common buttons (download, login, etc) + # + def home_page_buttons(on_user_page = false) + render 'common/home_page_buttons', {:on_user_page => on_user_page} + end + +end diff --git a/app/helpers/download_helper.rb b/app/helpers/download_helper.rb new file mode 100644 index 0000000..ee0fe73 --- /dev/null +++ b/app/helpers/download_helper.rb @@ -0,0 +1,33 @@ +module DownloadHelper + + def alternative_client_links(os = nil) + alternative_clients(os).map do |client| + link_to(I18n.t("os."+client), client_download_url(client)) + end + end + + def alternative_clients(os = nil) + available_clients - [os] + end + + def client_download_url(os = nil) + client_download_domain + client_download_path(os) + end + + def client_download_path(os) + download_paths[os.to_s] || download_paths['other'] || '' + end + + def available_clients + APP_CONFIG[:available_clients] || [] + end + + def client_download_domain + APP_CONFIG[:client_download_domain] || '' + end + + def download_paths + APP_CONFIG[:download_paths] || {} + end + +end diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb new file mode 100644 index 0000000..19cb934 --- /dev/null +++ b/app/helpers/navigation_helper.rb @@ -0,0 +1,82 @@ +module NavigationHelper + + # + # used to create a side navigation link. + # + # Signature is the same as link_to, except it accepts an :active value in the html_options + # + def link_to_navigation(*args) + if args.last.is_a? Hash + html_options = args.pop.dup + active_class = html_options.delete(:active) ? 'active' : nil + html_options[:class] = [html_options[:class], active_class].join(' ') + args << html_options + else + active_class = nil + end + content_tag :li, :class => active_class do + link_to(*args) + end + end + + # + # returns true if params[:action] matches one of the args. + # + def action?(*actions) + actions.detect do |action| + if action.is_a? String + action == action_string + elsif action.is_a? Symbol + if action == :none + action_string == nil + else + action == action_symbol + end + end + end + end + + # + # returns true if params[:controller] matches one of the args. + # + # for example: + # controller?(:me, :home) + # controller?('groups/') <-- matches any controller in namespace 'groups' + # + def controller?(*controllers) + controllers.each do |cntr| + if cntr.is_a? String + if cntr.ends_with?('/') + return true if controller_string.starts_with?(cntr.chop) + end + return true if cntr == controller_string + elsif cntr.is_a? Symbol + return true if cntr == controller_symbol + end + end + return false + end + + private + + def controller_string + @controller_string ||= params[:controller].to_s.gsub(/^\//, '') + end + + def controller_symbol + @controller_symbol ||= params[:controller].gsub(/^\//,'').gsub('/','_').to_sym + end + + def action_string + params[:action] + end + + def action_symbol + @action_symbol ||= if params[:action].present? + params[:action].to_sym + else + nil + end + end + +end diff --git a/app/views/common/_download_for_os.html.haml b/app/views/common/_download_for_os.html.haml new file mode 100644 index 0000000..3a11d10 --- /dev/null +++ b/app/views/common/_download_for_os.html.haml @@ -0,0 +1,17 @@ +- os = download_for_os +%div{:class => "os-#{os}"} + %span.link + - btn_class = (os == "other") ? "disabled" : "btn-primary" + = link_to client_download_url(os), :class => "btn btn-large #{btn_class}" do + = big_icon('download') + .pull-right + = t(:download_client) + %br/ + %small= I18n.t("os.#{os}") + %span.info + %div= t(:client_info, :provider => content_tag(:b,APP_CONFIG[:domain])).html_safe + %div + - if os == "other" + = t(:all_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe + - else + = t(:other_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml new file mode 100644 index 0000000..c9ea7a2 --- /dev/null +++ b/app/views/common/_home_page_buttons.html.haml @@ -0,0 +1,23 @@ +- icon_color = :black + +.home-buttons + .row-fluid.first + .span2 + .download.span8 + = render partial: 'common/download_for_os', collection: available_clients + ['other'] + .span2 + - if local_assigns[:divider] + .row-fluid + .span12 + = render local_assigns[:divider] + - if !local_assigns[:on_user_page] + .row-fluid.second + .login.span4 + %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') + %span.info= t(:login_info) + .signup.span4 + %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') + %span.info= t(:signup_info) + .help.span4 + %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') + %span.info= t(:help_info) diff --git a/app/views/kaminari/_first_page.html.haml b/app/views/kaminari/_first_page.html.haml new file mode 100644 index 0000000..34436e3 --- /dev/null +++ b/app/views/kaminari/_first_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "First" page +-# available local variables +-# url: url to the first page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/_gap.html.haml b/app/views/kaminari/_gap.html.haml new file mode 100644 index 0000000..51de678 --- /dev/null +++ b/app/views/kaminari/_gap.html.haml @@ -0,0 +1,8 @@ +-# Non-link tag that stands for skipped pages... +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li.disabled + = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/_last_page.html.haml b/app/views/kaminari/_last_page.html.haml new file mode 100644 index 0000000..c90433c --- /dev/null +++ b/app/views/kaminari/_last_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Last" page +-# available local variables +-# url: url to the last page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 0000000..ea6cab2 --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.last? + %li.disabled + %span= raw(t 'views.pagination.next') +- else + %li= link_to(raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote) diff --git a/app/views/kaminari/_page.html.haml b/app/views/kaminari/_page.html.haml new file mode 100644 index 0000000..2f2f142 --- /dev/null +++ b/app/views/kaminari/_page.html.haml @@ -0,0 +1,14 @@ +-# Link showing page number +-# available local variables +-# page: a page object for "this" page +-# url: url to this page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote + +- if page.current? + %li.active + %span= page +- else + %li= link_to(page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}) diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 0000000..79c5b92 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,19 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + .pagination + %ul + -#= first_page_tag unless current_page.first? + = prev_page_tag #unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag #unless current_page.last? + -#= last_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 0000000..d274bf4 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.first? + %li.disabled + %span= raw(t 'views.pagination.previous') +- else + %li= link_to(raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote) diff --git a/billing/leap_web_billing.gemspec b/billing/leap_web_billing.gemspec index 94d92e0..cb0335e 100644 --- a/billing/leap_web_billing.gemspec +++ b/billing/leap_web_billing.gemspec @@ -15,7 +15,6 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] - s.add_dependency "leap_web_core", LeapWeb::VERSION # s.add_dependency "braintree-rails", "~> 0.4.5" s.add_dependency "braintree" #s.add_dependency "carmen-rails" diff --git a/billing/lib/leap_web_billing/engine.rb b/billing/lib/leap_web_billing/engine.rb index 6d76add..ab574f2 100644 --- a/billing/lib/leap_web_billing/engine.rb +++ b/billing/lib/leap_web_billing/engine.rb @@ -1,7 +1,4 @@ # thou shall require all your dependencies in an engine. -require "leap_web_core" -require "leap_web_core/ui_dependencies" - #require "braintree-rails" require "braintree" #require "carmen-rails" diff --git a/certs/leap_web_certs.gemspec b/certs/leap_web_certs.gemspec index 21be09d..87b5be4 100644 --- a/certs/leap_web_certs.gemspec +++ b/certs/leap_web_certs.gemspec @@ -15,7 +15,6 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"] s.test_files = Dir["test/**/*"] - s.add_dependency "leap_web_core", LeapWeb::VERSION s.add_dependency "certificate_authority", [">= 0.2.0"] end diff --git a/certs/lib/leap_web_certs/engine.rb b/certs/lib/leap_web_certs/engine.rb index 3c8948a..33a446e 100644 --- a/certs/lib/leap_web_certs/engine.rb +++ b/certs/lib/leap_web_certs/engine.rb @@ -1,5 +1,3 @@ -require "leap_web_core" - module LeapWebCerts class Engine < ::Rails::Engine diff --git a/config/initializers/couchrest_model.rb b/config/initializers/couchrest_model.rb new file mode 100644 index 0000000..ce4f41a --- /dev/null +++ b/config/initializers/couchrest_model.rb @@ -0,0 +1,3 @@ +CouchRest::Model::Base.configure do |config| + config.auto_update_design_doc = false +end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb new file mode 100644 index 0000000..e3f8d09 --- /dev/null +++ b/config/initializers/simple_form.rb @@ -0,0 +1,142 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, :class => :input, + :hint_class => :field_with_hint, :error_class => :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input :placeholder => "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable the lookup for any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + b.optional :maxlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, :wrap_with => { :tag => :span, :class => :hint } + b.use :error, :wrap_with => { :tag => :span, :class => :error } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # :inline => input + label + # :nested => label > input + config.boolean_style = :nested + + # Default class for buttons + config.button_class = 'btn' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-error' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. Please note that when using :boolean_style = :nested, + # SimpleForm will force this option to be a label. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required| "#{required} #{label}" } + + # You can define the class to use on all labels. Default is nil. + config.label_class = 'control-label' + + # You can define the class to use on all forms. Default is simple_form. + # config.form_class = :simple_form + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use default HTML5 validations (novalidate option). + # Default is enabled. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { :string => :prepend } + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # Default size for text inputs. + # config.default_input_size = 50 + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? +end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 0000000..c949f5e --- /dev/null +++ b/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,57 @@ +# 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| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' 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| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend| + prepend.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-append' do |append| + append.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + # + # when you don't want any bootstrap "control-group" or "controls" wrappers. + # + config.wrappers :none, :tag => 'div', :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.use :input + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } + end + + # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. + # Check the Bootstrap docs (http://twitter.github.com/bootstrap) + # to learn about the different styles for forms and inputs, + # buttons and other elements. + config.default_wrapper = :bootstrap +end diff --git a/config/initializers/validations.rb b/config/initializers/validations.rb new file mode 100644 index 0000000..e8acfbe --- /dev/null +++ b/config/initializers/validations.rb @@ -0,0 +1,4 @@ +# 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.yml b/config/locales/en.yml index 96c47ca..ac154d6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,4 +3,44 @@ en: terms_of_service: Terms of Service pricing: Pricing about: About Us - contact: Contact \ No newline at end of file + contact: Contact + no_such_thing: "No such %{thing}." + thing_was_successfully_created: "%{thing} was successfully created." + create_thing: "Create %{thing}" + + overview: "Overview" + user_control_panel: "user control panel" + + created: "Created" + created_by_on: "Created by %{user} on %{time}" + updated: "Updated" + + none: "None" + unknown: "Unknown" + admin: "Admin" + anonymous: "Anonymous" + save: "Save" + add: "Add" + remove: "Remove" + changes_saved: "Changes saved successfully." + are_you_sure: "Are you sure? This change cannot be undone." + + download_client: "Download Bitmask" + client_info: "The Bitmask application allows you to use %{provider} services." + all_downloads_info: "It is available for %{clients}." + other_downloads_info: "Bitmask is also available for %{clients}." + login_info: "Log in to change your account settings, create support tickets, and manage payments." + signup_info: "Sign up for a new user account via this website (it is better if you use the Bitmask application to sign up, but this website works too)." + welcome: "Welcome to %{provider}." + get_help: "Get Help" + help_info: "Can't login? Create a new support ticket anonymously." + example_email: 'user@domain.org' + os: + linux32: "Linux (32 bit)" + linux64: "Linux (64 bit)" + linux: "GNU/Linux" + windows: "Windows" + android: "Android" + osx: "Mac OS" + other: "(not available for your OS)" + diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml new file mode 100644 index 0000000..0df11fe --- /dev/null +++ b/config/locales/simple_form.en.yml @@ -0,0 +1,26 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + # Labels and hints examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + diff --git a/core/Gemfile b/core/Gemfile deleted file mode 100644 index b552dc5..0000000 --- a/core/Gemfile +++ /dev/null @@ -1,17 +0,0 @@ -source "https://rubygems.org" - -# Declare your gem's dependencies in leap_web_core.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. -gemspec - -# jquery-rails is used by the dummy application -gem "jquery-rails" - -# Declare any dependencies that are still in development here instead of in -# your gemspec. These might include edge Rails or gems from your path or -# Git. Remember to move these dependencies to your gemspec before releasing -# your gem to rubygems.org. - -# To use debugger -# gem 'ruby-debug' diff --git a/core/Rakefile b/core/Rakefile deleted file mode 100644 index 3c6539c..0000000 --- a/core/Rakefile +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env rake - -require 'rake/packagetask' -require 'rubygems/package_task' - -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end -begin - require 'rdoc/task' -rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' - RDoc::Task = Rake::RDocTask -end - -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebCore' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = eval(File.read('leap_web_core.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - - -task :default => :test diff --git a/core/Readme.md b/core/Readme.md deleted file mode 100644 index 795c0a4..0000000 --- a/core/Readme.md +++ /dev/null @@ -1,6 +0,0 @@ -Leap Web Core -=== - -[Leap](http://www.leap.se) is the Leap Encryption Access Project and this is the rails app for its web interface. - -This is the its core gem. Currently it only serves to load shared dependencies. diff --git a/core/app/assets/javascripts/leap.js b/core/app/assets/javascripts/leap.js deleted file mode 100644 index 94e602d..0000000 --- a/core/app/assets/javascripts/leap.js +++ /dev/null @@ -1,7 +0,0 @@ - -// -// add a bootstrap alert to the page via javascript. -// -function alert_message(msg) { - $('#messages').append('
×'+msg+'
'); -} diff --git a/core/app/assets/javascripts/platform.js b/core/app/assets/javascripts/platform.js deleted file mode 100644 index 108c162..0000000 --- a/core/app/assets/javascripts/platform.js +++ /dev/null @@ -1,93 +0,0 @@ -/* Inspired by mozillas platform detection: - https://github.com/mozilla/bedrock/tree/master/media/js/base -*/ - (function () { - 'use strict'; - function getPlatform() { - var ua = navigator.userAgent, - pf = navigator.platform; - if (/Win(16|9[x58]|NT( [1234]| 5\.0| [^0-9]|[^ -]|$))/.test(ua) || - /Windows ([MC]E|9[x58]|3\.1|4\.10|NT( [1234]| 5\.0| [^0-9]|[^ ]|$))/.test(ua) || - /Windows_95/.test(ua)) { - /** - * Officially unsupported platforms are Windows 95, 98, ME, NT 4.x, 2000 - * These regular expressions match: - * - Win16 - * - Win9x - * - Win95 - * - Win98 - * - WinNT (not followed by version or followed by version <= 5) - * - Windows ME - * - Windows CE - * - Windows 9x - * - Windows 95 - * - Windows 98 - * - Windows 3.1 - * - Windows 4.10 - * - Windows NT (not followed by version or followed by version <= 5) - * - Windows_95 - */ - return 'oldwin'; - } - if (ua.indexOf("MSIE 6.0") !== -1 && - ua.indexOf("Windows NT 5.1") !== -1 && - ua.indexOf("SV1") === -1) { - // Windows XP SP1 - return 'oldwin'; - } - if (pf.indexOf("Win32") !== -1 || - pf.indexOf("Win64") !== -1) { - return 'windows'; - } - if (/android/i.test(ua)) { - return 'android'; - } - if (/armv[6-7]l/.test(pf)) { - return 'android'; - } - if (pf.indexOf("Linux") !== -1) { - return 'linux'; - //if (pf.indexOf("64") !== -1) { - // return 'linux64'; - //} else { - // return 'linux32'; - //} - } - if (pf.indexOf("MacPPC") !== -1) { - return 'oldmac'; - } - if (/Mac OS X 10.[0-5]/.test(ua)) { - return 'oldmac'; - } - if (pf.indexOf('iPhone') !== -1 || - pf.indexOf('iPad') !== -1 || - pf.indexOf('iPod') !== -1 ) { - return 'ios'; - } - if (ua.indexOf("Mac OS X") !== -1) { - return 'osx'; - } - if (ua.indexOf("MSIE 5.2") !== -1) { - return 'oldmac'; - } - if (pf.indexOf("Mac") !== -1) { - return 'oldmac'; - } - if (navigator.platform === '' && - navigator.userAgent.indexOf("Firefox") !== -1 && - navigator.userAgent.indexOf("Mobile") !== -1) { - return 'fxos'; - } - - return 'other'; - } - (function () { - // Immediately set the platform classname on the html-element - // to avoid lots of flickering - var h = document.documentElement; - window.site = { - platform : getPlatform() - }; - h.className = window.site.platform; - })(); - })(); diff --git a/core/app/helpers/core_helper.rb b/core/app/helpers/core_helper.rb deleted file mode 100644 index a6c7479..0000000 --- a/core/app/helpers/core_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -# -# Misc. helpers needed throughout. -# -module CoreHelper - - # - # insert common buttons (download, login, etc) - # - def home_page_buttons(on_user_page = false) - render 'common/home_page_buttons', {:on_user_page => on_user_page} - end - -end diff --git a/core/app/helpers/download_helper.rb b/core/app/helpers/download_helper.rb deleted file mode 100644 index ee0fe73..0000000 --- a/core/app/helpers/download_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -module DownloadHelper - - def alternative_client_links(os = nil) - alternative_clients(os).map do |client| - link_to(I18n.t("os."+client), client_download_url(client)) - end - end - - def alternative_clients(os = nil) - available_clients - [os] - end - - def client_download_url(os = nil) - client_download_domain + client_download_path(os) - end - - def client_download_path(os) - download_paths[os.to_s] || download_paths['other'] || '' - end - - def available_clients - APP_CONFIG[:available_clients] || [] - end - - def client_download_domain - APP_CONFIG[:client_download_domain] || '' - end - - def download_paths - APP_CONFIG[:download_paths] || {} - end - -end diff --git a/core/app/helpers/navigation_helper.rb b/core/app/helpers/navigation_helper.rb deleted file mode 100644 index 19cb934..0000000 --- a/core/app/helpers/navigation_helper.rb +++ /dev/null @@ -1,82 +0,0 @@ -module NavigationHelper - - # - # used to create a side navigation link. - # - # Signature is the same as link_to, except it accepts an :active value in the html_options - # - def link_to_navigation(*args) - if args.last.is_a? Hash - html_options = args.pop.dup - active_class = html_options.delete(:active) ? 'active' : nil - html_options[:class] = [html_options[:class], active_class].join(' ') - args << html_options - else - active_class = nil - end - content_tag :li, :class => active_class do - link_to(*args) - end - end - - # - # returns true if params[:action] matches one of the args. - # - def action?(*actions) - actions.detect do |action| - if action.is_a? String - action == action_string - elsif action.is_a? Symbol - if action == :none - action_string == nil - else - action == action_symbol - end - end - end - end - - # - # returns true if params[:controller] matches one of the args. - # - # for example: - # controller?(:me, :home) - # controller?('groups/') <-- matches any controller in namespace 'groups' - # - def controller?(*controllers) - controllers.each do |cntr| - if cntr.is_a? String - if cntr.ends_with?('/') - return true if controller_string.starts_with?(cntr.chop) - end - return true if cntr == controller_string - elsif cntr.is_a? Symbol - return true if cntr == controller_symbol - end - end - return false - end - - private - - def controller_string - @controller_string ||= params[:controller].to_s.gsub(/^\//, '') - end - - def controller_symbol - @controller_symbol ||= params[:controller].gsub(/^\//,'').gsub('/','_').to_sym - end - - def action_string - params[:action] - end - - def action_symbol - @action_symbol ||= if params[:action].present? - params[:action].to_sym - else - nil - end - end - -end diff --git a/core/app/views/common/_download_for_os.html.haml b/core/app/views/common/_download_for_os.html.haml deleted file mode 100644 index 3a11d10..0000000 --- a/core/app/views/common/_download_for_os.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- os = download_for_os -%div{:class => "os-#{os}"} - %span.link - - btn_class = (os == "other") ? "disabled" : "btn-primary" - = link_to client_download_url(os), :class => "btn btn-large #{btn_class}" do - = big_icon('download') - .pull-right - = t(:download_client) - %br/ - %small= I18n.t("os.#{os}") - %span.info - %div= t(:client_info, :provider => content_tag(:b,APP_CONFIG[:domain])).html_safe - %div - - if os == "other" - = t(:all_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe - - else - = t(:other_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe diff --git a/core/app/views/common/_home_page_buttons.html.haml b/core/app/views/common/_home_page_buttons.html.haml deleted file mode 100644 index c9ea7a2..0000000 --- a/core/app/views/common/_home_page_buttons.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -- icon_color = :black - -.home-buttons - .row-fluid.first - .span2 - .download.span8 - = render partial: 'common/download_for_os', collection: available_clients + ['other'] - .span2 - - if local_assigns[:divider] - .row-fluid - .span12 - = render local_assigns[:divider] - - if !local_assigns[:on_user_page] - .row-fluid.second - .login.span4 - %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') - %span.info= t(:login_info) - .signup.span4 - %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') - %span.info= t(:signup_info) - .help.span4 - %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') - %span.info= t(:help_info) diff --git a/core/app/views/kaminari/_first_page.html.haml b/core/app/views/kaminari/_first_page.html.haml deleted file mode 100644 index 34436e3..0000000 --- a/core/app/views/kaminari/_first_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "First" page --# available local variables --# url: url to the first page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/core/app/views/kaminari/_gap.html.haml b/core/app/views/kaminari/_gap.html.haml deleted file mode 100644 index 51de678..0000000 --- a/core/app/views/kaminari/_gap.html.haml +++ /dev/null @@ -1,8 +0,0 @@ --# Non-link tag that stands for skipped pages... --# available local variables --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li.disabled - = raw(t 'views.pagination.truncate') diff --git a/core/app/views/kaminari/_last_page.html.haml b/core/app/views/kaminari/_last_page.html.haml deleted file mode 100644 index c90433c..0000000 --- a/core/app/views/kaminari/_last_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Last" page --# available local variables --# url: url to the last page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%li - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/core/app/views/kaminari/_next_page.html.haml b/core/app/views/kaminari/_next_page.html.haml deleted file mode 100644 index ea6cab2..0000000 --- a/core/app/views/kaminari/_next_page.html.haml +++ /dev/null @@ -1,12 +0,0 @@ --# Link to the "Next" page --# available local variables --# url: url to the next page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -- if current_page.last? - %li.disabled - %span= raw(t 'views.pagination.next') -- else - %li= link_to(raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote) diff --git a/core/app/views/kaminari/_page.html.haml b/core/app/views/kaminari/_page.html.haml deleted file mode 100644 index 2f2f142..0000000 --- a/core/app/views/kaminari/_page.html.haml +++ /dev/null @@ -1,14 +0,0 @@ --# Link showing page number --# available local variables --# page: a page object for "this" page --# url: url to this page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote - -- if page.current? - %li.active - %span= page -- else - %li= link_to(page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}) diff --git a/core/app/views/kaminari/_paginator.html.haml b/core/app/views/kaminari/_paginator.html.haml deleted file mode 100644 index 79c5b92..0000000 --- a/core/app/views/kaminari/_paginator.html.haml +++ /dev/null @@ -1,19 +0,0 @@ --# The container tag --# available local variables --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote --# paginator: the paginator that renders the pagination tags inside -= paginator.render do - .pagination - %ul - -#= first_page_tag unless current_page.first? - = prev_page_tag #unless current_page.first? - - each_page do |page| - - if page.left_outer? || page.right_outer? || page.inside_window? - = page_tag page - - elsif !page.was_truncated? - = gap_tag - = next_page_tag #unless current_page.last? - -#= last_page_tag unless current_page.last? diff --git a/core/app/views/kaminari/_prev_page.html.haml b/core/app/views/kaminari/_prev_page.html.haml deleted file mode 100644 index d274bf4..0000000 --- a/core/app/views/kaminari/_prev_page.html.haml +++ /dev/null @@ -1,12 +0,0 @@ --# Link to the "Previous" page --# available local variables --# url: url to the previous page --# current_page: a page object for the currently displayed page --# num_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -- if current_page.first? - %li.disabled - %span= raw(t 'views.pagination.previous') -- else - %li= link_to(raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote) diff --git a/core/config/initializers/backtrace_silencers.rb b/core/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 59385cd..0000000 --- a/core/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! diff --git a/core/config/initializers/couchrest_model.rb b/core/config/initializers/couchrest_model.rb deleted file mode 100644 index ce4f41a..0000000 --- a/core/config/initializers/couchrest_model.rb +++ /dev/null @@ -1,3 +0,0 @@ -CouchRest::Model::Base.configure do |config| - config.auto_update_design_doc = false -end diff --git a/core/config/initializers/inflections.rb b/core/config/initializers/inflections.rb deleted file mode 100644 index 5d8d9be..0000000 --- a/core/config/initializers/inflections.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end -# -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| -# inflect.acronym 'RESTful' -# end diff --git a/core/config/initializers/mime_types.rb b/core/config/initializers/mime_types.rb deleted file mode 100644 index 72aca7e..0000000 --- a/core/config/initializers/mime_types.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/core/config/initializers/simple_form.rb b/core/config/initializers/simple_form.rb deleted file mode 100644 index e3f8d09..0000000 --- a/core/config/initializers/simple_form.rb +++ /dev/null @@ -1,142 +0,0 @@ -# Use this setup block to configure all options available in SimpleForm. -SimpleForm.setup do |config| - # Wrappers are used by the form builder to generate a - # complete input. You can remove any component from the - # wrapper, change the order or even add your own to the - # stack. The options given below are used to wrap the - # whole input. - config.wrappers :default, :class => :input, - :hint_class => :field_with_hint, :error_class => :field_with_errors do |b| - ## Extensions enabled by default - # Any of these extensions can be disabled for a - # given input by passing: `f.input EXTENSION_NAME => false`. - # You can make any of these extensions optional by - # renaming `b.use` to `b.optional`. - - # Determines whether to use HTML5 (:email, :url, ...) - # and required attributes - b.use :html5 - - # Calculates placeholders automatically from I18n - # You can also pass a string as f.input :placeholder => "Placeholder" - b.use :placeholder - - ## Optional extensions - # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup` - # to the input. If so, they will retrieve the values from the model - # if any exists. If you want to enable the lookup for any of those - # extensions by default, you can change `b.optional` to `b.use`. - - # Calculates maxlength from length validations for string inputs - b.optional :maxlength - - # Calculates pattern from format validations for string inputs - b.optional :pattern - - # Calculates min and max from length validations for numeric inputs - b.optional :min_max - - # Calculates readonly automatically from readonly attributes - b.optional :readonly - - ## Inputs - b.use :label_input - b.use :hint, :wrap_with => { :tag => :span, :class => :hint } - b.use :error, :wrap_with => { :tag => :span, :class => :error } - end - - # The default wrapper to be used by the FormBuilder. - config.default_wrapper = :default - - # Define the way to render check boxes / radio buttons with labels. - # Defaults to :nested for bootstrap config. - # :inline => input + label - # :nested => label > input - config.boolean_style = :nested - - # Default class for buttons - config.button_class = 'btn' - - # Method used to tidy up errors. Specify any Rails Array method. - # :first lists the first message for each field. - # Use :to_sentence to list all errors for each field. - # config.error_method = :first - - # Default tag used for error notification helper. - config.error_notification_tag = :div - - # CSS class to add for error notification helper. - config.error_notification_class = 'alert alert-error' - - # ID to add for error notification helper. - # config.error_notification_id = nil - - # Series of attempts to detect a default label method for collection. - # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] - - # Series of attempts to detect a default value method for collection. - # config.collection_value_methods = [ :id, :to_s ] - - # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. - # config.collection_wrapper_tag = nil - - # You can define the class to use on all collection wrappers. Defaulting to none. - # config.collection_wrapper_class = nil - - # You can wrap each item in a collection of radio/check boxes with a tag, - # defaulting to :span. Please note that when using :boolean_style = :nested, - # SimpleForm will force this option to be a label. - # config.item_wrapper_tag = :span - - # You can define a class to use in all item wrappers. Defaulting to none. - # config.item_wrapper_class = nil - - # How the label text should be generated altogether with the required text. - # config.label_text = lambda { |label, required| "#{required} #{label}" } - - # You can define the class to use on all labels. Default is nil. - config.label_class = 'control-label' - - # You can define the class to use on all forms. Default is simple_form. - # config.form_class = :simple_form - - # You can define which elements should obtain additional classes - # config.generate_additional_classes_for = [:wrapper, :label, :input] - - # Whether attributes are required by default (or not). Default is true. - # config.required_by_default = true - - # Tell browsers whether to use default HTML5 validations (novalidate option). - # Default is enabled. - config.browser_validations = false - - # Collection of methods to detect if a file type was given. - # config.file_methods = [ :mounted_as, :file?, :public_filename ] - - # Custom mappings for input types. This should be a hash containing a regexp - # to match as key, and the input type that will be used when the field name - # matches the regexp as value. - # config.input_mappings = { /count/ => :integer } - - # Custom wrappers for input types. This should be a hash containing an input - # type as key and the wrapper that will be used for all inputs with specified type. - # config.wrapper_mappings = { :string => :prepend } - - # Default priority for time_zone inputs. - # config.time_zone_priority = nil - - # Default priority for country inputs. - # config.country_priority = nil - - # Default size for text inputs. - # config.default_input_size = 50 - - # When false, do not use translations for labels. - # config.translate_labels = true - - # Automatically discover new inputs in Rails' autoload path. - # config.inputs_discovery = true - - # Cache SimpleForm inputs discovery - # config.cache_discovery = !Rails.env.development? -end diff --git a/core/config/initializers/simple_form_bootstrap.rb b/core/config/initializers/simple_form_bootstrap.rb deleted file mode 100644 index c949f5e..0000000 --- a/core/config/initializers/simple_form_bootstrap.rb +++ /dev/null @@ -1,57 +0,0 @@ -# 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| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' 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| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' do |input| - input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend| - prepend.use :input - end - input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } - input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - end - end - - config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' do |input| - input.wrapper :tag => 'div', :class => 'input-append' do |append| - append.use :input - end - input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } - input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - end - end - - # - # when you don't want any bootstrap "control-group" or "controls" wrappers. - # - config.wrappers :none, :tag => 'div', :error_class => 'error' do |b| - b.use :html5 - b.use :placeholder - b.use :label - b.use :input - b.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } - end - - # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. - # Check the Bootstrap docs (http://twitter.github.com/bootstrap) - # to learn about the different styles for forms and inputs, - # buttons and other elements. - config.default_wrapper = :bootstrap -end diff --git a/core/config/initializers/validations.rb b/core/config/initializers/validations.rb deleted file mode 100644 index e8acfbe..0000000 --- a/core/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/core/config/initializers/wrap_parameters.rb b/core/config/initializers/wrap_parameters.rb deleted file mode 100644 index 5fe232e..0000000 --- a/core/config/initializers/wrap_parameters.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains settings for ActionController::ParamsWrapper which -# is enabled by default. - -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters :format => [:json] -end - diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml deleted file mode 100644 index bb510d4..0000000 --- a/core/config/locales/en.yml +++ /dev/null @@ -1,41 +0,0 @@ -en: - no_such_thing: "No such %{thing}." - thing_was_successfully_created: "%{thing} was successfully created." - create_thing: "Create %{thing}" - - overview: "Overview" - user_control_panel: "user control panel" - - created: "Created" - created_by_on: "Created by %{user} on %{time}" - updated: "Updated" - - none: "None" - unknown: "Unknown" - admin: "Admin" - anonymous: "Anonymous" - save: "Save" - add: "Add" - remove: "Remove" - changes_saved: "Changes saved successfully." - are_you_sure: "Are you sure? This change cannot be undone." - - download_client: "Download Bitmask" - client_info: "The Bitmask application allows you to use %{provider} services." - all_downloads_info: "It is available for %{clients}." - other_downloads_info: "Bitmask is also available for %{clients}." - login_info: "Log in to change your account settings, create support tickets, and manage payments." - signup_info: "Sign up for a new user account via this website (it is better if you use the Bitmask application to sign up, but this website works too)." - welcome: "Welcome to %{provider}." - get_help: "Get Help" - help_info: "Can't login? Create a new support ticket anonymously." - example_email: 'user@domain.org' - os: - linux32: "Linux (32 bit)" - linux64: "Linux (64 bit)" - linux: "GNU/Linux" - windows: "Windows" - android: "Android" - osx: "Mac OS" - other: "(not available for your OS)" - diff --git a/core/config/locales/simple_form.en.yml b/core/config/locales/simple_form.en.yml deleted file mode 100644 index 0df11fe..0000000 --- a/core/config/locales/simple_form.en.yml +++ /dev/null @@ -1,26 +0,0 @@ -en: - simple_form: - "yes": 'Yes' - "no": 'No' - required: - text: 'required' - mark: '*' - # You can uncomment the line below if you need to overwrite the whole required html. - # When using html, text and mark won't be used. - # html: '*' - error_notification: - default_message: "Please review the problems below:" - # Labels and hints examples - # labels: - # defaults: - # password: 'Password' - # user: - # new: - # email: 'E-mail to sign in.' - # edit: - # email: 'E-mail.' - # hints: - # defaults: - # username: 'User name to sign in.' - # password: 'No special characters, please.' - diff --git a/core/config/routes.rb b/core/config/routes.rb deleted file mode 100644 index 1daf9a4..0000000 --- a/core/config/routes.rb +++ /dev/null @@ -1,2 +0,0 @@ -Rails.application.routes.draw do -end diff --git a/core/leap_web_core.gemspec b/core/leap_web_core.gemspec deleted file mode 100644 index 7ca4d90..0000000 --- a/core/leap_web_core.gemspec +++ /dev/null @@ -1,25 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "leap_web_core" - s.version = LeapWeb::VERSION - s.authors = ["Azul"] - s.email = ["azul@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "Web interface to the leap platform - core engine" - s.description = "This web interface provides various administrative tools for the leap platform through plugins. Currently it manages user accounts and certificates." - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"] - s.test_files = Dir["test/**/*"] - - s.add_dependency "rails", "~> 3.2.11" - - s.add_dependency "couchrest", "~> 1.1.3" - s.add_dependency "couchrest_model", "~> 2.0.0" - s.add_dependency "couchrest_session_store", "~> 0.2.4" - - s.add_dependency "json" -end diff --git a/core/lib/extensions/couchrest.rb b/core/lib/extensions/couchrest.rb deleted file mode 100644 index a9a195e..0000000 --- a/core/lib/extensions/couchrest.rb +++ /dev/null @@ -1,95 +0,0 @@ -module CouchRest - module Model - module Designs - - class View - - # so we can called Ticket.method.descending or Ticket.method.ascending - def ascending - self - end - end - - class DesignMapper - def load_views(dir) - Dir.glob("#{dir}/*.js") do |js| - name = File.basename(js, '.js') - file = File.open(js, 'r') - view name.to_sym, - :map => file.read, - :reduce => "function(key, values, rereduce) { return sum(values); }" - end - end - end - end - - module Connection - - module ClassMethods - - def use_database(db) - @database = prepare_database(db) - rescue RestClient::Exception, - Errno::EHOSTUNREACH, - Errno::ECONNREFUSED => e - message = "Could not connect to couch database #{db} due to #{e.to_s}" - Rails.logger.warn message - raise e.class.new(message) if APP_CONFIG[:reraise_errors] - end - end - - end - - module Utils - module Migrate - def self.load_all_models_with_engines - self.load_all_models_without_engines - return unless defined?(Rails) - Dir[Rails.root + '*/app/models/**/*.rb'].each do |path| - require path - end - end - - class << self - alias_method_chain :load_all_models, :engines - end - - def dump_all_models - prepare_directory - find_models.each do |model| - model.design_docs.each do |design| - dump_design(model, design) - end - end - end - - protected - - def dump_design(model, design) - dir = prepare_directory model.name.tableize - filename = design.id.sub('_design/','') + '.json' - puts dir + filename - design.checksum - File.open(dir + filename, "w") do |file| - file.write(JSON.pretty_generate(design.to_hash)) - end - end - - def prepare_directory(dir = '') - dir = Rails.root + 'tmp' + 'designs' + dir - Dir.mkdir(dir) unless Dir.exists?(dir) - return dir - end - - end - end - - end - - class ModelRailtie - config.action_dispatch.rescue_responses.merge!( - 'CouchRest::Model::DocumentNotFound' => :not_found, - 'RestClient::ResourceNotFound' => :not_found - ) - end -end diff --git a/core/lib/extensions/testing.rb b/core/lib/extensions/testing.rb deleted file mode 100644 index 8f7e73c..0000000 --- a/core/lib/extensions/testing.rb +++ /dev/null @@ -1,48 +0,0 @@ -module LeapWebCore - module AssertResponses - - # response that works with different TestCases: - # ActionController::TestCase has @response - # ActionDispatch::IntegrationTest has @response - # Rack::Test::Methods defines last_response - def get_response - @response || last_response - end - - def assert_attachement_filename(name) - assert_equal %Q(attachment; filename="#{name}"), - get_response.headers["Content-Disposition"] - end - - def json_response - response = JSON.parse(get_response.body) - response.respond_to?(:with_indifferent_access) ? - response.with_indifferent_access : - response - end - - def assert_json_response(object) - assert_equal 'application/json', - get_response.content_type.to_s.split(';').first - if object.is_a? Hash - object.stringify_keys! if object.respond_to? :stringify_keys! - assert_equal object, json_response - else - assert_equal object.to_json, get_response.body - end - end - - def assert_json_error(object) - object.stringify_keys! if object.respond_to? :stringify_keys! - assert_json_response :errors => object - end - end -end - -class ::ActionController::TestCase - include LeapWebCore::AssertResponses -end - -class ::ActionDispatch::IntegrationTest - include LeapWebCore::AssertResponses -end diff --git a/core/lib/leap_web_core.rb b/core/lib/leap_web_core.rb deleted file mode 100644 index a58d140..0000000 --- a/core/lib/leap_web_core.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "rails" - -require "couchrest" -require "couchrest_model" -require "couchrest_session_store" - -require "json" - -require "extensions/testing" -require "extensions/couchrest" -require "leap_web_core/engine" - -module LeapWebCore -end diff --git a/core/lib/leap_web_core/dependencies.rb b/core/lib/leap_web_core/dependencies.rb deleted file mode 100644 index 877e3d1..0000000 --- a/core/lib/leap_web_core/dependencies.rb +++ /dev/null @@ -1,40 +0,0 @@ -module LeapWebCore - class Dependencies - UI_DEV = { - "haml-rails" => "~> 0.3.4", - "sass-rails" => "~> 3.2.5", - "coffee-rails" => "~> 3.2.2", - "uglifier" => "~> 1.2.7" - } - - UI = { - "haml" => "~> 3.1.7", - "jquery-rails" => nil, - "simple_form" => nil, - "bootswatch-rails", "~> 0.5.0" - } - - def self.require_ui_gems - UI.keys.each {|dep| require dep} - if Rails.env == "development" - # This will be run in the app including plugins that run it. - # However not all development_dependencies might be present. - # So we better only require those that are. - available = Bundler.definition.specs.map(&:name) - gems_to_require = available & UI_DEV.keys - gems_to_require.each {|dep| require dep} - end - end - - def self.add_ui_gems_to_spec(spec) - UI.each do |dep, version| - spec.add_dependency dep, version - end - - UI_DEV.each do |dep, version| - spec.add_development_dependency dep, version - end - end - - end -end diff --git a/core/lib/leap_web_core/engine.rb b/core/lib/leap_web_core/engine.rb deleted file mode 100644 index 940b5e2..0000000 --- a/core/lib/leap_web_core/engine.rb +++ /dev/null @@ -1,9 +0,0 @@ -# thou shall require all your dependencies in an engine. -require "couchrest" -require "couchrest_model" - -module LeapWebCore - class Engine < ::Rails::Engine - - end -end diff --git a/core/lib/leap_web_core/ui_dependencies.rb b/core/lib/leap_web_core/ui_dependencies.rb deleted file mode 100644 index 2daee37..0000000 --- a/core/lib/leap_web_core/ui_dependencies.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "haml" -require "jquery-rails" -require "simple_form" -require "bootswatch-rails" - -if Rails.env == "development" - require "haml-rails" - require "sass-rails" - require "coffee-rails" - require "uglifier" -end diff --git a/core/lib/tasks/leap_web_core_tasks.rake b/core/lib/tasks/leap_web_core_tasks.rake deleted file mode 100644 index ec6abac..0000000 --- a/core/lib/tasks/leap_web_core_tasks.rake +++ /dev/null @@ -1,25 +0,0 @@ -namespace :couchrest do - - desc "Dump all the design docs found in each model" - task :dump => :environment do - CouchRest::Model::Utils::Migrate.load_all_models - CouchRest::Model::Utils::Migrate.dump_all_models - end -end - -namespace :cleanup do - - desc "Cleanup all expired session documents" - task :sessions => :environment do - # make sure this is the same as in - # config/initializers/session_store.rb - store = CouchRest::Session::Store.new expire_after: 1800 - store.cleanup(store.expired) - end - - desc "Cleanup all expired tokens" - task :tokens => :environment do - Token.destroy_all_expired - end -end - diff --git a/core/script/rails b/core/script/rails deleted file mode 100755 index c2ad538..0000000 --- a/core/script/rails +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby1.8 -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/leap_web_core/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' diff --git a/core/test/support/browser_integration_test.rb b/core/test/support/browser_integration_test.rb deleted file mode 100644 index 2885c3a..0000000 --- a/core/test/support/browser_integration_test.rb +++ /dev/null @@ -1,81 +0,0 @@ -# -# BrowserIntegrationTest -# -# Use this class for capybara based integration tests for the ui. -# - -class BrowserIntegrationTest < ActionDispatch::IntegrationTest - - CONFIG_RU = (Rails.root + 'config.ru').to_s - OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first - - require 'capybara/poltergeist' - - Capybara.register_driver :rack_test do |app| - Capybara::RackTest::Driver.new(app) - end - - Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app) - end - - # this is integration testing. So let's make the whole - # rack stack available... - Capybara.app = OUTER_APP - Capybara.run_server = true - Capybara.app_host = 'http://lvh.me:3003' - Capybara.server_port = 3003 - Capybara.javascript_driver = :poltergeist - Capybara.default_wait_time = 5 - - - # Make the Capybara DSL available - include Capybara::DSL - - setup do - Capybara.current_driver = Capybara.javascript_driver - page.driver.add_headers 'ACCEPT-LANGUAGE' => 'en-EN' - end - - teardown do - Capybara.reset_sessions! # Forget the (simulated) browser state - Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver - end - - def submit_signup(username = nil, password = nil) - 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 - - add_teardown_hook do |testcase| - unless testcase.passed? - testcase.save_state - end - end - - def save_state - page.save_screenshot screenshot_path - File.open(logfile_path, 'w') do |test_log| - test_log.puts self.class.name - test_log.puts "=========================" - test_log.puts __name__ - test_log.puts Time.now - test_log.puts current_path - test_log.puts page.status_code - test_log.puts page.response_headers - test_log.puts "page.html" - test_log.puts "------------------------" - test_log.puts page.html - test_log.puts "server log" - test_log.puts "------------------------" - test_log.puts `tail log/test.log -n 200` - end - end - -end diff --git a/core/test/support/rack_test.rb b/core/test/support/rack_test.rb deleted file mode 100644 index 2d8e5c4..0000000 --- a/core/test/support/rack_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -class RackTest < ActiveSupport::TestCase - include Rack::Test::Methods - include Warden::Test::Helpers - include LeapWebCore::AssertResponses - - CONFIG_RU = (Rails.root + 'config.ru').to_s - OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first - - def app - OUTER_APP - end - - def assert_access_denied - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity - end - - # inspired by rails 4 - # -> actionpack/lib/action_dispatch/testing/assertions/response.rb - def assert_response(type, message = nil) - # RackTest does not know @response - response_code = last_response.status - message ||= "Expected response to be a <#{type}>, but was <#{response_code}>" - - if Symbol === type - if [:success, :missing, :redirect, :error].include?(type) - assert last_response.send("#{type}?"), message - else - code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] - assert_equal code, response_code, message - end - else - assert_equal type, response_code, message - end - end - -end diff --git a/core/test/support/with_config_helper.rb b/core/test/support/with_config_helper.rb deleted file mode 100644 index 65eb7bc..0000000 --- a/core/test/support/with_config_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -module WithConfigHelper - extend ActiveSupport::Concern - - def with_config(options) - old_config = APP_CONFIG.dup - APP_CONFIG.merge! options - yield - ensure - APP_CONFIG.replace old_config - end - -end - -class ActiveSupport::TestCase - include WithConfigHelper -end diff --git a/help/leap_web_help.gemspec b/help/leap_web_help.gemspec index 4914694..ac6d78d 100644 --- a/help/leap_web_help.gemspec +++ b/help/leap_web_help.gemspec @@ -15,5 +15,4 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] - s.add_dependency "leap_web_core", LeapWeb::VERSION end diff --git a/help/lib/leap_web_help/engine.rb b/help/lib/leap_web_help/engine.rb index 4146dfc..dfa763f 100644 --- a/help/lib/leap_web_help/engine.rb +++ b/help/lib/leap_web_help/engine.rb @@ -1,7 +1,3 @@ -# thou shall require all your dependencies in an engine. -require "leap_web_core" -require "leap_web_core/ui_dependencies" - module LeapWebHelp class Engine < ::Rails::Engine end diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb new file mode 100644 index 0000000..a9a195e --- /dev/null +++ b/lib/extensions/couchrest.rb @@ -0,0 +1,95 @@ +module CouchRest + module Model + module Designs + + class View + + # so we can called Ticket.method.descending or Ticket.method.ascending + def ascending + self + end + end + + class DesignMapper + def load_views(dir) + Dir.glob("#{dir}/*.js") do |js| + name = File.basename(js, '.js') + file = File.open(js, 'r') + view name.to_sym, + :map => file.read, + :reduce => "function(key, values, rereduce) { return sum(values); }" + end + end + end + end + + module Connection + + module ClassMethods + + def use_database(db) + @database = prepare_database(db) + rescue RestClient::Exception, + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED => e + message = "Could not connect to couch database #{db} due to #{e.to_s}" + Rails.logger.warn message + raise e.class.new(message) if APP_CONFIG[:reraise_errors] + end + end + + end + + module Utils + module Migrate + def self.load_all_models_with_engines + self.load_all_models_without_engines + return unless defined?(Rails) + Dir[Rails.root + '*/app/models/**/*.rb'].each do |path| + require path + end + end + + class << self + alias_method_chain :load_all_models, :engines + end + + def dump_all_models + prepare_directory + find_models.each do |model| + model.design_docs.each do |design| + dump_design(model, design) + end + end + end + + protected + + def dump_design(model, design) + dir = prepare_directory model.name.tableize + filename = design.id.sub('_design/','') + '.json' + puts dir + filename + design.checksum + File.open(dir + filename, "w") do |file| + file.write(JSON.pretty_generate(design.to_hash)) + end + end + + def prepare_directory(dir = '') + dir = Rails.root + 'tmp' + 'designs' + dir + Dir.mkdir(dir) unless Dir.exists?(dir) + return dir + end + + end + end + + end + + class ModelRailtie + config.action_dispatch.rescue_responses.merge!( + 'CouchRest::Model::DocumentNotFound' => :not_found, + 'RestClient::ResourceNotFound' => :not_found + ) + end +end diff --git a/lib/extensions/testing.rb b/lib/extensions/testing.rb new file mode 100644 index 0000000..8f7e73c --- /dev/null +++ b/lib/extensions/testing.rb @@ -0,0 +1,48 @@ +module LeapWebCore + module AssertResponses + + # response that works with different TestCases: + # ActionController::TestCase has @response + # ActionDispatch::IntegrationTest has @response + # Rack::Test::Methods defines last_response + def get_response + @response || last_response + end + + def assert_attachement_filename(name) + assert_equal %Q(attachment; filename="#{name}"), + get_response.headers["Content-Disposition"] + end + + def json_response + response = JSON.parse(get_response.body) + response.respond_to?(:with_indifferent_access) ? + response.with_indifferent_access : + response + end + + def assert_json_response(object) + assert_equal 'application/json', + get_response.content_type.to_s.split(';').first + if object.is_a? Hash + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_equal object, json_response + else + assert_equal object.to_json, get_response.body + end + end + + def assert_json_error(object) + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_json_response :errors => object + end + end +end + +class ::ActionController::TestCase + include LeapWebCore::AssertResponses +end + +class ::ActionDispatch::IntegrationTest + include LeapWebCore::AssertResponses +end diff --git a/lib/leap_web_core.rb b/lib/leap_web_core.rb new file mode 100644 index 0000000..a58d140 --- /dev/null +++ b/lib/leap_web_core.rb @@ -0,0 +1,14 @@ +require "rails" + +require "couchrest" +require "couchrest_model" +require "couchrest_session_store" + +require "json" + +require "extensions/testing" +require "extensions/couchrest" +require "leap_web_core/engine" + +module LeapWebCore +end diff --git a/lib/leap_web_core/dependencies.rb b/lib/leap_web_core/dependencies.rb new file mode 100644 index 0000000..877e3d1 --- /dev/null +++ b/lib/leap_web_core/dependencies.rb @@ -0,0 +1,40 @@ +module LeapWebCore + class Dependencies + UI_DEV = { + "haml-rails" => "~> 0.3.4", + "sass-rails" => "~> 3.2.5", + "coffee-rails" => "~> 3.2.2", + "uglifier" => "~> 1.2.7" + } + + UI = { + "haml" => "~> 3.1.7", + "jquery-rails" => nil, + "simple_form" => nil, + "bootswatch-rails", "~> 0.5.0" + } + + def self.require_ui_gems + UI.keys.each {|dep| require dep} + if Rails.env == "development" + # This will be run in the app including plugins that run it. + # However not all development_dependencies might be present. + # So we better only require those that are. + available = Bundler.definition.specs.map(&:name) + gems_to_require = available & UI_DEV.keys + gems_to_require.each {|dep| require dep} + end + end + + def self.add_ui_gems_to_spec(spec) + UI.each do |dep, version| + spec.add_dependency dep, version + end + + UI_DEV.each do |dep, version| + spec.add_development_dependency dep, version + end + end + + end +end diff --git a/lib/leap_web_core/engine.rb b/lib/leap_web_core/engine.rb new file mode 100644 index 0000000..940b5e2 --- /dev/null +++ b/lib/leap_web_core/engine.rb @@ -0,0 +1,9 @@ +# thou shall require all your dependencies in an engine. +require "couchrest" +require "couchrest_model" + +module LeapWebCore + class Engine < ::Rails::Engine + + end +end diff --git a/lib/leap_web_core/ui_dependencies.rb b/lib/leap_web_core/ui_dependencies.rb new file mode 100644 index 0000000..2daee37 --- /dev/null +++ b/lib/leap_web_core/ui_dependencies.rb @@ -0,0 +1,11 @@ +require "haml" +require "jquery-rails" +require "simple_form" +require "bootswatch-rails" + +if Rails.env == "development" + require "haml-rails" + require "sass-rails" + require "coffee-rails" + require "uglifier" +end diff --git a/lib/tasks/leap_web_core_tasks.rake b/lib/tasks/leap_web_core_tasks.rake new file mode 100644 index 0000000..ec6abac --- /dev/null +++ b/lib/tasks/leap_web_core_tasks.rake @@ -0,0 +1,25 @@ +namespace :couchrest do + + desc "Dump all the design docs found in each model" + task :dump => :environment do + CouchRest::Model::Utils::Migrate.load_all_models + CouchRest::Model::Utils::Migrate.dump_all_models + end +end + +namespace :cleanup do + + desc "Cleanup all expired session documents" + task :sessions => :environment do + # make sure this is the same as in + # config/initializers/session_store.rb + store = CouchRest::Session::Store.new expire_after: 1800 + store.cleanup(store.expired) + end + + desc "Cleanup all expired tokens" + task :tokens => :environment do + Token.destroy_all_expired + end +end + diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb new file mode 100644 index 0000000..2885c3a --- /dev/null +++ b/test/support/browser_integration_test.rb @@ -0,0 +1,81 @@ +# +# BrowserIntegrationTest +# +# Use this class for capybara based integration tests for the ui. +# + +class BrowserIntegrationTest < ActionDispatch::IntegrationTest + + CONFIG_RU = (Rails.root + 'config.ru').to_s + OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first + + require 'capybara/poltergeist' + + Capybara.register_driver :rack_test do |app| + Capybara::RackTest::Driver.new(app) + end + + Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app) + end + + # this is integration testing. So let's make the whole + # rack stack available... + Capybara.app = OUTER_APP + Capybara.run_server = true + Capybara.app_host = 'http://lvh.me:3003' + Capybara.server_port = 3003 + Capybara.javascript_driver = :poltergeist + Capybara.default_wait_time = 5 + + + # Make the Capybara DSL available + include Capybara::DSL + + setup do + Capybara.current_driver = Capybara.javascript_driver + page.driver.add_headers 'ACCEPT-LANGUAGE' => 'en-EN' + end + + teardown do + Capybara.reset_sessions! # Forget the (simulated) browser state + Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver + end + + def submit_signup(username = nil, password = nil) + 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 + + add_teardown_hook do |testcase| + unless testcase.passed? + testcase.save_state + end + end + + def save_state + page.save_screenshot screenshot_path + File.open(logfile_path, 'w') do |test_log| + test_log.puts self.class.name + test_log.puts "=========================" + test_log.puts __name__ + test_log.puts Time.now + test_log.puts current_path + test_log.puts page.status_code + test_log.puts page.response_headers + test_log.puts "page.html" + test_log.puts "------------------------" + test_log.puts page.html + test_log.puts "server log" + test_log.puts "------------------------" + test_log.puts `tail log/test.log -n 200` + end + end + +end diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb new file mode 100644 index 0000000..2d8e5c4 --- /dev/null +++ b/test/support/rack_test.rb @@ -0,0 +1,37 @@ +class RackTest < ActiveSupport::TestCase + include Rack::Test::Methods + include Warden::Test::Helpers + include LeapWebCore::AssertResponses + + CONFIG_RU = (Rails.root + 'config.ru').to_s + OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first + + def app + OUTER_APP + end + + def assert_access_denied + assert_json_response('error' => I18n.t(:not_authorized)) + assert_response :unprocessable_entity + end + + # inspired by rails 4 + # -> actionpack/lib/action_dispatch/testing/assertions/response.rb + def assert_response(type, message = nil) + # RackTest does not know @response + response_code = last_response.status + message ||= "Expected response to be a <#{type}>, but was <#{response_code}>" + + if Symbol === type + if [:success, :missing, :redirect, :error].include?(type) + assert last_response.send("#{type}?"), message + else + code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] + assert_equal code, response_code, message + end + else + assert_equal type, response_code, message + end + end + +end diff --git a/test/support/with_config_helper.rb b/test/support/with_config_helper.rb new file mode 100644 index 0000000..65eb7bc --- /dev/null +++ b/test/support/with_config_helper.rb @@ -0,0 +1,16 @@ +module WithConfigHelper + extend ActiveSupport::Concern + + def with_config(options) + old_config = APP_CONFIG.dup + APP_CONFIG.merge! options + yield + ensure + APP_CONFIG.replace old_config + end + +end + +class ActiveSupport::TestCase + include WithConfigHelper +end diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index 7d1f220..630c095 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -15,8 +15,6 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile"] s.test_files = Dir["test/**/*"] - s.add_dependency "leap_web_core", LeapWeb::VERSION - s.add_dependency "ruby-srp", "~> 0.2.1" s.add_dependency "rails_warden" end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb index f8ed71c..a2be168 100644 --- a/users/lib/leap_web_users/engine.rb +++ b/users/lib/leap_web_users/engine.rb @@ -1,6 +1,4 @@ # thou shall require all your dependencies in an engine. -require "leap_web_core" -require "leap_web_core/ui_dependencies" require "rails_warden" require "ruby-srp" -- cgit v1.2.3 From 045237ff88ffd5f1fe23d9621b043a9e604e54fa Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 09:58:41 +0200 Subject: fix requiring core extensions - most tests pass some message tests are failing for me right now. --- config/initializers/couchrest_model.rb | 2 ++ lib/extensions/testing.rb | 48 ---------------------------------- lib/leap_web.rb | 4 --- lib/leap_web_core.rb | 14 ---------- lib/leap_web_core/dependencies.rb | 40 ---------------------------- lib/leap_web_core/engine.rb | 9 ------- lib/leap_web_core/ui_dependencies.rb | 11 -------- test/support/assert_responses.rb | 46 ++++++++++++++++++++++++++++++++ test/support/rack_test.rb | 2 +- test/test_helper.rb | 3 +++ 10 files changed, 52 insertions(+), 127 deletions(-) delete mode 100644 lib/extensions/testing.rb delete mode 100644 lib/leap_web.rb delete mode 100644 lib/leap_web_core.rb delete mode 100644 lib/leap_web_core/dependencies.rb delete mode 100644 lib/leap_web_core/engine.rb delete mode 100644 lib/leap_web_core/ui_dependencies.rb create mode 100644 test/support/assert_responses.rb diff --git a/config/initializers/couchrest_model.rb b/config/initializers/couchrest_model.rb index ce4f41a..1a3e921 100644 --- a/config/initializers/couchrest_model.rb +++ b/config/initializers/couchrest_model.rb @@ -1,3 +1,5 @@ +require 'extensions/couchrest' + CouchRest::Model::Base.configure do |config| config.auto_update_design_doc = false end diff --git a/lib/extensions/testing.rb b/lib/extensions/testing.rb deleted file mode 100644 index 8f7e73c..0000000 --- a/lib/extensions/testing.rb +++ /dev/null @@ -1,48 +0,0 @@ -module LeapWebCore - module AssertResponses - - # response that works with different TestCases: - # ActionController::TestCase has @response - # ActionDispatch::IntegrationTest has @response - # Rack::Test::Methods defines last_response - def get_response - @response || last_response - end - - def assert_attachement_filename(name) - assert_equal %Q(attachment; filename="#{name}"), - get_response.headers["Content-Disposition"] - end - - def json_response - response = JSON.parse(get_response.body) - response.respond_to?(:with_indifferent_access) ? - response.with_indifferent_access : - response - end - - def assert_json_response(object) - assert_equal 'application/json', - get_response.content_type.to_s.split(';').first - if object.is_a? Hash - object.stringify_keys! if object.respond_to? :stringify_keys! - assert_equal object, json_response - else - assert_equal object.to_json, get_response.body - end - end - - def assert_json_error(object) - object.stringify_keys! if object.respond_to? :stringify_keys! - assert_json_response :errors => object - end - end -end - -class ::ActionController::TestCase - include LeapWebCore::AssertResponses -end - -class ::ActionDispatch::IntegrationTest - include LeapWebCore::AssertResponses -end diff --git a/lib/leap_web.rb b/lib/leap_web.rb deleted file mode 100644 index 9495fc6..0000000 --- a/lib/leap_web.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'leap_web_core' -require 'leap_web_certs' -require 'leap_web_users' -# do we want billing and help here? diff --git a/lib/leap_web_core.rb b/lib/leap_web_core.rb deleted file mode 100644 index a58d140..0000000 --- a/lib/leap_web_core.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "rails" - -require "couchrest" -require "couchrest_model" -require "couchrest_session_store" - -require "json" - -require "extensions/testing" -require "extensions/couchrest" -require "leap_web_core/engine" - -module LeapWebCore -end diff --git a/lib/leap_web_core/dependencies.rb b/lib/leap_web_core/dependencies.rb deleted file mode 100644 index 877e3d1..0000000 --- a/lib/leap_web_core/dependencies.rb +++ /dev/null @@ -1,40 +0,0 @@ -module LeapWebCore - class Dependencies - UI_DEV = { - "haml-rails" => "~> 0.3.4", - "sass-rails" => "~> 3.2.5", - "coffee-rails" => "~> 3.2.2", - "uglifier" => "~> 1.2.7" - } - - UI = { - "haml" => "~> 3.1.7", - "jquery-rails" => nil, - "simple_form" => nil, - "bootswatch-rails", "~> 0.5.0" - } - - def self.require_ui_gems - UI.keys.each {|dep| require dep} - if Rails.env == "development" - # This will be run in the app including plugins that run it. - # However not all development_dependencies might be present. - # So we better only require those that are. - available = Bundler.definition.specs.map(&:name) - gems_to_require = available & UI_DEV.keys - gems_to_require.each {|dep| require dep} - end - end - - def self.add_ui_gems_to_spec(spec) - UI.each do |dep, version| - spec.add_dependency dep, version - end - - UI_DEV.each do |dep, version| - spec.add_development_dependency dep, version - end - end - - end -end diff --git a/lib/leap_web_core/engine.rb b/lib/leap_web_core/engine.rb deleted file mode 100644 index 940b5e2..0000000 --- a/lib/leap_web_core/engine.rb +++ /dev/null @@ -1,9 +0,0 @@ -# thou shall require all your dependencies in an engine. -require "couchrest" -require "couchrest_model" - -module LeapWebCore - class Engine < ::Rails::Engine - - end -end diff --git a/lib/leap_web_core/ui_dependencies.rb b/lib/leap_web_core/ui_dependencies.rb deleted file mode 100644 index 2daee37..0000000 --- a/lib/leap_web_core/ui_dependencies.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "haml" -require "jquery-rails" -require "simple_form" -require "bootswatch-rails" - -if Rails.env == "development" - require "haml-rails" - require "sass-rails" - require "coffee-rails" - require "uglifier" -end diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb new file mode 100644 index 0000000..b01166f --- /dev/null +++ b/test/support/assert_responses.rb @@ -0,0 +1,46 @@ +module AssertResponses + + # response that works with different TestCases: + # ActionController::TestCase has @response + # ActionDispatch::IntegrationTest has @response + # Rack::Test::Methods defines last_response + def get_response + @response || last_response + end + + def assert_attachement_filename(name) + assert_equal %Q(attachment; filename="#{name}"), + get_response.headers["Content-Disposition"] + end + + def json_response + response = JSON.parse(get_response.body) + response.respond_to?(:with_indifferent_access) ? + response.with_indifferent_access : + response + end + + def assert_json_response(object) + assert_equal 'application/json', + get_response.content_type.to_s.split(';').first + if object.is_a? Hash + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_equal object, json_response + else + assert_equal object.to_json, get_response.body + end + end + + def assert_json_error(object) + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_json_response :errors => object + end +end + +class ::ActionController::TestCase + include AssertResponses +end + +class ::ActionDispatch::IntegrationTest + include AssertResponses +end diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb index 2d8e5c4..35d191b 100644 --- a/test/support/rack_test.rb +++ b/test/support/rack_test.rb @@ -1,7 +1,7 @@ class RackTest < ActiveSupport::TestCase include Rack::Test::Methods include Warden::Test::Helpers - include LeapWebCore::AssertResponses + include AssertResponses CONFIG_RU = (Rails.root + 'config.ru').to_s OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first diff --git a/test/test_helper.rb b/test/test_helper.rb index f63591f..b844b90 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,9 @@ require 'rails/test_help' require 'mocha/setup' +# Load support files from toplevel +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + # Load support files from all engines Dir["#{File.dirname(__FILE__)}/../*/test/support/**/*.rb"].each { |f| require f } -- cgit v1.2.3 From a5aee9ec29501cf2535cb42edb7dbca95e081b8a Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 11:10:10 +0200 Subject: move all dependencies into Gemfile We used to keep them separated so one could require some engines without using the full webapp. We've never really supported this though and probably never will. --- Gemfile | 61 ++++++++++++++++++++++++++++++++++++++++++++------ common_dependencies.rb | 22 ------------------ ui_dependencies.rb | 28 ----------------------- 3 files changed, 54 insertions(+), 57 deletions(-) delete mode 100644 common_dependencies.rb delete mode 100644 ui_dependencies.rb diff --git a/Gemfile b/Gemfile index 7a82157..28cdc8e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,5 @@ source 'https://rubygems.org' -eval(File.read(File.dirname(__FILE__) + '/common_dependencies.rb')) -eval(File.read(File.dirname(__FILE__) + '/ui_dependencies.rb')) - gem "rails", "~> 3.2.11" gem "couchrest", "~> 1.1.3" gem "couchrest_model", "~> 2.0.0" @@ -21,12 +18,62 @@ gem 'debugger', :platforms => :mri_19 # ruby 1.8 is not supported anymore # gem 'ruby-debug', :platforms => :mri_18 +gem "haml", "~> 3.1.7" +gem "bootstrap-sass", "= 2.3.2.2" +gem "jquery-rails" +gem "simple_form" +gem 'client_side_validations' +gem 'client_side_validations-simple_form' +gem "bootswatch-rails", "~> 0.5.0" + +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 'rails-i18n' # locale files for built-in validation messages and times + # https://github.com/svenfuchs/rails-i18n + # for a list of keys: + # https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml + +gem 'rdiscount' # for rendering .md templates + group :test do - gem 'fake_braintree', require: false + + # integration testing gem 'capybara', require: false - gem 'launchy' # so save_and_open_page works in integration tests - gem 'phantomjs-binaries' - gem 'minitest-stub-const' + gem 'poltergeist' # headless js + gem 'launchy' # save_and_open_page + gem 'phantomjs-binaries' # binaries specific to the os + + # moching and stubbing + gem 'mocha', '~> 0.13.0', :require => false + gem 'minitest-stub-const' # why? + + # generating test data + gem 'factory_girl_rails' # test data factories + gem 'faker' # names and numbers for test data + + # billing tests + gem 'fake_braintree', require: false +end + +group :test, :development do + gem 'thin' +end + +group :assets do + gem "haml-rails", "~> 0.3.4" + gem "sass-rails", "~> 3.2.5" + gem "coffee-rails", "~> 3.2.2" + gem "uglifier", "~> 1.2.7" + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + gem 'therubyracer', "~> 0.10.2", :platforms => :ruby + gem 'quiet_assets' # stops logging all the asset requests +end + + +group :production do + gem 'SyslogLogger', '~> 2.0' end # unreleased so far ... but leap_web_certs need it diff --git a/common_dependencies.rb b/common_dependencies.rb deleted file mode 100644 index 2225613..0000000 --- a/common_dependencies.rb +++ /dev/null @@ -1,22 +0,0 @@ -group :test do - # moching and stubing - gem 'mocha', '~> 0.13.0', :require => false - # integration testing - gem 'capybara' - # headless js integration testing - gem 'poltergeist' - # required for save_and_open_page in integration tests - # gem 'launchy' - gem 'fake_braintree' #depends on rspec? - gem 'faker' - gem 'factory_girl_rails' -end - -group :test, :development do - gem 'thin' - gem 'quiet_assets' -end - -group :production do - gem 'SyslogLogger', '~> 2.0' -end diff --git a/ui_dependencies.rb b/ui_dependencies.rb deleted file mode 100644 index 4b5d706..0000000 --- a/ui_dependencies.rb +++ /dev/null @@ -1,28 +0,0 @@ -gem "haml", "~> 3.1.7" -gem "bootstrap-sass", "= 2.3.2.2" -gem "jquery-rails" -gem "simple_form" -gem 'client_side_validations' -gem 'client_side_validations-simple_form' -gem "bootswatch-rails", "~> 0.5.0" - -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 'rails-i18n' # locale files for built-in validation messages and times - # https://github.com/svenfuchs/rails-i18n - # for a list of keys: - # https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml - -gem 'rdiscount' # for rendering .md templates - -group :assets do - gem "haml-rails", "~> 0.3.4" - gem "sass-rails", "~> 3.2.5" - gem "coffee-rails", "~> 3.2.2" - gem "uglifier", "~> 1.2.7" - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - gem 'therubyracer', "~> 0.10.2", :platforms => :ruby - -end -- cgit v1.2.3 From 38963e49be5c8e14b3b8f685b57475a700173473 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 11:23:18 +0200 Subject: update documentation: included core in toplevel now we only include some engines - we don't build the whole webapp based on them. Reflecting this in the documentation. --- CUSTOM.md | 4 ++-- DEVELOP.md | 44 ++++++++++---------------------------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/CUSTOM.md b/CUSTOM.md index 8671323..53e4d88 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -9,6 +9,6 @@ See config/customization/README.md Engines --------------------- -Leap Web is based on Engines. All things in `app` will overwrite the default behaviour. You can either create a new rails app and include the leap_web gem or clone the leap web repository and add your customizations to the `app` directory. +Leap Web includes some Engines. All things in `app` will overwrite the engine behaviour. You can clone the leap web repository and add your customizations to the `app` directory. Including leap_web as a gem is currently not supported. It should not require too much work though and we would be happy to include the changes required. -If you have no use for one of the engines you can remove it from the Gemfile. Not however that your app might still need to provide some functionality for the other engines to work. For example the users engine provides `current_user` and other methods. +If you have no use for one of the engines you can remove it from the Gemfile. Engines should really be plugins - no other engines should depend upon them. If you need functionality in different engines it should probably go into the toplevel. The 'users' engine will soon become part of the main webapp for that reason. diff --git a/DEVELOP.md b/DEVELOP.md index 6aeccff..43af04d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -5,21 +5,23 @@ Some tips on modifying the views: * Many of the forms use [simple_form gem](https://github.com/plataformatec/simple_form) +* We still use client_side_validations for the validation code but since it is not maintained anymore we want to drop it asap. ## Engines ## -Leap Web consists of different Engines. 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. +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. -Currently Leap Web consists of 5 Engines: +Currently Leap Web includes of 4 Engines: -* [core](https://github.com/leapcode/leap_web/blob/master/core) - ships some dependencies that are used accross all engines. This might be removed at some point. -* [users](https://github.com/leapcode/leap_web/blob/master/users) - user registration and authorization -* [certs](https://github.com/leapcode/leap_web/blob/master/certs) - Cert distribution for the EIP client +* [users](https://github.com/leapcode/leap_web/blob/master/users) - user registration and authorization - this will probably be included in the main webapp any time soon +* [certs](https://github.com/leapcode/leap_web/blob/master/certs) - Cert distribution for the EIP client - might be included in toplevel too. * [help](https://github.com/leapcode/leap_web/blob/master/help) - Help ticket management * [billing](https://github.com/leapcode/leap_web/blob/master/billing) - Billing System ## Creating a new engine ## +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. + ### Rails plugin new ### Create the basic tree structure for an engine using @@ -32,28 +34,10 @@ rails plugin new ENGINE_NAME -O --full See http://guides.rubyonrails.org/engines.html for more general info about engines. -### Require Leap Web Core and dependencies ### - -Leap Web Core provides a common set of dependencies for the engines with CouchRest Model etc. -It also comes with an optional set of UI gems like haml, sass, coffeescript, uglifier, bootstrap-sass, jquery and simple_form. +You need to require engine specific gems in lib/my_engine/engine.rb: -In order to use the core dependencies you need to add leap_web_core to your .gemspec: ```ruby -# make sure LeapWeb::VERSION is available -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) -# ... - Gem::Specification.new do |s| - # ... - s.add_dependency "rails" - s.add_dependency "leap_web_core", LeapWeb::Version - end -``` - -You also need to require it before you define your engine in lib/my_engine/engine.rb: -```ruby -require "leap_web_core" -# uncomment if you want the ui gems: -# require "leap_web_core/ui_dependencies" +require "my_dependency" module MyEngine class Engine < ::Rails::Engine @@ -62,17 +46,9 @@ module MyEngine end ``` -Some development and UI dependencies can not be loaded via leap_web_core. To make them available add the following lines to your engines Gemfile - -```ruby - eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) - # uncomment if you want the ui gems: - # eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb')) -``` - ## Creating Models ## -You can use the normal rails generators to create models. Since you required the leap_web_core gem you will be using CouchRest::Model. So your models inherit from CouchRest::Model::Base. +You can use the normal rails generators to create models. You probably want to require couchrest_model so your models inherit from CouchRest::Model::Base. http://www.couchrest.info/model/definition.html has some good first steps for setting up the model. CouchRest Model behaved strangely when using a model without a design block. So make sure to define an initial view: http://www.couchrest.info/model/view_objects.html . -- cgit v1.2.3 From 2e11e3ca2c7b02fdb5ff54f0bcd766cc5fa39975 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 11:41:21 +0200 Subject: moving users: dependencies --- Gemfile | 5 ++++- users/Gemfile | 15 --------------- users/Rakefile | 44 -------------------------------------------- users/leap_web_users.gemspec | 20 -------------------- 4 files changed, 4 insertions(+), 80 deletions(-) delete mode 100644 users/Gemfile delete mode 100644 users/Rakefile delete mode 100644 users/leap_web_users.gemspec diff --git a/Gemfile b/Gemfile index 28cdc8e..b19d119 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,10 @@ gem "couchrest_model", "~> 2.0.0" gem "couchrest_session_store", "~> 0.2.4" gem "json" -gem 'leap_web_users', :path => 'users' +# user management +gem "ruby-srp", "~> 0.2.1" +gem "rails_warden" + gem 'leap_web_certs', :path => 'certs' gem 'leap_web_help', :path => 'help' gem 'leap_web_billing', :path => 'billing' diff --git a/users/Gemfile b/users/Gemfile deleted file mode 100644 index 4101ead..0000000 --- a/users/Gemfile +++ /dev/null @@ -1,15 +0,0 @@ -source "https://rubygems.org" - -eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) -eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb')) - -# We require leap_web_core from here so we can use the path option. -gem "leap_web_core", :path => '../core' - -# Declare your gem's dependencies in leap_web_users.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. -gemspec - -# To use debugger -# gem 'ruby-debug' diff --git a/users/Rakefile b/users/Rakefile deleted file mode 100644 index 38da5fc..0000000 --- a/users/Rakefile +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env rake - -require 'rake/packagetask' -require 'rubygems/package_task' - -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end -begin - require 'rdoc/task' -rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' - RDoc::Task = Rake::RDocTask -end - -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebUsers' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = eval(File.read('leap_web_users.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - - -task :default => :test diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec deleted file mode 100644 index 630c095..0000000 --- a/users/leap_web_users.gemspec +++ /dev/null @@ -1,20 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "leap_web_users" - s.version = LeapWeb::VERSION - s.authors = ["Azul"] - s.email = ["azul@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "User registration and authorization for the leap platform" - s.description = "This this plugin for the leap platform provides user signup and login. It uses Secure Remote Password for the authentication." - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile"] - s.test_files = Dir["test/**/*"] - - s.add_dependency "ruby-srp", "~> 0.2.1" - s.add_dependency "rails_warden" -end -- cgit v1.2.3 From b6d14dc19dd350a807826e3e097738a36613e083 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 11:49:14 +0200 Subject: moving users: app and test files --- app/assets/images/leap_web_users/.gitkeep | 0 app/assets/javascripts/leap_web_users/.gitkeep | 0 app/assets/javascripts/users.js | 132 +++++++++++++++ app/assets/stylesheets/leap_web_users/.gitkeep | 0 app/controllers/.gitkeep | 0 app/controllers/account_settings_controller.rb | 0 .../controller_extension/authentication.rb | 75 +++++++++ .../controller_extension/token_authentication.rb | 27 ++++ app/controllers/keys_controller.rb | 18 +++ app/controllers/sessions_controller.rb | 28 ++++ app/controllers/users_base_controller.rb | 18 +++ app/controllers/users_controller.rb | 69 ++++++++ app/controllers/v1/messages_controller.rb | 25 +++ app/controllers/v1/sessions_controller.rb | 45 ++++++ app/controllers/v1/users_controller.rb | 32 ++++ app/controllers/webfinger_controller.rb | 19 +++ app/designs/message/by_user_ids_to_show.js | 7 + .../message/by_user_ids_to_show_and_created_at.js | 9 ++ ...by_created_at_and_one_month_warning_not_sent.js | 5 + app/helpers/.gitkeep | 0 app/helpers/email_aliases_helper.rb | 11 ++ app/helpers/sessions_helper.rb | 2 + app/helpers/users_helper.rb | 14 ++ app/models/account.rb | 68 ++++++++ app/models/email.rb | 26 +++ app/models/identity.rb | 136 ++++++++++++++++ app/models/local_email.rb | 68 ++++++++ app/models/login_format_validation.rb | 21 +++ app/models/message.rb | 29 ++++ app/models/pgp_key.rb | 48 ++++++ app/models/service_level.rb | 19 +++ app/models/session.rb | 32 ++++ app/models/token.rb | 69 ++++++++ app/models/unauthenticated_user.rb | 6 + app/models/user.rb | 179 +++++++++++++++++++++ app/views/.gitkeep | 0 app/views/emails/_email.html.haml | 6 + app/views/sessions/new.html.haml | 10 ++ app/views/sessions/new.json.erb | 3 + app/views/users/_change_password.html.haml | 21 +++ app/views/users/_change_pgp_key.html.haml | 13 ++ app/views/users/_change_service_level.html.haml | 18 +++ app/views/users/_destroy_account.html.haml | 27 ++++ app/views/users/_edit.html.haml | 14 ++ app/views/users/_user.html.haml | 4 + app/views/users/_warnings.html.haml | 12 ++ app/views/users/edit.html.haml | 1 + app/views/users/index.html.haml | 13 ++ app/views/users/new.html.haml | 19 +++ app/views/users/show.html.haml | 25 +++ app/views/v1/sessions/new.json.erb | 3 + app/views/webfinger/host_meta.xml.erb | 11 ++ app/views/webfinger/search.xml.erb | 7 + test/factories.rb | 34 ++++ test/functional/application_controller_test.rb | 28 ++++ test/functional/helper_methods_test.rb | 39 +++++ test/functional/keys_controller_test.rb | 32 ++++ test/functional/sessions_controller_test.rb | 59 +++++++ test/functional/test_helpers_test.rb | 38 +++++ test/functional/users_controller_test.rb | 165 +++++++++++++++++++ test/functional/v1/messages_controller_test.rb | 57 +++++++ test/functional/v1/sessions_controller_test.rb | 62 +++++++ test/functional/v1/users_controller_test.rb | 74 +++++++++ test/functional/webfinger_controller_test.rb | 33 ++++ test/integration/api/Readme.md | 23 +++ test/integration/api/login_test.rb | 50 ++++++ test/integration/api/pgp_key_test.rb | 35 ++++ test/integration/api/python/flow_with_srp.py | 96 +++++++++++ .../integration/api/python/login_wrong_username.py | 19 +++ test/integration/api/python/signup.py | 20 +++ test/integration/api/python/signup_and_login.py | 44 +++++ .../api/python/signup_and_login_wrong_password.py | 43 +++++ test/integration/api/python/umlauts.py | 79 +++++++++ test/integration/api/signup_test.rb | 20 +++ test/integration/api/srp_test.rb | 104 ++++++++++++ test/integration/api/update_account_test.rb | 51 ++++++ test/integration/browser/account_test.rb | 147 +++++++++++++++++ test/integration/browser/session_test.rb | 27 ++++ test/integration/navigation_test.rb | 9 ++ test/leap_web_users_test.rb | 7 + test/support/auth_test_helper.rb | 65 ++++++++ test/support/stub_record_helper.rb | 53 ++++++ test/support/time_test_helper.rb | 30 ++++ test/unit/account_test.rb | 47 ++++++ test/unit/helpers/session_helper_test.rb | 4 + test/unit/helpers/users_helper_test.rb | 4 + test/unit/identity_test.rb | 133 +++++++++++++++ test/unit/local_email_test.rb | 65 ++++++++ test/unit/token_test.rb | 89 ++++++++++ test/unit/unauthenticated_user_test.rb | 7 + test/unit/user_test.rb | 68 ++++++++ .../warden_strategy_secure_remote_password_test.rb | 63 ++++++++ test/unit/webfinger/host_meta_presenter_test.rb | 24 +++ test/unit/webfinger/user_presenter_test.rb | 49 ++++++ users/app/assets/images/leap_web_users/.gitkeep | 0 .../app/assets/javascripts/leap_web_users/.gitkeep | 0 users/app/assets/javascripts/srp | 1 - users/app/assets/javascripts/users.js | 132 --------------- .../app/assets/stylesheets/leap_web_users/.gitkeep | 0 users/app/controllers/.gitkeep | 0 .../app/controllers/account_settings_controller.rb | 0 .../controller_extension/authentication.rb | 75 --------- .../controller_extension/token_authentication.rb | 27 ---- users/app/controllers/keys_controller.rb | 18 --- users/app/controllers/sessions_controller.rb | 28 ---- users/app/controllers/users_base_controller.rb | 18 --- users/app/controllers/users_controller.rb | 69 -------- users/app/controllers/v1/messages_controller.rb | 25 --- users/app/controllers/v1/sessions_controller.rb | 45 ------ users/app/controllers/v1/users_controller.rb | 32 ---- users/app/controllers/webfinger_controller.rb | 19 --- users/app/designs/message/by_user_ids_to_show.js | 7 - .../message/by_user_ids_to_show_and_created_at.js | 9 -- ...by_created_at_and_one_month_warning_not_sent.js | 5 - users/app/helpers/.gitkeep | 0 users/app/helpers/email_aliases_helper.rb | 11 -- users/app/helpers/sessions_helper.rb | 2 - users/app/helpers/users_helper.rb | 14 -- users/app/mailers/.gitkeep | 0 users/app/models/.gitkeep | 0 users/app/models/account.rb | 68 -------- users/app/models/email.rb | 26 --- users/app/models/identity.rb | 136 ---------------- users/app/models/local_email.rb | 68 -------- users/app/models/login_format_validation.rb | 21 --- users/app/models/message.rb | 29 ---- users/app/models/pgp_key.rb | 48 ------ users/app/models/service_level.rb | 19 --- users/app/models/session.rb | 32 ---- users/app/models/token.rb | 69 -------- users/app/models/unauthenticated_user.rb | 6 - users/app/models/user.rb | 179 --------------------- users/app/views/.gitkeep | 0 users/app/views/emails/_email.html.haml | 6 - users/app/views/sessions/new.html.haml | 10 -- users/app/views/sessions/new.json.erb | 3 - users/app/views/users/_change_password.html.haml | 21 --- users/app/views/users/_change_pgp_key.html.haml | 13 -- .../views/users/_change_service_level.html.haml | 18 --- users/app/views/users/_destroy_account.html.haml | 27 ---- users/app/views/users/_edit.html.haml | 14 -- users/app/views/users/_user.html.haml | 4 - users/app/views/users/_warnings.html.haml | 12 -- users/app/views/users/edit.html.haml | 1 - users/app/views/users/index.html.haml | 13 -- users/app/views/users/new.html.haml | 19 --- users/app/views/users/show.html.haml | 25 --- users/app/views/v1/sessions/new.json.erb | 3 - users/app/views/webfinger/host_meta.xml.erb | 11 -- users/app/views/webfinger/search.xml.erb | 7 - users/test/factories.rb | 34 ---- users/test/fixtures/.gitkeep | 0 users/test/functional/.gitkeep | 0 .../test/functional/application_controller_test.rb | 28 ---- users/test/functional/helper_methods_test.rb | 39 ----- users/test/functional/keys_controller_test.rb | 32 ---- users/test/functional/sessions_controller_test.rb | 59 ------- users/test/functional/test_helpers_test.rb | 38 ----- users/test/functional/users_controller_test.rb | 165 ------------------- .../test/functional/v1/messages_controller_test.rb | 57 ------- .../test/functional/v1/sessions_controller_test.rb | 62 ------- users/test/functional/v1/users_controller_test.rb | 74 --------- users/test/functional/webfinger_controller_test.rb | 33 ---- users/test/integration/.gitkeep | 0 users/test/integration/api/Readme.md | 23 --- users/test/integration/api/login_test.rb | 50 ------ users/test/integration/api/pgp_key_test.rb | 35 ---- users/test/integration/api/python/flow_with_srp.py | 96 ----------- .../integration/api/python/login_wrong_username.py | 19 --- users/test/integration/api/python/signup.py | 20 --- .../integration/api/python/signup_and_login.py | 44 ----- .../api/python/signup_and_login_wrong_password.py | 43 ----- users/test/integration/api/python/umlauts.py | 79 --------- users/test/integration/api/signup_test.rb | 20 --- users/test/integration/api/srp_test.rb | 104 ------------ users/test/integration/api/update_account_test.rb | 51 ------ users/test/integration/browser/account_test.rb | 147 ----------------- users/test/integration/browser/session_test.rb | 27 ---- users/test/integration/navigation_test.rb | 9 -- users/test/leap_web_users_test.rb | 7 - users/test/support/auth_test_helper.rb | 65 -------- users/test/support/stub_record_helper.rb | 53 ------ users/test/support/time_test_helper.rb | 30 ---- users/test/test_helper.rb | 9 -- users/test/unit/.gitkeep | 0 users/test/unit/account_test.rb | 47 ------ users/test/unit/email_test.rb | 19 --- users/test/unit/helpers/session_helper_test.rb | 4 - users/test/unit/helpers/users_helper_test.rb | 4 - users/test/unit/identity_test.rb | 133 --------------- users/test/unit/local_email_test.rb | 65 -------- users/test/unit/token_test.rb | 89 ---------- users/test/unit/unauthenticated_user_test.rb | 7 - users/test/unit/user_test.rb | 68 -------- .../warden_strategy_secure_remote_password_test.rb | 63 -------- .../unit/webfinger/host_meta_presenter_test.rb | 24 --- users/test/unit/webfinger/user_presenter_test.rb | 49 ------ 197 files changed, 3540 insertions(+), 3569 deletions(-) create mode 100644 app/assets/images/leap_web_users/.gitkeep create mode 100644 app/assets/javascripts/leap_web_users/.gitkeep create mode 100644 app/assets/javascripts/users.js create mode 100644 app/assets/stylesheets/leap_web_users/.gitkeep create mode 100644 app/controllers/.gitkeep create mode 100644 app/controllers/account_settings_controller.rb create mode 100644 app/controllers/controller_extension/authentication.rb create mode 100644 app/controllers/controller_extension/token_authentication.rb create mode 100644 app/controllers/keys_controller.rb create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/controllers/users_base_controller.rb create mode 100644 app/controllers/users_controller.rb create mode 100644 app/controllers/v1/messages_controller.rb create mode 100644 app/controllers/v1/sessions_controller.rb create mode 100644 app/controllers/v1/users_controller.rb create mode 100644 app/controllers/webfinger_controller.rb create mode 100644 app/designs/message/by_user_ids_to_show.js create mode 100644 app/designs/message/by_user_ids_to_show_and_created_at.js create mode 100644 app/designs/user/by_created_at_and_one_month_warning_not_sent.js create mode 100644 app/helpers/.gitkeep create mode 100644 app/helpers/email_aliases_helper.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/helpers/users_helper.rb create mode 100644 app/models/account.rb create mode 100644 app/models/email.rb create mode 100644 app/models/identity.rb create mode 100644 app/models/local_email.rb create mode 100644 app/models/login_format_validation.rb create mode 100644 app/models/message.rb create mode 100644 app/models/pgp_key.rb create mode 100644 app/models/service_level.rb create mode 100644 app/models/session.rb create mode 100644 app/models/token.rb create mode 100644 app/models/unauthenticated_user.rb create mode 100644 app/models/user.rb create mode 100644 app/views/.gitkeep create mode 100644 app/views/emails/_email.html.haml create mode 100644 app/views/sessions/new.html.haml create mode 100644 app/views/sessions/new.json.erb create mode 100644 app/views/users/_change_password.html.haml create mode 100644 app/views/users/_change_pgp_key.html.haml create mode 100644 app/views/users/_change_service_level.html.haml create mode 100644 app/views/users/_destroy_account.html.haml create mode 100644 app/views/users/_edit.html.haml create mode 100644 app/views/users/_user.html.haml create mode 100644 app/views/users/_warnings.html.haml create mode 100644 app/views/users/edit.html.haml create mode 100644 app/views/users/index.html.haml create mode 100644 app/views/users/new.html.haml create mode 100644 app/views/users/show.html.haml create mode 100644 app/views/v1/sessions/new.json.erb create mode 100644 app/views/webfinger/host_meta.xml.erb create mode 100644 app/views/webfinger/search.xml.erb create mode 100644 test/functional/application_controller_test.rb create mode 100644 test/functional/helper_methods_test.rb create mode 100644 test/functional/keys_controller_test.rb create mode 100644 test/functional/sessions_controller_test.rb create mode 100644 test/functional/test_helpers_test.rb create mode 100644 test/functional/users_controller_test.rb create mode 100644 test/functional/v1/messages_controller_test.rb create mode 100644 test/functional/v1/sessions_controller_test.rb create mode 100644 test/functional/v1/users_controller_test.rb create mode 100644 test/functional/webfinger_controller_test.rb create mode 100644 test/integration/api/Readme.md create mode 100644 test/integration/api/login_test.rb create mode 100644 test/integration/api/pgp_key_test.rb create mode 100755 test/integration/api/python/flow_with_srp.py create mode 100755 test/integration/api/python/login_wrong_username.py create mode 100755 test/integration/api/python/signup.py create mode 100755 test/integration/api/python/signup_and_login.py create mode 100755 test/integration/api/python/signup_and_login_wrong_password.py create mode 100755 test/integration/api/python/umlauts.py create mode 100644 test/integration/api/signup_test.rb create mode 100644 test/integration/api/srp_test.rb create mode 100644 test/integration/api/update_account_test.rb create mode 100644 test/integration/browser/account_test.rb create mode 100644 test/integration/browser/session_test.rb create mode 100644 test/integration/navigation_test.rb create mode 100644 test/leap_web_users_test.rb create mode 100644 test/support/auth_test_helper.rb create mode 100644 test/support/stub_record_helper.rb create mode 100644 test/support/time_test_helper.rb create mode 100644 test/unit/account_test.rb create mode 100644 test/unit/helpers/session_helper_test.rb create mode 100644 test/unit/helpers/users_helper_test.rb create mode 100644 test/unit/identity_test.rb create mode 100644 test/unit/local_email_test.rb create mode 100644 test/unit/token_test.rb create mode 100644 test/unit/unauthenticated_user_test.rb create mode 100644 test/unit/user_test.rb create mode 100644 test/unit/warden_strategy_secure_remote_password_test.rb create mode 100644 test/unit/webfinger/host_meta_presenter_test.rb create mode 100644 test/unit/webfinger/user_presenter_test.rb delete mode 100644 users/app/assets/images/leap_web_users/.gitkeep delete mode 100644 users/app/assets/javascripts/leap_web_users/.gitkeep delete mode 160000 users/app/assets/javascripts/srp delete mode 100644 users/app/assets/javascripts/users.js delete mode 100644 users/app/assets/stylesheets/leap_web_users/.gitkeep delete mode 100644 users/app/controllers/.gitkeep delete mode 100644 users/app/controllers/account_settings_controller.rb delete mode 100644 users/app/controllers/controller_extension/authentication.rb delete mode 100644 users/app/controllers/controller_extension/token_authentication.rb delete mode 100644 users/app/controllers/keys_controller.rb delete mode 100644 users/app/controllers/sessions_controller.rb delete mode 100644 users/app/controllers/users_base_controller.rb delete mode 100644 users/app/controllers/users_controller.rb delete mode 100644 users/app/controllers/v1/messages_controller.rb delete mode 100644 users/app/controllers/v1/sessions_controller.rb delete mode 100644 users/app/controllers/v1/users_controller.rb delete mode 100644 users/app/controllers/webfinger_controller.rb delete mode 100644 users/app/designs/message/by_user_ids_to_show.js delete mode 100644 users/app/designs/message/by_user_ids_to_show_and_created_at.js delete mode 100644 users/app/designs/user/by_created_at_and_one_month_warning_not_sent.js delete mode 100644 users/app/helpers/.gitkeep delete mode 100644 users/app/helpers/email_aliases_helper.rb delete mode 100644 users/app/helpers/sessions_helper.rb delete mode 100644 users/app/helpers/users_helper.rb delete mode 100644 users/app/mailers/.gitkeep delete mode 100644 users/app/models/.gitkeep delete mode 100644 users/app/models/account.rb delete mode 100644 users/app/models/email.rb delete mode 100644 users/app/models/identity.rb delete mode 100644 users/app/models/local_email.rb delete mode 100644 users/app/models/login_format_validation.rb delete mode 100644 users/app/models/message.rb delete mode 100644 users/app/models/pgp_key.rb delete mode 100644 users/app/models/service_level.rb delete mode 100644 users/app/models/session.rb delete mode 100644 users/app/models/token.rb delete mode 100644 users/app/models/unauthenticated_user.rb delete mode 100644 users/app/models/user.rb delete mode 100644 users/app/views/.gitkeep delete mode 100644 users/app/views/emails/_email.html.haml delete mode 100644 users/app/views/sessions/new.html.haml delete mode 100644 users/app/views/sessions/new.json.erb delete mode 100644 users/app/views/users/_change_password.html.haml delete mode 100644 users/app/views/users/_change_pgp_key.html.haml delete mode 100644 users/app/views/users/_change_service_level.html.haml delete mode 100644 users/app/views/users/_destroy_account.html.haml delete mode 100644 users/app/views/users/_edit.html.haml delete mode 100644 users/app/views/users/_user.html.haml delete mode 100644 users/app/views/users/_warnings.html.haml delete mode 100644 users/app/views/users/edit.html.haml delete mode 100644 users/app/views/users/index.html.haml delete mode 100644 users/app/views/users/new.html.haml delete mode 100644 users/app/views/users/show.html.haml delete mode 100644 users/app/views/v1/sessions/new.json.erb delete mode 100644 users/app/views/webfinger/host_meta.xml.erb delete mode 100644 users/app/views/webfinger/search.xml.erb delete mode 100644 users/test/factories.rb delete mode 100644 users/test/fixtures/.gitkeep delete mode 100644 users/test/functional/.gitkeep delete mode 100644 users/test/functional/application_controller_test.rb delete mode 100644 users/test/functional/helper_methods_test.rb delete mode 100644 users/test/functional/keys_controller_test.rb delete mode 100644 users/test/functional/sessions_controller_test.rb delete mode 100644 users/test/functional/test_helpers_test.rb delete mode 100644 users/test/functional/users_controller_test.rb delete mode 100644 users/test/functional/v1/messages_controller_test.rb delete mode 100644 users/test/functional/v1/sessions_controller_test.rb delete mode 100644 users/test/functional/v1/users_controller_test.rb delete mode 100644 users/test/functional/webfinger_controller_test.rb delete mode 100644 users/test/integration/.gitkeep delete mode 100644 users/test/integration/api/Readme.md delete mode 100644 users/test/integration/api/login_test.rb delete mode 100644 users/test/integration/api/pgp_key_test.rb delete mode 100755 users/test/integration/api/python/flow_with_srp.py delete mode 100755 users/test/integration/api/python/login_wrong_username.py delete mode 100755 users/test/integration/api/python/signup.py delete mode 100755 users/test/integration/api/python/signup_and_login.py delete mode 100755 users/test/integration/api/python/signup_and_login_wrong_password.py delete mode 100755 users/test/integration/api/python/umlauts.py delete mode 100644 users/test/integration/api/signup_test.rb delete mode 100644 users/test/integration/api/srp_test.rb delete mode 100644 users/test/integration/api/update_account_test.rb delete mode 100644 users/test/integration/browser/account_test.rb delete mode 100644 users/test/integration/browser/session_test.rb delete mode 100644 users/test/integration/navigation_test.rb delete mode 100644 users/test/leap_web_users_test.rb delete mode 100644 users/test/support/auth_test_helper.rb delete mode 100644 users/test/support/stub_record_helper.rb delete mode 100644 users/test/support/time_test_helper.rb delete mode 100644 users/test/test_helper.rb delete mode 100644 users/test/unit/.gitkeep delete mode 100644 users/test/unit/account_test.rb delete mode 100644 users/test/unit/email_test.rb delete mode 100644 users/test/unit/helpers/session_helper_test.rb delete mode 100644 users/test/unit/helpers/users_helper_test.rb delete mode 100644 users/test/unit/identity_test.rb delete mode 100644 users/test/unit/local_email_test.rb delete mode 100644 users/test/unit/token_test.rb delete mode 100644 users/test/unit/unauthenticated_user_test.rb delete mode 100644 users/test/unit/user_test.rb delete mode 100644 users/test/unit/warden_strategy_secure_remote_password_test.rb delete mode 100644 users/test/unit/webfinger/host_meta_presenter_test.rb delete mode 100644 users/test/unit/webfinger/user_presenter_test.rb diff --git a/app/assets/images/leap_web_users/.gitkeep b/app/assets/images/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/leap_web_users/.gitkeep b/app/assets/javascripts/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js new file mode 100644 index 0000000..8486756 --- /dev/null +++ b/app/assets/javascripts/users.js @@ -0,0 +1,132 @@ +(function() { + // + // LOCAL FUNCTIONS + // + + var poll_users, + prevent_default, + form_failed, + form_passed, + clear_errors, + update_user; + + prevent_default = function(event) { + return event.preventDefault(); + }; + + poll_users = function(query, process) { + return $.get("/1/users.json", { + query: query + }).done(process); + }; + + clear_errors = function() { + return $('#messages').empty(); + }; + + update_user = function(submitEvent) { + var form = submitEvent.target; + var token = form.dataset.token; + var url = form.action; + var req = $.ajax({ + url: url, + type: 'PUT', + headers: { Authorization: 'Token token="' + token + '"' }, + data: $(form).serialize() + }); + req.done( function() { + $(form).find('input[type="submit"]').button('reset'); + }); + }; + + markAsSubmitted = function(submitEvent) { + var form = submitEvent.target; + $(form).addClass('submitted') + // bootstrap loading state: + $(form).find('input[type="submit"]').button('loading'); + }; + + resetButtons = function(submitEvent) { + var form = $('form.submitted') + // bootstrap loading state: + $(form).find('input[type="submit"]').button('reset'); + $(form).removeClass('submitted') + }; + + // + // PUBLIC FUNCTIONS + // + + srp.session = new srp.Session(); + + srp.signedUp = function() { + return srp.login(); + }; + + srp.loggedIn = function() { + return window.location = '/'; + }; + + srp.updated = function() { + return window.location = '/users/' + srp.session.id(); + }; + + // + // if a json request returns an error, this function gets called and + // decorates the appropriate fields with the error messages. + // + srp.error = function(message) { + clear_errors(); + var errors = extractErrors(message); + displayErrors(errors); + resetButtons(); + } + + function extractErrors(message) { + if ($.isPlainObject(message) && message.errors) { + return message.errors; + } else { + return { + base: (message.error || JSON.stringify(message)) + }; + } + } + + function displayErrors(errors) { + for (var field in errors) { + var error = errors[field]; + if (field === 'base') { + alert_message(error); + } else { + displayFieldError(field, error); + } + } + } + + function displayFieldError(field, error) { + var element = $('form input[name$="[' + field + ']"]'); + if (element) { + element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false); + } + }; + + // + // INIT + // + + $(document).ready(function() { + $('form').submit(markAsSubmitted); + $('#new_user').submit(prevent_default); + $('#new_user').submit(srp.signup); + $('#new_session').submit(prevent_default); + $('#new_session').submit(srp.login); + $('#update_login_and_password').submit(prevent_default); + $('#update_login_and_password').submit(srp.update); + $('#update_pgp_key').submit(prevent_default); + $('#update_pgp_key').submit(update_user); + return $('#user-typeahead').typeahead({ + source: poll_users + }); + }); + +}).call(this); diff --git a/app/assets/stylesheets/leap_web_users/.gitkeep b/app/assets/stylesheets/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/.gitkeep b/app/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/account_settings_controller.rb b/app/controllers/account_settings_controller.rb new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb new file mode 100644 index 0000000..03d3989 --- /dev/null +++ b/app/controllers/controller_extension/authentication.rb @@ -0,0 +1,75 @@ +module ControllerExtension::Authentication + extend ActiveSupport::Concern + + private + + included do + helper_method :current_user, :logged_in?, :admin? + end + + def current_user + @current_user ||= token_authenticate || warden.user + end + + def logged_in? + !!current_user + end + + def require_login + access_denied unless logged_in? + end + + # some actions only make sense if you are not logged in yet. + # (login, signup). If a user tries to perform these they will + # be redirected to their dashboard. + def redirect_if_logged_in + redirect_to home_url if logged_in? + end + + def access_denied + respond_to do |format| + format.html do + if logged_in? + redirect_to home_url, :alert => t(:not_authorized) + else + redirect_to login_url, :alert => t(:not_authorized_login) + end + end + format.json do + render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity + end + end + end + + def admin? + current_user && current_user.is_admin? + end + + def require_admin + access_denied unless admin? + end + + def authentication_errors + return unless attempted_login? + errors = get_warden_errors + errors.inject({}) do |translated,err| + translated[err.first] = I18n.t(err.last) + translated + end + end + + def get_warden_errors + if strategy = warden.winning_strategy + message = strategy.message + # in case we get back the default message to fail! + message.respond_to?(:inject) ? message : { base: message } + else + { login: :all_strategies_failed } + end + end + + def attempted_login? + request.env['warden.options'] && + request.env['warden.options'][:attempted_path] + end +end diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb new file mode 100644 index 0000000..6e0a6ce --- /dev/null +++ b/app/controllers/controller_extension/token_authentication.rb @@ -0,0 +1,27 @@ +module ControllerExtension::TokenAuthentication + extend ActiveSupport::Concern + + def token + @token ||= authenticate_with_http_token do |token_id, options| + Token.find(token_id) + end + end + + def token_authenticate + @token_authenticated ||= token.authenticate if token + end + + def require_token + access_denied unless token_authenticate + end + + def logout + super + clear_token + end + + def clear_token + token.destroy if token + end +end + diff --git a/app/controllers/keys_controller.rb b/app/controllers/keys_controller.rb new file mode 100644 index 0000000..fb28901 --- /dev/null +++ b/app/controllers/keys_controller.rb @@ -0,0 +1,18 @@ +class KeysController < ApplicationController + + # + # Render the user's key as plain text, without a layout. + # + # We will show blank page if user doesn't have key (which shouldn't generally occur) + # and a 404 error if user doesn't exist + # + def show + user = User.find_by_login(params[:login]) + if user + render text: user.public_key, content_type: 'text/text' + else + raise ActionController::RoutingError.new('Not Found') + end + end + +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..8919a4d --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,28 @@ +class SessionsController < ApplicationController + + before_filter :redirect_if_logged_in, :only => [:new] + + def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end + end + + def destroy + logout + redirect_to home_url + end + + # + # this is a bad hack, but user_url(user) is not available + # also, this doesn't work because the redirect happens as a PUT. no idea why. + # + #Warden::Manager.after_authentication do |user, auth, opts| + # response = Rack::Response.new + # response.redirect "/users/#{user.id}" + # throw :warden, response.finish + #end + +end diff --git a/app/controllers/users_base_controller.rb b/app/controllers/users_base_controller.rb new file mode 100644 index 0000000..9becf0d --- /dev/null +++ b/app/controllers/users_base_controller.rb @@ -0,0 +1,18 @@ +# +# common base class for all user related controllers +# + +class UsersBaseController < ApplicationController + + protected + + def fetch_user + @user = User.find(params[:user_id] || params[:id]) + if !@user && admin? + redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') + elsif !admin? && @user != current_user + access_denied + end + end + +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..c8e09b6 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,69 @@ +# +# This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb +# + +class UsersController < UsersBaseController + + before_filter :require_login, :except => [:new] + before_filter :redirect_if_logged_in, :only => [:new] + before_filter :require_admin, :only => [:index, :deactivate, :enable] + before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable] + + respond_to :html + + def index + if params[:query] + if @user = User.find_by_login(params[:query]) + redirect_to @user + return + else + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + end + else + @users = User.by_created_at.descending + end + @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]) + respond_with @user + end + + def deactivate + @user.enabled = false + @user.save + respond_with @user + end + + def enable + @user.enabled = true + @user.save + respond_with @user + end + + def destroy + @user.account.destroy + flash[:notice] = I18n.t(:account_destroyed) + # admins can destroy other users + if @user != current_user + redirect_to users_url + else + # let's remove the invalid session + logout + redirect_to bye_url + end + end + +end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb new file mode 100644 index 0000000..f71d0f1 --- /dev/null +++ b/app/controllers/v1/messages_controller.rb @@ -0,0 +1,25 @@ +module V1 + class MessagesController < ApplicationController + + skip_before_filter :verify_authenticity_token + before_filter :require_token + + respond_to :json + + def index + render json: (current_user ? current_user.messages : [] ) + end + + def update + message = Message.find(params[:id]) + if (message and current_user) + message.mark_as_read_by(current_user) + message.save + render json: true + else + render json: false + end + end + + end +end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb new file mode 100644 index 0000000..eae3a1e --- /dev/null +++ b/app/controllers/v1/sessions_controller.rb @@ -0,0 +1,45 @@ +module V1 + class SessionsController < ApplicationController + + skip_before_filter :verify_authenticity_token + before_filter :require_token, only: :destroy + + def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end + end + + def create + logout if logged_in? + if params['A'] + authenticate! + else + @user = User.find_by_login(params['login']) + render :json => {salt: @user.salt} + end + end + + def update + authenticate! + @token = Token.create(:user_id => current_user.id) + session[:token] = @token.id + render :json => login_response + end + + def destroy + logout + head :no_content + end + + protected + + def login_response + handshake = session.delete(:handshake) || {} + handshake.to_hash.merge(:id => current_user.id, :token => @token.id) + end + + end +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb new file mode 100644 index 0000000..8897d01 --- /dev/null +++ b/app/controllers/v1/users_controller.rb @@ -0,0 +1,32 @@ +module V1 + class UsersController < UsersBaseController + + skip_before_filter :verify_authenticity_token + before_filter :fetch_user, :only => [:update] + before_filter :require_admin, :only => [:index] + before_filter :require_token, :only => [:update] + + respond_to :json + + # used for autocomplete for admins in the web ui + def index + if params[:query] + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + respond_with @users.map(&:login).sort + else + render :json => {'error' => 'query required', 'status' => :unprocessable_entity} + end + end + + def create + @user = Account.create(params[:user]) + respond_with @user # return ID instead? + end + + def update + @user.account.update params[:user] + respond_with @user + end + + end +end diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb new file mode 100644 index 0000000..8872802 --- /dev/null +++ b/app/controllers/webfinger_controller.rb @@ -0,0 +1,19 @@ +class WebfingerController < ApplicationController + + respond_to :xml, :json + layout false + + def host_meta + @host_meta = Webfinger::HostMetaPresenter.new(request) + respond_with @host_meta + end + + def search + username = params[:q].split('@')[0].to_s.downcase + user = User.find_by_login(username) + raise RECORD_NOT_FOUND, 'User not found' unless user.present? + @presenter = Webfinger::UserPresenter.new(user, request) + respond_with @presenter + end + +end diff --git a/app/designs/message/by_user_ids_to_show.js b/app/designs/message/by_user_ids_to_show.js new file mode 100644 index 0000000..e33566b --- /dev/null +++ b/app/designs/message/by_user_ids_to_show.js @@ -0,0 +1,7 @@ +function (doc) { + if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { + doc.user_ids_to_show.forEach(function (userId) { + emit(userId, 1); + }); + } +} diff --git a/app/designs/message/by_user_ids_to_show_and_created_at.js b/app/designs/message/by_user_ids_to_show_and_created_at.js new file mode 100644 index 0000000..54e4604 --- /dev/null +++ b/app/designs/message/by_user_ids_to_show_and_created_at.js @@ -0,0 +1,9 @@ +// not using at moment +// call with something like Message.by_user_ids_to_show_and_created_at.startkey([user_id, start_date]).endkey([user_id,end_date]) +function (doc) { + if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { + doc.user_ids_to_show.forEach(function (userId) { + emit([userId, doc.created_at], 1); + }); + } +} diff --git a/app/designs/user/by_created_at_and_one_month_warning_not_sent.js b/app/designs/user/by_created_at_and_one_month_warning_not_sent.js new file mode 100644 index 0000000..53a95de --- /dev/null +++ b/app/designs/user/by_created_at_and_one_month_warning_not_sent.js @@ -0,0 +1,5 @@ +function (doc) { + if ((doc['type'] == 'User') && (doc['created_at'] != null) && (doc['one_month_warning_sent'] == null)) { + emit(doc['created_at'], 1); + } +} diff --git a/app/helpers/.gitkeep b/app/helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/helpers/email_aliases_helper.rb b/app/helpers/email_aliases_helper.rb new file mode 100644 index 0000000..b56b068 --- /dev/null +++ b/app/helpers/email_aliases_helper.rb @@ -0,0 +1,11 @@ +module EmailAliasesHelper + + def email_alias_form(options = {}) + simple_form_for [@user, EmailAlias.new()], + :html => {:class => "form-horizontal email-alias form"}, + :validate => true do |f| + yield f + end + end + +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..309f8b2 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..f56faab --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,14 @@ +module UsersHelper + + def user_form_class(*classes) + (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') + end + + def wrapped(item, options = {}) + options[:as] ||= :div + content_tag options[:as], :class => dom_class(item), :id => dom_id(item) do + yield + end + end + +end diff --git a/app/models/account.rb b/app/models/account.rb new file mode 100644 index 0000000..cf998e4 --- /dev/null +++ b/app/models/account.rb @@ -0,0 +1,68 @@ +# +# The Account model takes care of the livecycle of a user. +# It composes a User record and it's identity records. +# It also allows for other engines to hook into the livecycle by +# monkeypatching the create, update and destroy methods. +# There's an ActiveSupport load_hook at the end of this file to +# make this more easy. +# +class Account + + attr_reader :user + + def initialize(user = nil) + @user = user + end + + # Returns the user record so it can be used in views. + def self.create(attrs) + @user = User.create(attrs).tap do |user| + Identity.create_for user + end + end + + def update(attrs) + if attrs[:password_verifier].present? + update_login(attrs[:login]) + @user.update_attributes attrs.slice(:password_verifier, :password_salt) + end + # TODO: move into identity controller + key = update_pgp_key(attrs[:public_key]) + @user.errors.set :public_key, key.errors.full_messages + @user.save && save_identities + @user.refresh_identity + end + + def destroy + return unless @user + Identity.disable_all_for(@user) + @user.destroy + end + + protected + + def update_login(login) + return unless login.present? + @old_identity = Identity.for(@user) + @user.login = login + @new_identity = Identity.for(@user) # based on the new login + @old_identity.destination = @user.email_address # alias old -> new + end + + def update_pgp_key(key) + PgpKey.new(key).tap do |key| + if key.present? && key.valid? + @new_identity ||= Identity.for(@user) + @new_identity.set_key(:pgp, key) + end + end + end + + def save_identities + @new_identity.try(:save) && @old_identity.try(:save) + end + + # You can hook into the account lifecycle from different engines using + # ActiveSupport.on_load(:account) do ... + ActiveSupport.run_load_hooks(:account, self) +end diff --git a/app/models/email.rb b/app/models/email.rb new file mode 100644 index 0000000..a9a503f --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,26 @@ +class Email < String + include ActiveModel::Validations + + validates :email, + :format => { + :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, #local part of email is case-sensitive, so allow uppercase letter. + :message => "needs to be a valid email address" + } + + def to_partial_path + "emails/email" + end + + def to_param + to_s + end + + def email + self + end + + def handle + self.split('@').first + end + +end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 0000000..9b97b51 --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,136 @@ +class Identity < CouchRest::Model::Base + include LoginFormatValidation + + use_database :identities + + belongs_to :user + + property :address, LocalEmail + property :destination, Email + property :keys, HashWithIndifferentAccess + + validate :unique_forward + validate :alias_available + validate :address_local_email + validate :destination_email + + design do + view :by_user_id + view :by_address_and_destination + view :by_address + view :pgp_key_by_email, + map: <<-EOJS + function(doc) { + if (doc.type != 'Identity') { + return; + } + if (typeof doc.keys === "object") { + emit(doc.address, doc.keys["pgp"]); + } + } + EOJS + view :disabled, + map: <<-EOJS + function(doc) { + if (doc.type != 'Identity') { + return; + } + if (typeof doc.user_id === "undefined") { + emit(doc._id, 1); + } + } + EOJS + + end + + def self.for(user, attributes = {}) + find_for(user, attributes) || build_for(user, attributes) + end + + def self.find_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + find_by_address_and_destination [attributes[:address], attributes[:destination]] + end + + def self.build_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + Identity.new(attributes) + end + + def self.create_for(user, attributes = {}) + identity = build_for(user, attributes) + identity.save + identity + end + + def self.disable_all_for(user) + Identity.by_user_id.key(user.id).each do |identity| + identity.disable + identity.save + end + end + + def self.destroy_all_disabled + Identity.disabled.each do |identity| + identity.destroy + end + end + + def self.attributes_from_user(user) + { user_id: user.id, + address: user.email_address, + destination: user.email_address + } + end + + def enabled? + self.destination && self.user_id + end + + def disable + self.destination = nil + self.user_id = nil + end + + def keys + read_attribute('keys') || HashWithIndifferentAccess.new + end + + def set_key(type, key) + return if keys[type] == key.to_s + write_attribute('keys', keys.merge(type => key.to_s)) + end + + # for LoginFormatValidation + def login + self.address.handle + end + + protected + + def unique_forward + same = Identity.find_by_address_and_destination([address, destination]) + if same && same != self + errors.add :base, "This alias already exists" + end + end + + def alias_available + same = Identity.find_by_address(address) + if same && same.user != self.user + errors.add :base, "This email has already been taken" + end + end + + def address_local_email + return if address.valid? #this ensures it is LocalEmail + self.errors.add(:address, address.errors.messages[:email].first) #assumes only one error + end + + def destination_email + return if destination.nil? # this identity is disabled + return if destination.valid? # this ensures it is Email + self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO + end + +end diff --git a/app/models/local_email.rb b/app/models/local_email.rb new file mode 100644 index 0000000..2b4c65e --- /dev/null +++ b/app/models/local_email.rb @@ -0,0 +1,68 @@ +class LocalEmail < Email + + BLACKLIST_FROM_RFC2142 = [ + 'postmaster', 'hostmaster', 'domainadmin', 'webmaster', 'www', + 'abuse', 'noc', 'security', 'usenet', 'news', 'uucp', + 'ftp', 'sales', 'marketing', 'support', 'info' + ] + + def self.domain + APP_CONFIG[:domain] + end + + validates :email, + :format => { + :with => /@#{domain}\Z/i, + :message => "needs to end in @#{domain}" + } + + validate :handle_allowed + + def initialize(s) + super + append_domain_if_needed + end + + def to_key + [handle] + end + + def domain + LocalEmail.domain + end + + protected + + def append_domain_if_needed + unless self.index('@') + self << '@' + domain + end + end + + def handle_allowed + errors.add(:handle, "is reserved.") if handle_reserved? + end + + def handle_reserved? + # *ARRAY in a case statement tests if ARRAY includes the handle. + case handle + when *APP_CONFIG[:handle_blacklist] + true + when *APP_CONFIG[:handle_whitelist] + false + when *BLACKLIST_FROM_RFC2142 + true + else + handle_in_passwd? + end + end + + def handle_in_passwd? + begin + !!Etc.getpwnam(handle) + rescue ArgumentError + # handle was not found + return false + end + end +end diff --git a/app/models/login_format_validation.rb b/app/models/login_format_validation.rb new file mode 100644 index 0000000..c1fcf70 --- /dev/null +++ b/app/models/login_format_validation.rb @@ -0,0 +1,21 @@ +module LoginFormatValidation + extend ActiveSupport::Concern + + #TODO: Probably will replace this. Playing with using it for aliases too, but won't want it connected to login field. + + included do + # Have multiple regular expression validations so we can get specific error messages: + validates :login, + :format => { :with => /\A.{2,}\z/, + :message => "Must have at least two characters"} + validates :login, + :format => { :with => /\A[a-z\d_\.-]+\z/, + :message => "Only lowercase letters, digits, . - and _ allowed."} + validates :login, + :format => { :with => /\A[a-z].*\z/, + :message => "Must begin with a lowercase letter"} + validates :login, + :format => { :with => /\A.*[a-z\d]\z/, + :message => "Must end with a letter or digit"} + end +end diff --git a/app/models/message.rb b/app/models/message.rb new file mode 100644 index 0000000..424f094 --- /dev/null +++ b/app/models/message.rb @@ -0,0 +1,29 @@ +class Message < CouchRest::Model::Base + + use_database :messages + + property :text, String + property :user_ids_to_show, [String] + property :user_ids_have_shown, [String] # is this necessary to store? + + timestamps! + + design do + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'message')) + end + + def mark_as_read_by(user) + user_ids_to_show.delete(user.id) + # is it necessary to keep track of what users have already seen it? + user_ids_have_shown << user.id unless read_by?(user) + end + + def read_by?(user) + user_ids_have_shown.include?(user.id) + end + + def unread_by?(user) + user_ids_to_show.include?(user.id) + end +end diff --git a/app/models/pgp_key.rb b/app/models/pgp_key.rb new file mode 100644 index 0000000..66f8660 --- /dev/null +++ b/app/models/pgp_key.rb @@ -0,0 +1,48 @@ +class PgpKey + include ActiveModel::Validations + + KEYBLOCK_IDENTIFIERS = [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----', + '-----END PGP PUBLIC KEY BLOCK-----', + ] + + # mostly for testing. + attr_accessor :keyblock + + validate :validate_keyblock_format + + def initialize(keyblock = nil) + @keyblock = keyblock + end + + def to_s + @keyblock + end + + def present? + @keyblock.present? + end + + # allow comparison with plain keyblock strings. + def ==(other) + self.equal?(other) or + # relax the comparison on line ends. + self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '') + end + + protected + + def validate_keyblock_format + if keyblock_identifier_missing? + errors.add :public_key_block, + "does not look like an armored pgp public key block" + end + end + + def keyblock_identifier_missing? + KEYBLOCK_IDENTIFIERS.find do |identify| + !@keyblock.include?(identify) + end + end + +end diff --git a/app/models/service_level.rb b/app/models/service_level.rb new file mode 100644 index 0000000..299aaf1 --- /dev/null +++ b/app/models/service_level.rb @@ -0,0 +1,19 @@ +class ServiceLevel + + def initialize(attributes = {}) + @id = attributes[:id] || APP_CONFIG[:default_service_level] + end + + def self.authenticated_select_options + APP_CONFIG[:service_levels].map { |id,config_hash| [config_hash[:description], id] if config_hash[:name] != 'anonymous'}.compact + end + + def id + @id + end + + def config_hash + APP_CONFIG[:service_levels][@id] + end + +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..0d7e10e --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,32 @@ +class Session < SRP::Session + include ActiveModel::Validations + include LoginFormatValidation + + attr_accessor :login + + validates :login, :presence => true + + def initialize(user = nil, aa = nil) + super(user, aa) if user + end + + def persisted? + false + end + + def new_record? + true + end + + def to_model + self + end + + def to_key + [object_id] + end + + def to_param + nil + end +end diff --git a/app/models/token.rb b/app/models/token.rb new file mode 100644 index 0000000..4856c31 --- /dev/null +++ b/app/models/token.rb @@ -0,0 +1,69 @@ +class Token < CouchRest::Model::Base + + use_database :tokens + + belongs_to :user + + # timestamps! does not create setters and only sets updated_at + # if the object has changed and been saved. Instead of triggering + # that we rather use our own property we have control over: + property :last_seen_at, Time, accessible: false + + validates :user_id, presence: true + + design do + view :by_last_seen_at + end + + def self.expires_after + APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] + end + + def self.expired + return [] unless expires_after + by_last_seen_at.endkey(expires_after.minutes.ago) + end + + def self.destroy_all_expired + self.expired.each do |token| + token.destroy + end + end + + def authenticate + if expired? + destroy + return nil + else + touch + return user + end + end + + # Tokens can be cleaned up in different ways. + # So let's make sure we don't crash if they disappeared + def destroy_with_rescue + destroy_without_rescue + rescue RestClient::ResourceNotFound + end + alias_method_chain :destroy, :rescue + + def touch + self.last_seen_at = Time.now + save + end + + def expired? + Token.expires_after and + last_seen_at < Token.expires_after.minutes.ago + end + + def initialize(*args) + super + if new_record? + self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.last_seen_at = Time.now + end + end +end + diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb new file mode 100644 index 0000000..0fc17d2 --- /dev/null +++ b/app/models/unauthenticated_user.rb @@ -0,0 +1,6 @@ +# The nil object for the user class +class UnauthenticatedUser < Object + + # will probably want something here to return service level as APP_CONFIG[:service_levels][0] but not sure how will be accessing. + +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c297ac8 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,179 @@ +class User < CouchRest::Model::Base + include LoginFormatValidation + + use_database :users + + property :login, String, :accessible => true + property :password_verifier, String, :accessible => true + property :password_salt, String, :accessible => true + + property :enabled, TrueClass, :default => true + + # these will be null by default but we shouldn't ever pull them directly, but only via the methods that will return the full ServiceLevel + property :desired_service_level_code, Integer, :accessible => true + property :effective_service_level_code, Integer, :accessible => true + + property :one_month_warning_sent, TrueClass + + before_save :update_effective_service_level + + validates :login, :password_salt, :password_verifier, + :presence => true + + validates :login, + :uniqueness => true, + :if => :serverside? + + validate :login_is_unique_alias + + validates :password_salt, :password_verifier, + :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } + + validates :password, :presence => true, + :confirmation => true, + :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } + + timestamps! + + design do + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'user')) + view :by_login + view :by_created_at + end # end of design + + def to_json(options={}) + { + :login => login, + :ok => valid? + }.to_json(options) + end + + def salt + password_salt.hex + end + + def verifier + password_verifier.hex + end + + def username + login + end + + def email_address + LocalEmail.new(login) + end + + # Since we are storing admins by login, we cannot allow admins to change their login. + def is_admin? + APP_CONFIG['admins'].include? self.login + end + + def most_recent_tickets(count=3) + Ticket.for_user(self).limit(count).all #defaults to having most recent updated first + end + + def messages(unseen = true) + #TODO for now this only shows unseen messages. Will we ever want seen ones? Is it necessary to store? + + # we don't want to emit all the userids associated with a message, so only emit id and text. + Message.by_user_ids_to_show.key(self.id).map { |message| [message.id, message.text] } + + end + + # DEPRECATED + # + # Please set the key on the identity directly + # WARNING: This will not be serialized with the user record! + # It is only a workaround for the key form. + def public_key=(value) + identity.set_key(:pgp, value) + end + + # DEPRECATED + # + # Please access identity.keys[:pgp] directly + def public_key + identity.keys[:pgp] + end + + def account + Account.new(self) + end + + def identity + @identity ||= Identity.for(self) + end + + def refresh_identity + @identity = Identity.for(self) + end + + def desired_service_level + code = self.desired_service_level_code || APP_CONFIG[:default_service_level] + ServiceLevel.new({id: code}) + end + + def effective_service_level + code = self.effective_service_level_code || self.desired_service_level.id + ServiceLevel.new({id: code}) + end + + + def self.send_one_month_warnings + + # To determine warnings to send, need to get all users where one_month_warning_sent is not set, and where it was created greater than or equal to 1 month ago. + # TODO: might want to further limit to enabled accounts, and, based on provider's service level configuration, for particular service levels. + users_to_warn = User.by_created_at_and_one_month_warning_not_sent.endkey(Time.now-1.month) + + users_to_warn.each do |user| + # instead of loop could use something like: + # message.user_ids_to_show = users_to_warn.map(&:id) + # but would still need to loop through users to store one_month_warning_sent + + if !@message + # create a message for today's date + # only want to create once, and only if it will be used. + @message = Message.new(:text => I18n.t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + end + + @message.user_ids_to_show << user.id + user.one_month_warning_sent = true + user.save + end + @message.save if @message + + end + + protected + + ## + # Validation Functions + ## + + def login_is_unique_alias + alias_identity = Identity.find_by_address(self.email_address) + return if alias_identity.blank? + if alias_identity.user != self + errors.add(:login, "has already been taken") + end + end + + def password + password_verifier + end + + # used as a condition for validations that are server side only + def serverside? + true + end + + def update_effective_service_level + # TODO: Is this always the case? Might there be a situation where the admin has set the effective service level and we don't want it changed to match the desired one? + if self.desired_service_level_code_changed? + self.effective_service_level_code = self.desired_service_level_code + end + end + +end diff --git a/app/views/.gitkeep b/app/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/views/emails/_email.html.haml b/app/views/emails/_email.html.haml new file mode 100644 index 0000000..ea59cec --- /dev/null +++ b/app/views/emails/_email.html.haml @@ -0,0 +1,6 @@ += wrapped(email, local_assigns) do + = email + - if local_assigns[:with].try(:include?, :delete) + = link_to(user_email_alias_path(@user, email), :method => :delete) do + %i.icon-remove + diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml new file mode 100644 index 0000000..771dc97 --- /dev/null +++ b/app/views/sessions/new.html.haml @@ -0,0 +1,10 @@ +.span1 +.span9 + = render :partial => 'users/warnings' + %h2=t :login + = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } 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 } + .form-actions + = f.button :submit, :value => t(:login), :class => 'btn-primary' + = link_to t(:cancel), home_path, :class => 'btn' diff --git a/app/views/sessions/new.json.erb b/app/views/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/app/views/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/app/views/users/_change_password.html.haml b/app/views/users/_change_password.html.haml new file mode 100644 index 0000000..425e3ee --- /dev/null +++ b/app/views/users/_change_password.html.haml @@ -0,0 +1,21 @@ +-# +-# CHANGE PASSWORD +-# +-# * everything about this form is handled with javascript. So take care when changing any ids. +-# * the login is required when changing the password because it is used as part of the salt when calculating the password verifier. +-# however, we don't want the user to change their login without generating a new key, so we hide the ui for this +-# (although it works perfectly fine to change username if the field was visible). +-# + +- form_options = {:url => '/not-used', :html => {:class => user_form_class('form-horizontal'), :id => 'update_login_and_password', :data => {token: session[:token]}}, :validate => true} += simple_form_for @user, form_options do |f| + %legend= t(:change_password) + = hidden_field_tag 'user_param', @user.to_param + .hidden + = 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 + .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 new file mode 100644 index 0000000..e465125 --- /dev/null +++ b/app/views/users/_change_pgp_key.html.haml @@ -0,0 +1,13 @@ +-# +-# CHANGE PGP KEY +-# +-# this will be replaced by a identities controller/view at some point +-# + +- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_pgp_key', :data => {token: session[:token]}}, :validate => true} += 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 + .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 new file mode 100644 index 0000000..61e67d9 --- /dev/null +++ b/app/views/users/_change_service_level.html.haml @@ -0,0 +1,18 @@ +-# 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] + - 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) + - if @user != current_user + = t(:desired_service_level) + = f.select :desired_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.desired_service_level.id + - if @user != current_user + %p + = t(:effective_service_level) + = f.select :effective_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.effective_service_level.id + .control-group + .controls + = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/app/views/users/_destroy_account.html.haml b/app/views/users/_destroy_account.html.haml new file mode 100644 index 0000000..445f3c4 --- /dev/null +++ b/app/views/users/_destroy_account.html.haml @@ -0,0 +1,27 @@ +-# +-# DESTROY ACCOUNT +-# + +%legend + - if @user == current_user + = t(:destroy_my_account) + - else + = t(:admin_destroy_account, :username => @user.login) +%p= t(:destroy_account_info) += link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do + %i.icon-remove.icon-white + = t(:destroy_my_account) +- if @user != current_user and @user.enabled? + %legend + = t(:deactivate_account, :username => @user.login) + %p= t(:deactivate_description) + = link_to deactivate_user_path(@user), :method => :post, :class => "btn btn-warning" do + %i.icon-pause.icon-white + = t(:deactivate) +- elsif @user != current_user and !@user.enabled? + %legend + = t(:enable_account, :username => @user.login) + %p= t(:enable_description) + = link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do + %i.icon-ok.icon-white + = t(:enable) diff --git a/app/views/users/_edit.html.haml b/app/views/users/_edit.html.haml new file mode 100644 index 0000000..1d2b68a --- /dev/null +++ b/app/views/users/_edit.html.haml @@ -0,0 +1,14 @@ +-# +-# edit user form, used by both show and edit actions. +-# +-# We render a bunch of forms here. Which we use depends upon config settings +-# user_actions and admin_actions. They both include an array of actions +-# allowed to users and admins. +-# Possible forms are: +-# 'change_password' +-# 'change_pgp_key' +-# 'change_service_level' +-# 'destroy_account' +- actions = APP_CONFIG[admin? ? :admin_actions : :user_actions] || [] +- actions.each do |action| + = render action diff --git a/app/views/users/_user.html.haml b/app/views/users/_user.html.haml new file mode 100644 index 0000000..583d22f --- /dev/null +++ b/app/views/users/_user.html.haml @@ -0,0 +1,4 @@ +%tr + %td= link_to user.login, user + %td= l(user.created_at, :format => :short) + %td= l(user.updated_at, :format => :short) diff --git a/app/views/users/_warnings.html.haml b/app/views/users/_warnings.html.haml new file mode 100644 index 0000000..79ab103 --- /dev/null +++ b/app/views/users/_warnings.html.haml @@ -0,0 +1,12 @@ +%noscript + %div.alert.alert-error=t :js_required_html +#cookie_warning.alert.alert-error{:style => "display:none"} + =t :cookie_disabled_warning +:javascript + document.cookie = "testing=cookies_enabled; path=/"; + if(document.cookie.indexOf("testing=cookies_enabled") < 0) + { + document.getElementById('cookie_warning').style.display = 'block'; + } else { + document.getElementById('cookie_warning').style.display = 'none'; + } \ No newline at end of file diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100644 index 0000000..434c025 --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1 @@ += render 'edit' diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 0000000..fc1001e --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,13 @@ +- @show_navigation = false + += form_tag users_path, :method => :get, :class => "form-search" do + .input-append + = text_field_tag :query, params[:query], :id => 'user-typeahead', :class => "search-query", :autocomplete => :off + %button.btn{:type => :submit}= t(:search) + +%table.table.table-striped + %tr + %th= t(:username) + %th= t(:created) + %th= t(:updated) + = render @users.all diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml new file mode 100644 index 0000000..aecf831 --- /dev/null +++ b/app/views/users/new.html.haml @@ -0,0 +1,19 @@ +-# +-# This form is handled entirely by javascript, so take care when changing element ids. +-# + +- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} + +.span1 +.span9 + = render :partial => 'warnings' + %h2=t :signup + = simple_form_for(@user, form_options) do |f| + %legend= t(:signup_message) + = 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, :validate => true, :input_html => { :id => :srp_password_confirmation } + .form-actions + = f.button :submit, :value => t(:signup), :class => 'btn btn-primary' + = link_to t(:cancel), home_path, :class => 'btn' + diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100644 index 0000000..c587017 --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1,25 @@ +.overview + + %h2.first= t(:overview_welcome, :username => @user.login) + + - if admin? + %p + = t(:created) + = @user.created_at + %br + = t(:updated) + = @user.updated_at + %br + = t(:enabled) + = @user.enabled? + + %p= t(:overview_intro) + + %ul.unstyled + %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user)) + - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly} + %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user)) + %li= icon('shopping-cart') + link_to(t(:overview_billing), billing_top_link(@user)) if APP_CONFIG[:billing] + .container-fluid + .row-fluid + = home_page_buttons(true) \ No newline at end of file diff --git a/app/views/v1/sessions/new.json.erb b/app/views/v1/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/app/views/v1/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/app/views/webfinger/host_meta.xml.erb b/app/views/webfinger/host_meta.xml.erb new file mode 100644 index 0000000..cfcbcc0 --- /dev/null +++ b/app/views/webfinger/host_meta.xml.erb @@ -0,0 +1,11 @@ + + + + <%= @host_meta.subject %> + + <%- @host_meta.links.each do |rel, link| %> + + <%- end %> + diff --git a/app/views/webfinger/search.xml.erb b/app/views/webfinger/search.xml.erb new file mode 100644 index 0000000..7328552 --- /dev/null +++ b/app/views/webfinger/search.xml.erb @@ -0,0 +1,7 @@ + + + <%= @presenter.subject %> + <%- @presenter.links.each do |rel, link| %> + type=<%=link[:type]%> href="<%= link[:key] %>"/> + <% end %> + diff --git a/test/factories.rb b/test/factories.rb index 6c671f8..98bb39b 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -1,3 +1,37 @@ Dir.glob(Rails.root.join('**','test','factories.rb')) do |factory_file| require factory_file end +FactoryGirl.define do + + factory :user do + login { Faker::Internet.user_name } + password_verifier "1234ABCD" + password_salt "4321AB" + + factory :user_with_settings do + email_forward { Faker::Internet.email } + email_aliases_attributes do + {:a => Faker::Internet.user_name + '@' + APP_CONFIG[:domain]} + end + end + + factory :admin_user do + after(:build) do |admin| + admin.stubs(:is_admin?).returns(true) + end + end + end + + factory :token do + user + end + + factory :pgp_key do + keyblock <<-EOPGP +-----BEGIN PGP PUBLIC KEY BLOCK----- ++Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+ +#{SecureRandom.base64(4032)} +-----END PGP PUBLIC KEY BLOCK----- + EOPGP + end +end diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb new file mode 100644 index 0000000..c4c922b --- /dev/null +++ b/test/functional/application_controller_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class ApplicationControllerTest < ActionController::TestCase + + def setup + # so we can test the effect on the response + @controller.response = @response + end + + def test_require_login_redirect + @controller.send(:require_login) + assert_access_denied(true, false) + end + + def test_require_login + login + @controller.send(:require_login) + assert_access_denied(false) + end + + def test_require_admin + login + @current_user.expects(:is_admin?).returns(false) + @controller.send(:require_admin) + assert_access_denied + end + +end diff --git a/test/functional/helper_methods_test.rb b/test/functional/helper_methods_test.rb new file mode 100644 index 0000000..44226ae --- /dev/null +++ b/test/functional/helper_methods_test.rb @@ -0,0 +1,39 @@ +# +# Testing and documenting the helper methods available from +# ApplicationController +# + +require 'test_helper' + +class HelperMethodsTest < ActionController::TestCase + tests ApplicationController + + # we test them right in here... + include ApplicationController._helpers + + # the helpers all reference the controller. + def controller + @controller + end + + def test_current_user + login + assert_equal @current_user, current_user + end + + def test_logged_in + login + assert logged_in? + end + + def test_logged_out + assert !logged_in? + end + + def test_admin + login + @current_user.expects(:is_admin?).returns(bool = stub) + assert_equal bool, admin? + end + +end diff --git a/test/functional/keys_controller_test.rb b/test/functional/keys_controller_test.rb new file mode 100644 index 0000000..863be93 --- /dev/null +++ b/test/functional/keys_controller_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class KeysControllerTest < ActionController::TestCase + + test "get existing public key" do + public_key = 'my public key' + @user = stub_record :user, :public_key => public_key + User.stubs(:find_by_login).with(@user.login).returns(@user) + get :show, :login => @user.login + assert_response :success + assert_equal "text/text", response.content_type + assert_equal public_key, response.body + end + + test "get non-existing public key for user" do + # this isn't a scenerio that should generally occur. + @user = stub_record :user + User.stubs(:find_by_login).with(@user.login).returns(@user) + get :show, :login => @user.login + assert_response :success + assert_equal "text/text", response.content_type + assert_equal '', response.body.strip + end + + test "get public key for non-existing user" do + # raise 404 error if user doesn't exist (doesn't need to be this routing error, but seems fine to assume for now): + assert_raise(ActionController::RoutingError) { + get :show, :login => 'asdkljslksjfdlskfj' + } + end + +end diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb new file mode 100644 index 0000000..fe7903f --- /dev/null +++ b/test/functional/sessions_controller_test.rb @@ -0,0 +1,59 @@ +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 SessionsControllerTest < ActionController::TestCase + + setup do + @user = stub :login => "me", :id => 123 + @client_hex = 'a123' + end + + test "should get login screen" do + get :new + assert_response :success + assert_equal "text/html", response.content_type + assert_template "sessions/new" + end + + test "redirect to home_url if logged in" do + login + get :new + assert_response :redirect + assert_redirected_to home_url + end + + test "renders json" do + get :new, :format => :json + assert_response :success + assert_json_error nil + end + + test "renders warden errors" do + request.env['warden.options'] = {attempted_path: '/1/sessions/asdf.json'} + 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 + assert_response 422 + assert_json_error :field => "translation stub" + end + + test "renders failed attempt message" do + request.env['warden.options'] = {attempted_path: '/1/sessions/asdf.json'} + request.env['warden'].stubs(:winning_strategy).returns(nil) + get :new, :format => :json + assert_response 422 + assert_json_error :login => I18n.t(:all_strategies_failed) + end + + test "destory should logout" do + login + expect_logout + delete :destroy + assert_response :redirect + assert_redirected_to home_url + end + +end diff --git a/test/functional/test_helpers_test.rb b/test/functional/test_helpers_test.rb new file mode 100644 index 0000000..845e516 --- /dev/null +++ b/test/functional/test_helpers_test.rb @@ -0,0 +1,38 @@ +# +# There are a few test helpers for dealing with login etc. +# We test them here and also document their behaviour. +# + +require 'test_helper' + +class TestHelpersTest < ActionController::TestCase + tests ApplicationController # testing no controller in particular + + def test_login_stubs_warden + login + assert_equal @current_user, request.env['warden'].user + end + + def test_login_token_authenticates + login + assert_equal @current_user, @controller.send(:token_authenticate) + end + + def test_login_stubs_token + login + assert @token + assert_equal @current_user, @token.authenticate + end + + def test_login_adds_token_header + login + token_present = @controller.authenticate_with_http_token do |token, options| + assert_equal @token.id, token + end + # authenticate_with_http_token just returns nil and does not + # execute the block if there is no token. So we have to also + # ensure it was run: + assert token_present + end +end + diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb new file mode 100644 index 0000000..0713836 --- /dev/null +++ b/test/functional/users_controller_test.rb @@ -0,0 +1,165 @@ +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 + assert_response :redirect + assert_redirected_to login_path + end + + test "user can see user" do + user = find_record :user, + :most_recent_tickets => [] + login user + get :show, :id => user.id + assert_response :success + end + + test "admin can see other user" do + user = find_record :user, + :most_recent_tickets => [] + login :is_admin? => true + get :show, :id => user.id + assert_response :success + + end + + test "user cannot see other user" do + user = find_record :user, + :most_recent_tickets => [] + login + get :show, :id => user.id + assert_response :redirect + assert_access_denied + end + + test "may not show non-existing user without auth" do + nonid = 'thisisnotanexistinguserid' + + get :show, :id => nonid + assert_access_denied(true, false) + end + + test "may not show non-existing user without admin" do + nonid = 'thisisnotanexistinguserid' + login + + get :show, :id => nonid + assert_access_denied + end + + test "redirect admin to user list for non-existing user" do + nonid = 'thisisnotanexistinguserid' + login :is_admin? => true + get :show, :id => nonid + assert_response :redirect + assert_equal({:alert => "No such user."}, flash.to_hash) + assert_redirected_to users_path + end + + test "should get edit view" do + user = find_record :user + + login user + get :edit, :id => user.id + + assert_equal user, assigns[:user] + end + + test "admin can destroy user" do + user = find_record :user + + # we destroy the user record and the associated data... + user.expects(:destroy) + Identity.expects(:disable_all_for).with(user) + Ticket.expects(:destroy_all_from).with(user) + + login :is_admin? => true + delete :destroy, :id => user.id + + assert_response :redirect + assert_redirected_to users_path + end + + test "user can cancel account" do + user = find_record :user + + # we destroy the user record and the associated data... + user.expects(:destroy) + Identity.expects(:disable_all_for).with(user) + Ticket.expects(:destroy_all_from).with(user) + + login user + expect_logout + delete :destroy, :id => @current_user.id + + assert_response :redirect + assert_redirected_to bye_url + end + + test "non-admin can't destroy user" do + user = find_record :user + + login + delete :destroy, :id => user.id + + assert_access_denied + end + + test "admin can list users" do + login :is_admin? => true + get :index + + assert_response :success + assert assigns(:users) + end + + test "non-admin can't list users" do + login + get :index + + assert_access_denied + end + + test "admin can search users" do + login :is_admin? => true + get :index, :query => "a" + + assert_response :success + assert assigns(:users) + end + + test "user cannot enable own account" do + user = find_record :user + login + post :enable, :id => user.id + assert_access_denied + end + + test "admin can deactivate user" do + user = find_record :user + assert user.enabled? + user.expects(:save).returns(true) + + login :is_admin? => true + + post :deactivate, :id => user.id + assert !assigns(:user).enabled? + end + +end diff --git a/test/functional/v1/messages_controller_test.rb b/test/functional/v1/messages_controller_test.rb new file mode 100644 index 0000000..24a5b1f --- /dev/null +++ b/test/functional/v1/messages_controller_test.rb @@ -0,0 +1,57 @@ +require 'test_helper' + +class V1::MessagesControllerTest < ActionController::TestCase + + setup do + @user = FactoryGirl.build(:user) + @user.save + @message = Message.new(:text => 'a test message') + @message.user_ids_to_show << @user.id + @message.save + end + + teardown do + @message.destroy + @user.destroy + end + + test "get messages for user" do + login @user + get :index + assert response.body.include? @message.text + assert response.body.include? @message.id + end + + test "mark message read for user" do + login @user + assert @message.user_ids_to_show.include?(@user.id) + assert !@message.user_ids_have_shown.include?(@user.id) + put :update, :id => @message.id + @message.reload + assert !@message.user_ids_to_show.include?(@user.id) + assert @message.user_ids_have_shown.include?(@user.id) + assert_json_response true + end + + test "do not get seen messages" do + login @user + put :update, :id => @message.id + @message.reload + get :index + assert !(response.body.include? @message.text) + assert !(response.body.include? @message.id) + end + + + test "mark read responds even with bad inputs" do + login @user + put :update, :id => 'more nonsense' + assert_json_response false + end + + test "fails if not authenticated" do + get :index, :format => :json + assert_access_denied + end + +end diff --git a/test/functional/v1/sessions_controller_test.rb b/test/functional/v1/sessions_controller_test.rb new file mode 100644 index 0000000..df0d681 --- /dev/null +++ b/test/functional/v1/sessions_controller_test.rb @@ -0,0 +1,62 @@ +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 + + setup do + @request.env['HTTP_HOST'] = 'api.lvh.me' + @user = stub_record :user, {}, true + @client_hex = 'a123' + end + + test "renders json" do + get :new, :format => :json + assert_response :success + assert_json_error nil + end + + test "renders warden errors" do + request.env['warden.options'] = {attempted_path: 'path/to/controller'} + 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 + assert_response 422 + assert_json_error :field => "translation stub" + end + + # Warden takes care of parsing the params and + # rendering the response. So not much to test here. + test "should perform handshake" do + 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 + end + + test "should authenticate" do + request.env['warden'].expects(:authenticate!) + @controller.stubs(:current_user).returns(@user) + handshake = stub(:to_hash => {h: "ash"}) + session[:handshake] = handshake + + post :update, :id => @user.login, :client_auth => @client_hex + + assert_nil session[:handshake] + assert_response :success + assert json_response.keys.include?("id") + assert json_response.keys.include?("token") + assert token = Token.find(json_response['token']) + assert_equal @user.id, token.user_id + end + + test "destroy should logout" do + login + expect_logout + delete :destroy + assert_response 204 + end + +end diff --git a/test/functional/v1/users_controller_test.rb b/test/functional/v1/users_controller_test.rb new file mode 100644 index 0000000..7cd9b0c --- /dev/null +++ b/test/functional/v1/users_controller_test.rb @@ -0,0 +1,74 @@ +require 'test_helper' + +class V1::UsersControllerTest < ActionController::TestCase + + test "user can change settings" do + user = find_record :user + changed_attribs = record_attributes_for :user_with_settings + account_settings = stub + account_settings.expects(:update).with(changed_attribs) + Account.expects(:new).with(user).returns(account_settings) + + login user + put :update, :user => changed_attribs, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can update user" do + user = find_record :user + changed_attribs = record_attributes_for :user_with_settings + account_settings = stub + account_settings.expects(:update).with(changed_attribs) + Account.expects(:new).with(user).returns(account_settings) + + login :is_admin? => true + put :update, :user => changed_attribs, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + end + + test "user cannot update other user" do + user = find_record :user + login + put :update, :user => record_attributes_for(:user_with_settings), :id => user.id, :format => :json + assert_access_denied + end + + test "should create new user" do + user_attribs = record_attributes_for :user + user = User.new(user_attribs) + Account.expects(:create).with(user_attribs).returns(user) + + post :create, :user => user_attribs, :format => :json + + assert_nil session[:user_id] + assert_json_response user + assert_response :success + end + + test "should redirect to signup form on failed attempt" do + user_attribs = record_attributes_for :user + user_attribs.slice!('login') + user = User.new(user_attribs) + assert !user.valid? + Account.expects(:create).with(user_attribs).returns(user) + + post :create, :user => user_attribs, :format => :json + + assert_json_error user.errors.messages + assert_response 422 + end + + test "admin can autocomplete users" do + login :is_admin? => true + get :index, :query => 'a', :format => :json + + assert_response :success + assert assigns(:users) + end + +end diff --git a/test/functional/webfinger_controller_test.rb b/test/functional/webfinger_controller_test.rb new file mode 100644 index 0000000..6597b69 --- /dev/null +++ b/test/functional/webfinger_controller_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class WebfingerControllerTest < ActionController::TestCase + + test "get host meta xml" do + get :host_meta, :format => :xml + assert_response :success + assert_equal "application/xml", response.content_type + end + + test "get host meta json" do + get :host_meta, :format => :json + assert_response :success + assert_equal "application/json", response.content_type + end + + test "get user webfinger xml" do + @user = stub_record :user, :public_key => 'my public key' + User.stubs(:find_by_login).with(@user.login).returns(@user) + get :search, :q => @user.email_address.to_s, :format => :xml + assert_response :success + assert_equal "application/xml", response.content_type + end + + test "get user webfinger json" do + @user = stub_record :user, :public_key => 'my public key' + User.stubs(:find_by_login).with(@user.login).returns(@user) + get :search, :q => @user.email_address.to_s, :format => :json + assert_response :success + assert_equal "application/json", response.content_type + end + +end diff --git a/test/integration/api/Readme.md b/test/integration/api/Readme.md new file mode 100644 index 0000000..04363bd --- /dev/null +++ b/test/integration/api/Readme.md @@ -0,0 +1,23 @@ +API tests +========== + + +Testing the restful api from a simple python client as that's what we'll be using. + +This test so far mostly demoes the API. We have no SRP calc in there. + +TODO: keep track of the cookies during login. The server uses the session to keep track of the random numbers A and B. + +The output of signup_and_login_wrong_password pretty well describes the SRP API: + +``` +POST: http://localhost:9292/users.json + {"user[password_salt]": "54321", "user[password_verifier]": "12345", "user[login]": "SWQ055"} + -> {"password_salt":"54321","login":"SWQ055"} +POST: http://localhost:9292/sessions + {"A": "12345", "login": "SWQ055"} + -> {"B":"1778367531e93a4c7713c76f67649f35a4211ebc520926ae8c3848cd66171651"} +PUT: http://localhost:9292/sessions/SWQ055 + {"M": "123ABC"} + -> {"errors":[{"login":"Not a valid username/password combination"},{"password":"Not a valid username/password combination"}]} +``` diff --git a/test/integration/api/login_test.rb b/test/integration/api/login_test.rb new file mode 100644 index 0000000..92d153f --- /dev/null +++ b/test/integration/api/login_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' +require_relative 'srp_test' + +class LoginTest < SrpTest + + setup do + register_user + end + + test "requires handshake before validation" do + validate("bla") + assert_json_error login: I18n.t(:all_strategies_failed) + end + + test "login with srp" do + authenticate + assert_equal ["M2", "id", "token"], server_auth.keys + assert last_response.successful? + assert_nil server_auth["errors"] + assert server_auth["M2"] + end + + test "wrong password login attempt" do + authenticate password: "wrong password" + assert_json_error "base" => "Not a valid username/password combination" + assert !last_response.successful? + assert_nil server_auth["M2"] + end + + test "wrong username login attempt" do + assert_raises RECORD_NOT_FOUND do + authenticate login: "wrong login" + end + assert_json_error "base" => "Not a valid username/password combination" + assert !last_response.successful? + assert_nil server_auth + end + + test "logout" do + authenticate + logout + assert_equal 204, last_response.status + end + + test "logout requires token" do + authenticate + logout(nil, {}) + assert_equal 422, last_response.status + end +end diff --git a/test/integration/api/pgp_key_test.rb b/test/integration/api/pgp_key_test.rb new file mode 100644 index 0000000..4c7fb4c --- /dev/null +++ b/test/integration/api/pgp_key_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' +require_relative 'srp_test' + +class PgpKeyTest < SrpTest + + setup do + # todo: prepare user and login without doing the srp dance + register_user + authenticate + end + + test "upload pgp key" do + update_user public_key: key + assert_equal key, Identity.for(@user).keys[:pgp] + end + + # eventually probably want to remove most of this into a non-integration + # functional test + test "prevent uploading invalid key" do + update_user public_key: "invalid key" + assert_nil Identity.for(@user).keys[:pgp] + end + + test "prevent emptying public key" do + update_user public_key: key + update_user public_key: "" + assert_equal key, Identity.for(@user).keys[:pgp] + end + + protected + + def key + @key ||= FactoryGirl.build :pgp_key + end +end diff --git a/test/integration/api/python/flow_with_srp.py b/test/integration/api/python/flow_with_srp.py new file mode 100755 index 0000000..9fc168b --- /dev/null +++ b/test/integration/api/python/flow_with_srp.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# under development + +import requests +import json +import string +import random +import srp._pysrp as srp +import binascii + +safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) + +# using globals for now +# server = 'https://dev.bitmask.net/1' +server = 'http://api.lvh.me:3000/1' + +def run_tests(): + login = 'test_' + id_generator() + password = id_generator() + id_generator() + usr = srp.User( login, password, srp.SHA256, srp.NG_1024 ) + print_and_parse(signup(login, password)) + + auth = print_and_parse(authenticate(usr)) + verify_or_debug(auth, usr) + assert usr.authenticated() + + usr = change_password(auth['id'], login, auth['token']) + + auth = print_and_parse(authenticate(usr)) + verify_or_debug(auth, usr) + # At this point the authentication process is complete. + assert usr.authenticated() + +# let's have some random name +def id_generator(size=6, chars=string.ascii_lowercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +# log the server communication +def print_and_parse(response): + request = response.request + print request.method + ': ' + response.url + if hasattr(request, 'data'): + print " " + json.dumps(response.request.data) + print " -> " + response.text + try: + return json.loads(response.text) + except ValueError: + return None + +def signup(login, password): + salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) + user_params = { + 'user[login]': login, + 'user[password_verifier]': binascii.hexlify(vkey), + 'user[password_salt]': binascii.hexlify(salt) + } + return requests.post(server + '/users.json', data = user_params, verify = False) + +def change_password(user_id, login, token): + password = id_generator() + id_generator() + salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) + user_params = { + 'user[password_verifier]': binascii.hexlify(vkey), + 'user[password_salt]': binascii.hexlify(salt) + } + auth_headers = { 'Authorization': 'Token token="' + token + '"'} + print user_params + print_and_parse(requests.put(server + '/users/' + user_id + '.json', data = user_params, verify = False, headers = auth_headers)) + return srp.User( login, password, srp.SHA256, srp.NG_1024 ) + + +def authenticate(usr): + session = requests.session() + uname, A = usr.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + init = print_and_parse(session.post(server + '/sessions', data = params, verify=False)) + M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) + return session.put(server + '/sessions/' + uname, verify = False, + data = {'client_auth': binascii.hexlify(M)}) + +def verify_or_debug(auth, usr): + if ( 'errors' in auth ): + print ' u = "%x"' % usr.u + print ' x = "%x"' % usr.x + print ' v = "%x"' % usr.v + print ' S = "%x"' % usr.S + print ' K = "' + binascii.hexlify(usr.K) + '"' + print ' M = "' + binascii.hexlify(usr.M) + '"' + else: + usr.verify_session( safe_unhexlify(auth["M2"]) ) + +run_tests() diff --git a/test/integration/api/python/login_wrong_username.py b/test/integration/api/python/login_wrong_username.py new file mode 100755 index 0000000..390f250 --- /dev/null +++ b/test/integration/api/python/login_wrong_username.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +server = 'http://localhost:3000' + +import requests +import json +import string +import random + +def id_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +params = { + 'login': 'python_test_user_'+id_generator(), + 'A': '12345', + } +r = requests.post(server + '/sessions', data = params) +print r.url +print r.text diff --git a/test/integration/api/python/signup.py b/test/integration/api/python/signup.py new file mode 100755 index 0000000..0d3a4e0 --- /dev/null +++ b/test/integration/api/python/signup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +server = 'http://localhost:3000' + +import requests +import json +import string +import random + +def id_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +user_params = { + 'user[login]': 'python_test_user_'+id_generator(), + 'user[password_verifier]': '12345', + 'user[password_salt]': '54321' + } +r = requests.post(server + '/users.json', data = user_params) +print r.url +print r.text diff --git a/test/integration/api/python/signup_and_login.py b/test/integration/api/python/signup_and_login.py new file mode 100755 index 0000000..ac611d7 --- /dev/null +++ b/test/integration/api/python/signup_and_login.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# FAILS +# +# This test is currently failing for me because the session is not kept. +# Played with it a bunch - is probably messed up right now as well. + + +server = 'http://localhost:3000' + +import requests +import json +import string +import random + +def id_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +def print_and_parse(response): + print response.request.method + ': ' + response.url + print " " + json.dumps(response.request.data) + print " -> " + response.text + return json.loads(response.text) + +def signup(session): + user_params = { + 'user[login]': id_generator(), + 'user[password_verifier]': '12345', + 'user[password_salt]': 'AB54321' + } + return session.post(server + '/users.json', data = user_params) + +def authenticate(session, login): + params = { + 'login': login, + 'A': '12345', + } + init = print_and_parse(session.post(server + '/sessions', data = params)) + return session.put(server + '/sessions/' + login, data = {'client_auth': '123'}) + +session = requests.session() +user = print_and_parse(signup(session)) +# SRP signup would happen here and calculate M hex +auth = print_and_parse(authenticate(session, user['login'])) diff --git a/test/integration/api/python/signup_and_login_wrong_password.py b/test/integration/api/python/signup_and_login_wrong_password.py new file mode 100755 index 0000000..9efffa1 --- /dev/null +++ b/test/integration/api/python/signup_and_login_wrong_password.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +server = 'http://localhost:9292' + +import requests +import json +import string +import random + +def id_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +def print_and_parse(response): + print response.request.method + ': ' + response.url + print " " + json.dumps(response.request.data) + print " -> " + response.text +# print " () " + json.dumps(requests.utils.dict_from_cookiejar(response.cookies)) + return json.loads(response.text) + +def signup(): + user_params = { + 'user[login]': id_generator(), + 'user[password_verifier]': '12345', + 'user[password_salt]': '54321' + } + return requests.post(server + '/users.json', data = user_params) + +def handshake(login): + params = { + 'login': login, + 'A': '12345', + } + return requests.post(server + '/sessions', data = params) + +def authenticate(login, M): + return requests.put(server + '/sessions/' + login, data = {'M': M}) + + +user = print_and_parse(signup()) +handshake = print_and_parse(handshake(user['login'])) +# SRP signup would happen here and calculate M hex +M = '123ABC' +auth = print_and_parse(authenticate(user['login'], M)) diff --git a/test/integration/api/python/umlauts.py b/test/integration/api/python/umlauts.py new file mode 100755 index 0000000..96fecbf --- /dev/null +++ b/test/integration/api/python/umlauts.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# coding: utf-8 + +# under development + +import requests +import json +import string +import random +import srp._pysrp as srp +import binascii + +safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) + +# using globals for now +# server = 'https://dev.bitmask.net/1' +server = 'http://api.lvh.me:3000/1' + +def run_tests(): + login = 'test_' + id_generator() + password = id_generator() + "äöì" + id_generator() + usr = srp.User( login, password, srp.SHA256, srp.NG_1024 ) + print_and_parse(signup(login, password)) + + auth = print_and_parse(authenticate(usr)) + verify_or_debug(auth, usr) + assert usr.authenticated() + + +# let's have some random name +def id_generator(size=6, chars=string.ascii_lowercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + +# log the server communication +def print_and_parse(response): + request = response.request + print request.method + ': ' + response.url + if hasattr(request, 'data'): + print " " + json.dumps(response.request.data) + print " -> " + response.text + try: + return json.loads(response.text) + except ValueError: + return None + +def signup(login, password): + salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) + user_params = { + 'user[login]': login, + 'user[password_verifier]': binascii.hexlify(vkey), + 'user[password_salt]': binascii.hexlify(salt) + } + print json.dumps(user_params) + return requests.post(server + '/users.json', data = user_params, verify = False) + +def authenticate(usr): + session = requests.session() + uname, A = usr.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + init = print_and_parse(session.post(server + '/sessions', data = params, verify=False)) + M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) + return session.put(server + '/sessions/' + uname, verify = False, + data = {'client_auth': binascii.hexlify(M)}) + +def verify_or_debug(auth, usr): + if ( 'errors' in auth ): + print ' u = "%x"' % usr.u + print ' x = "%x"' % usr.x + print ' v = "%x"' % usr.v + print ' S = "%x"' % usr.S + print ' K = "' + binascii.hexlify(usr.K) + '"' + print ' M = "' + binascii.hexlify(usr.M) + '"' + else: + usr.verify_session( safe_unhexlify(auth["M2"]) ) + +run_tests() diff --git a/test/integration/api/signup_test.rb b/test/integration/api/signup_test.rb new file mode 100644 index 0000000..236c547 --- /dev/null +++ b/test/integration/api/signup_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' +require_relative 'srp_test' + +class SignupTest < SrpTest + + setup do + register_user + end + + test "signup response" do + assert_json_response :login => @login, :ok => true + assert last_response.successful? + end + + test "signup creates user" do + assert @user + assert_equal @login, @user.login + end +end + diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb new file mode 100644 index 0000000..946450e --- /dev/null +++ b/test/integration/api/srp_test.rb @@ -0,0 +1,104 @@ +class SrpTest < RackTest + + teardown do + if @user + cleanup_user + end + Warden.test_reset! + end + + # 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", + :login => login, + 'A' => aa, + :format => :json + response = JSON.parse(last_response.body) + if response['errors'] + raise RECORD_NOT_FOUND.new(response['errors']) + else + return response['B'] + end + end + + def validate(m) + put "http://api.lvh.me:3000/1/sessions/" + @login + '.json', + :client_auth => m, + :format => :json + return JSON.parse(last_response.body) + end + + protected + + attr_reader :server_auth + + def register_user(login = "integration_test_user", password = 'srp, verify me!') + cleanup_user(login) + post 'http://api.lvh.me:3000/1/users.json', + user_params(login: login, password: password) + @user = User.find_by_login(login) + @login = login + @password = password + end + + def update_user(params) + put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', + user_params(params), + auth_headers + end + + def authenticate(params = nil) + @server_auth = srp(params).authenticate(self) + end + + def auth_headers + return {} if @server_auth.nil? + { + "HTTP_AUTHORIZATION" => encoded_token + } + end + + def encoded_token + ActionController::HttpAuthentication::Token.encode_credentials(server_auth["token"]) + end + + def logout(params=nil, headers=nil) + delete "http://api.lvh.me:3000/1/logout.json", + params || {format: :json}, + headers || auth_headers + end + + def cleanup_user(login = nil) + login ||= @user.login + Identity.by_address.key(login + '@' + APP_CONFIG[:domain]).each do |identity| + identity.destroy + end + if user = User.find_by_login(login) + user.destroy + end + end + + def user_params(params) + if params.keys.include?(:password) + srp_process_password(params) + end + return { user: params, format: :json } + end + + def srp_process_password(params) + params.reverse_merge! login: @login, salt: @salt + @srp = SRP::Client.new params[:login], password: params.delete(:password) + @salt = srp.salt.to_s(16) + params.merge! :password_verifier => srp.verifier.to_s(16), + :password_salt => @salt + end + + def srp(params = nil) + if params.nil? + @srp + else + params.reverse_merge! password: @password + SRP::Client.new(params.delete(:login) || @login, params) + end + end +end diff --git a/test/integration/api/update_account_test.rb b/test/integration/api/update_account_test.rb new file mode 100644 index 0000000..63429e7 --- /dev/null +++ b/test/integration/api/update_account_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' +require_relative 'srp_test' + +class UpdateAccountTest < SrpTest + + setup do + register_user + end + + test "require authentication" do + update_user password: "No! Verify me instead." + assert_access_denied + end + + test "require token" do + authenticate + put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', + user_params(password: "No! Verify me instead.") + assert_access_denied + end + + test "update password via api" do + authenticate + update_user password: "No! Verify me instead." + authenticate + assert last_response.successful? + assert_nil server_auth["errors"] + assert server_auth["M2"] + end + + test "change login with password_verifier" do + authenticate + new_login = 'zaph' + cleanup_user new_login + update_user login: new_login, password: @password + authenticate + assert last_response.successful? + assert_equal new_login, @user.reload.login + end + + test "prevent changing login without changing password_verifier" do + authenticate + original_login = @user.login + new_login = 'zaph' + cleanup_user new_login + update_user login: new_login + assert last_response.successful? + # does not change login if no password_verifier is present + assert_equal original_login, @user.reload.login + end +end diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb new file mode 100644 index 0000000..a5677ad --- /dev/null +++ b/test/integration/browser/account_test.rb @@ -0,0 +1,147 @@ +require 'test_helper' + +class AccountTest < BrowserIntegrationTest + + teardown do + Identity.destroy_all_disabled + end + + test "normal account workflow" do + username, password = submit_signup + assert page.has_content?("Welcome #{username}") + click_on 'Logout' + assert page.has_content?("Log In") + assert_equal '/', current_path + assert user = User.find_by_login(username) + user.account.destroy + end + + test "successful login" do + username, password = submit_signup + click_on 'Logout' + 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')) + 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 "default user actions" do + username, password = submit_signup + 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 + username, password = submit_signup + with_config admins: [username] 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 password" do + with_config user_actions: ['change_password'] do + username, password = submit_signup + click_on "Account Settings" + within('#update_login_and_password') do + fill_in 'Password', with: "other password" + fill_in 'Password confirmation', with: "other password" + click_on 'Save' + end + click_on 'Logout' + attempt_login(username, "other password") + assert page.has_content?("Welcome #{username}") + User.find_by_login(username).account.destroy + end + end + + test "change pgp key" do + with_config user_actions: ['change_pgp_key'] do + pgp_key = FactoryGirl.build :pgp_key + username, password = submit_signup + 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 = User.find_by_login(username) + assert_equal pgp_key, user.public_key + user.account.destroy + end + end + + + # trying to seed an invalid A for srp login + test "detects attempt to circumvent SRP" do + 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 + V1::UsersController.any_instance.stubs(:create).raises + submit_signup + assert page.has_content?("server failed") + 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? 'input.btn-primary.disabled' + assert page.has_content? I18n.t(:invalid_user_pass) + assert page.has_no_selector? 'input.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/session_test.rb b/test/integration/browser/session_test.rb new file mode 100644 index 0000000..3a41b3a --- /dev/null +++ b/test/integration/browser/session_test.rb @@ -0,0 +1,27 @@ +require 'test_helper' + +class SessionTest < BrowserIntegrationTest + + setup do + @username, password = submit_signup + end + + teardown do + user = User.find_by_login(@username) + id = user.identity + id.destroy + user.destroy + end + + test "valid session" do + assert page.has_content?("Welcome #{@username}") + end + + test "expired session" do + assert page.has_content?("Welcome #{@username}") + pretend_now_is(Time.now + 40.minutes) do + visit '/' + assert page.has_no_content?("Welcome #{@username}") + end + end +end diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb new file mode 100644 index 0000000..eec8c0e --- /dev/null +++ b/test/integration/navigation_test.rb @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..f142e54 --- /dev/null +++ b/test/leap_web_users_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class LeapWebUsersTest < ActiveSupport::TestCase + test "module exists" do + assert_kind_of Module, LeapWebUsers + end +end diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb new file mode 100644 index 0000000..57f9f9b --- /dev/null +++ b/test/support/auth_test_helper.rb @@ -0,0 +1,65 @@ +module AuthTestHelper + extend ActiveSupport::Concern + + # Controller will fetch current user from warden. + # Make it pick up our current_user + included do + setup do + request.env['warden'] ||= stub :user => nil + end + end + + def login(user_or_method_hash = {}) + if user_or_method_hash.respond_to?(:reverse_merge) + user_or_method_hash.reverse_merge! :is_admin? => false + end + @current_user = stub_record(:user, user_or_method_hash) + request.env['warden'] = stub :user => @current_user + request.env['HTTP_AUTHORIZATION'] = header_for_token_auth + return @current_user + end + + def assert_access_denied(denied = true, logged_in = true) + if denied + if @response.content_type == 'application/json' + assert_json_response('error' => I18n.t(:not_authorized)) + assert_response :unprocessable_entity + else + if logged_in + assert_equal({:alert => I18n.t(:not_authorized)}, flash.to_hash) + assert_redirected_to home_url + else + assert_equal({:alert => I18n.t(:not_authorized_login)}, flash.to_hash) + assert_redirected_to login_url + end + end + else + assert flash[:alert].blank? + end + end + + def expect_logout + expect_warden_logout + @token.expects(:destroy) if @token + end + + protected + + def header_for_token_auth + @token = find_record(:token, :authenticate => @current_user) + ActionController::HttpAuthentication::Token.encode_credentials @token.id + end + + def expect_warden_logout + raw = mock('raw session') do + expects(:inspect) + end + request.env['warden'].expects(:raw_session).returns(raw) + request.env['warden'].expects(:logout) + end + +end + +class ActionController::TestCase + include AuthTestHelper +end diff --git a/test/support/stub_record_helper.rb b/test/support/stub_record_helper.rb new file mode 100644 index 0000000..25138a0 --- /dev/null +++ b/test/support/stub_record_helper.rb @@ -0,0 +1,53 @@ +module StubRecordHelper + + # + # We will stub find when called on the records class and + # return the record given. + # + # If no record is given but a hash or nil will create a stub based on + # that instead and returns the stub. + # + def find_record(factory, record_or_attribs_hash = {}) + record = stub_record factory, record_or_attribs_hash, true + klass = record.class + # find is just an alias for get with CouchRest Model + klass.stubs(:get).with(record.to_param.to_s).returns(record) + klass.stubs(:find).with(record.to_param.to_s).returns(record) + return record + end + + # Create a stub that has the usual functions of a database record. + # It won't fail on rendering a form for example. + # + # If the second parameter is a record we return the record itself. + # This way you can build functions that either take a record or a + # method hash to stub from. See find_record for an example. + def stub_record(factory, record_or_method_hash = {}, persisted=false) + if record_or_method_hash && !record_or_method_hash.is_a?(Hash) + return record_or_method_hash + end + FactoryGirl.build_stubbed(factory).tap do |record| + if persisted or record.persisted? + record_or_method_hash.reverse_merge! :created_at => Time.now, + :updated_at => Time.now, :id => Random.rand(100000).to_s + end + record.stubs(record_or_method_hash) if record_or_method_hash.present? + end + end + + # returns deep stringified attributes so they can be compared to + # what the controller receives as params + def record_attributes_for(factory, attribs_hash = nil) + FactoryGirl.attributes_for(factory, attribs_hash).tap do |attribs| + attribs.keys.each do |key| + val = attribs.delete(key) + attribs[key.to_s] = val.is_a?(Hash) ? val.stringify_keys! : val + end + end + end + +end + +class ActionController::TestCase + include StubRecordHelper +end diff --git a/test/support/time_test_helper.rb b/test/support/time_test_helper.rb new file mode 100644 index 0000000..f673f12 --- /dev/null +++ b/test/support/time_test_helper.rb @@ -0,0 +1,30 @@ +# Extend the Time class so that we can offset the time that 'now' +# returns. This should allow us to effectively time warp for functional +# tests that require limits per hour, what not. +class Time #:nodoc: + class < 0 + Identity.destroy_all_disabled + assert_equal 0, Identity.disabled.count + end + + def alias_name + @alias_name ||= Faker::Internet.user_name + end + + def forward_address + @forward_address ||= Faker::Internet.email + end + + def pgp_key_string + @pgp_key ||= "DUMMY PGP KEY ... "+SecureRandom.base64(4096) + end +end diff --git a/test/unit/local_email_test.rb b/test/unit/local_email_test.rb new file mode 100644 index 0000000..20ee7f1 --- /dev/null +++ b/test/unit/local_email_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +class LocalEmailTest < ActiveSupport::TestCase + + test "appends domain" do + local = LocalEmail.new(handle) + assert_equal LocalEmail.new(email), local + assert local.valid? + end + + test "returns handle" do + local = LocalEmail.new(email) + assert_equal handle, local.handle + end + + test "prints full email" do + local = LocalEmail.new(handle) + assert_equal email, "#{local}" + end + + test "validates domain" do + local = LocalEmail.new(Faker::Internet.email) + assert !local.valid? + assert_equal ["needs to end in @#{LocalEmail.domain}"], local.errors[:email] + end + + test "blacklists rfc2142" do + black_listed = LocalEmail.new('hostmaster') + assert !black_listed.valid? + end + + test "blacklists etc passwd" do + black_listed = LocalEmail.new('nobody') + assert !black_listed.valid? + end + + test "whitelist overwrites automatic blacklists" do + with_config handle_whitelist: ['nobody', 'hostmaster'] do + white_listed = LocalEmail.new('nobody') + assert white_listed.valid? + white_listed = LocalEmail.new('hostmaster') + assert white_listed.valid? + end + end + + test "blacklists from config" do + black_listed = LocalEmail.new('www-data') + assert !black_listed.valid? + end + + test "blacklist from config overwrites whitelist" do + with_config handle_whitelist: ['www-data'] do + black_listed = LocalEmail.new('www-data') + assert !black_listed.valid? + end + end + + def handle + @handle ||= Faker::Internet.user_name + end + + def email + handle + "@" + APP_CONFIG[:domain] + end +end diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb new file mode 100644 index 0000000..a3c6cf6 --- /dev/null +++ b/test/unit/token_test.rb @@ -0,0 +1,89 @@ +require 'test_helper' + +class ClientCertificateTest < ActiveSupport::TestCase + include StubRecordHelper + + setup do + @user = find_record :user + end + + test "new token for user" do + sample = Token.new(:user_id => @user.id) + assert sample.valid? + assert_equal @user.id, sample.user_id + assert_equal @user, sample.authenticate + end + + test "token id is secure" do + sample = Token.new(:user_id => @user.id) + other = Token.new(:user_id => @user.id) + assert sample.id, + "id is set on initialization" + assert sample.id[0..10] != other.id[0..10], + "token id prefixes should not repeat" + assert /[g-zG-Z]/.match(sample.id), + "should use non hex chars in the token id" + assert sample.id.size > 16, + "token id should be more than 16 chars long" + end + + test "token checks for user" do + sample = Token.new + assert !sample.valid?, "Token should require a user record" + end + + test "token updates timestamps" do + sample = Token.new(user_id: @user.id) + sample.last_seen_at = 1.minute.ago + sample.expects(:save) + assert_equal @user, sample.authenticate + assert Time.now - sample.last_seen_at < 1.minute, "last_seen_at has not been updated" + end + + test "token will not expire if token_expires_after is not set" do + sample = Token.new(user_id: @user.id) + sample.last_seen_at = 2.years.ago + with_config auth: {} do + sample.expects(:save) + assert_equal @user, sample.authenticate + end + end + + test "expired token returns nil on authenticate" do + sample = Token.new(user_id: @user.id) + sample.last_seen_at = 2.hours.ago + with_config auth: {token_expires_after: 60} do + sample.expects(:destroy) + assert_nil sample.authenticate + end + end + + test "Token.destroy_all_expired is noop if no expiry is set" do + expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago + with_config auth: {} do + Token.destroy_all_expired + end + assert_equal expired, Token.find(expired.id) + end + + test "Token.destroy_all_expired cleans up expired tokens only" do + expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago + fresh = FactoryGirl.create :token + with_config auth: {token_expires_after: 60} do + Token.destroy_all_expired + end + assert_nil Token.find(expired.id) + assert_equal fresh, Token.find(fresh.id) + fresh.destroy + end + + + test "Token.destroy_all_expired does not interfere with expired.authenticate" do + expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago + with_config auth: {token_expires_after: 60} do + Token.destroy_all_expired + end + assert_nil expired.authenticate + end + +end diff --git a/test/unit/unauthenticated_user_test.rb b/test/unit/unauthenticated_user_test.rb new file mode 100644 index 0000000..e5fafb8 --- /dev/null +++ b/test/unit/unauthenticated_user_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class UnauthenticatedUserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb new file mode 100644 index 0000000..ffbb7d8 --- /dev/null +++ b/test/unit/user_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + + include SRP::Util + setup do + @user = FactoryGirl.build(:user) + end + + test "design docs in database are authorative" do + assert !User.design_doc.auto_update, + "Automatic update of design docs should be disabled" + end + + test "test set of attributes should be valid" do + @user.valid? + assert_equal Hash.new, @user.errors.messages + end + + test "test require hex for password_verifier" do + @user.password_verifier = "QWER" + assert !@user.valid? + end + + test "test require alphanumerical for login" do + @user.login = "qw#r" + assert !@user.valid? + end + + test "verifier returns number for the hex in password_verifier" do + assert_equal @user.password_verifier.hex, @user.verifier + end + + test "salt returns number for the hex in password_salt" do + assert_equal @user.password_salt.hex, @user.salt + end + + test 'normal user is no admin' do + assert !@user.is_admin? + end + + test 'user with login in APP_CONFIG is an admin' do + admin_login = APP_CONFIG['admins'].first + @user.login = admin_login + assert @user.is_admin? + end + + test "login needs to be unique" do + other_user = FactoryGirl.create :user, login: @user.login + assert !@user.valid? + other_user.destroy + end + + test "login needs to be unique amongst aliases" do + other_user = FactoryGirl.create :user + id = Identity.create_for other_user, address: @user.login + assert !@user.valid? + id.destroy + other_user.destroy + end + + test "deprecated public key api still works" do + key = SecureRandom.base64(4096) + @user.public_key = key + assert_equal key, @user.public_key + end + +end diff --git a/test/unit/warden_strategy_secure_remote_password_test.rb b/test/unit/warden_strategy_secure_remote_password_test.rb new file mode 100644 index 0000000..e6fcfbe --- /dev/null +++ b/test/unit/warden_strategy_secure_remote_password_test.rb @@ -0,0 +1,63 @@ +class WardenStrategySecureRemotePasswordTest < ActiveSupport::TestCase + +# TODO : turn this into sth. real +=begin + setup do + @user = stub :login => "me", :id => 123 + @client_hex = 'a123' + @client_rnd = @client_hex.hex + @server_hex = 'b123' + @server_rnd = @server_hex.hex + @server_rnd_exp = 'e123'.hex + @salt = 'stub user salt' + @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp + @server_auth = 'adfe' + end + + + test "should perform handshake" do + @user.expects(:initialize_auth). + with(@client_rnd). + returns(@server_handshake) + @server_handshake.expects(:to_json). + returns({'B' => @server_hex, 'salt' => @salt}.to_json) + User.expects(:find).with(@user.login).returns(@user) + assert_equal @server_handshake, session[:handshake] + assert_response :success + assert_json_response :B => @server_hex, :salt => @salt + end + + test "should report user not found" do + unknown = "login_that_does_not_exist" + User.expects(:find).with(unknown).raises(RECORD_NOT_FOUND) + post :create, :login => unknown + assert_response :success + assert_json_error "login" => ["unknown user"] + end + + test "should authorize" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + returns(@user) + @server_handshake.expects(:to_json). + returns({:M2 => @server_auth}.to_json) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_json_response :M2 => @server_auth + assert_equal @user.id, session[:user_id] + end + + test "should report wrong password" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + raises(WRONG_PASSWORD) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_nil session[:user_id] + assert_json_error "password" => ["wrong password"] + end + +=end +end diff --git a/test/unit/webfinger/host_meta_presenter_test.rb b/test/unit/webfinger/host_meta_presenter_test.rb new file mode 100644 index 0000000..af86404 --- /dev/null +++ b/test/unit/webfinger/host_meta_presenter_test.rb @@ -0,0 +1,24 @@ +require 'test_helper' +require 'webfinger' +require 'json' + +class Webfinger::HostMetaPresenterTest < ActiveSupport::TestCase + + setup do + @request = stub( + url: "https://#{APP_CONFIG[:domain]}/.well-known/host-meta" + ) + @meta = Webfinger::HostMetaPresenter.new(@request) + end + + test "creates proper json" do + hash = JSON.parse @meta.to_json + assert_equal ["subject", "links"].sort, hash.keys.sort + hash.each do |key, value| + assert_equal @meta.send(key.to_sym).to_json, value.to_json + end + end + +end + + diff --git a/test/unit/webfinger/user_presenter_test.rb b/test/unit/webfinger/user_presenter_test.rb new file mode 100644 index 0000000..04aeb22 --- /dev/null +++ b/test/unit/webfinger/user_presenter_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' +require 'webfinger' +require 'json' + +class Webfinger::UserPresenterTest < ActiveSupport::TestCase + + + setup do + @user = stub( + username: 'testuser', + email_address: "testuser@#{APP_CONFIG[:domain]}" + ) + @request = stub( + host: APP_CONFIG[:domain] + ) + end + + test "user without key has no links" do + @user.stubs :public_key => nil + presenter = Webfinger::UserPresenter.new(@user, @request) + assert_equal Hash.new, presenter.links + end + + test "user with key has corresponding link" do + @user.stubs :public_key => "here's a key" + presenter = Webfinger::UserPresenter.new(@user, @request) + assert_equal [:public_key], presenter.links.keys + assert_equal "PGP", presenter.links[:public_key][:type] + assert_equal presenter.send(:key), presenter.links[:public_key][:href] + end + + test "key is base64 encoded" do + @user.stubs :public_key => "here's a key" + presenter = Webfinger::UserPresenter.new(@user, @request) + assert_equal Base64.encode64(@user.public_key), presenter.send(:key) + end + + test "creates proper json representation" do + @user.stubs :public_key => "here's a key" + presenter = Webfinger::UserPresenter.new(@user, @request) + hash = JSON.parse presenter.to_json + assert_equal ["subject", "links"].sort, hash.keys.sort + hash.each do |key, value| + assert_equal presenter.send(key.to_sym).to_json, value.to_json + end + end + + +end diff --git a/users/app/assets/images/leap_web_users/.gitkeep b/users/app/assets/images/leap_web_users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/assets/javascripts/leap_web_users/.gitkeep b/users/app/assets/javascripts/leap_web_users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/assets/javascripts/srp b/users/app/assets/javascripts/srp deleted file mode 160000 index 8f33d32..0000000 --- a/users/app/assets/javascripts/srp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8f33d32d40b1e21ae7fb9a92c78a275422af4217 diff --git a/users/app/assets/javascripts/users.js b/users/app/assets/javascripts/users.js deleted file mode 100644 index 8486756..0000000 --- a/users/app/assets/javascripts/users.js +++ /dev/null @@ -1,132 +0,0 @@ -(function() { - // - // LOCAL FUNCTIONS - // - - var poll_users, - prevent_default, - form_failed, - form_passed, - clear_errors, - update_user; - - prevent_default = function(event) { - return event.preventDefault(); - }; - - poll_users = function(query, process) { - return $.get("/1/users.json", { - query: query - }).done(process); - }; - - clear_errors = function() { - return $('#messages').empty(); - }; - - update_user = function(submitEvent) { - var form = submitEvent.target; - var token = form.dataset.token; - var url = form.action; - var req = $.ajax({ - url: url, - type: 'PUT', - headers: { Authorization: 'Token token="' + token + '"' }, - data: $(form).serialize() - }); - req.done( function() { - $(form).find('input[type="submit"]').button('reset'); - }); - }; - - markAsSubmitted = function(submitEvent) { - var form = submitEvent.target; - $(form).addClass('submitted') - // bootstrap loading state: - $(form).find('input[type="submit"]').button('loading'); - }; - - resetButtons = function(submitEvent) { - var form = $('form.submitted') - // bootstrap loading state: - $(form).find('input[type="submit"]').button('reset'); - $(form).removeClass('submitted') - }; - - // - // PUBLIC FUNCTIONS - // - - srp.session = new srp.Session(); - - srp.signedUp = function() { - return srp.login(); - }; - - srp.loggedIn = function() { - return window.location = '/'; - }; - - srp.updated = function() { - return window.location = '/users/' + srp.session.id(); - }; - - // - // if a json request returns an error, this function gets called and - // decorates the appropriate fields with the error messages. - // - srp.error = function(message) { - clear_errors(); - var errors = extractErrors(message); - displayErrors(errors); - resetButtons(); - } - - function extractErrors(message) { - if ($.isPlainObject(message) && message.errors) { - return message.errors; - } else { - return { - base: (message.error || JSON.stringify(message)) - }; - } - } - - function displayErrors(errors) { - for (var field in errors) { - var error = errors[field]; - if (field === 'base') { - alert_message(error); - } else { - displayFieldError(field, error); - } - } - } - - function displayFieldError(field, error) { - var element = $('form input[name$="[' + field + ']"]'); - if (element) { - element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false); - } - }; - - // - // INIT - // - - $(document).ready(function() { - $('form').submit(markAsSubmitted); - $('#new_user').submit(prevent_default); - $('#new_user').submit(srp.signup); - $('#new_session').submit(prevent_default); - $('#new_session').submit(srp.login); - $('#update_login_and_password').submit(prevent_default); - $('#update_login_and_password').submit(srp.update); - $('#update_pgp_key').submit(prevent_default); - $('#update_pgp_key').submit(update_user); - return $('#user-typeahead').typeahead({ - source: poll_users - }); - }); - -}).call(this); diff --git a/users/app/assets/stylesheets/leap_web_users/.gitkeep b/users/app/assets/stylesheets/leap_web_users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/controllers/.gitkeep b/users/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/controllers/account_settings_controller.rb b/users/app/controllers/account_settings_controller.rb deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb deleted file mode 100644 index 03d3989..0000000 --- a/users/app/controllers/controller_extension/authentication.rb +++ /dev/null @@ -1,75 +0,0 @@ -module ControllerExtension::Authentication - extend ActiveSupport::Concern - - private - - included do - helper_method :current_user, :logged_in?, :admin? - end - - def current_user - @current_user ||= token_authenticate || warden.user - end - - def logged_in? - !!current_user - end - - def require_login - access_denied unless logged_in? - end - - # some actions only make sense if you are not logged in yet. - # (login, signup). If a user tries to perform these they will - # be redirected to their dashboard. - def redirect_if_logged_in - redirect_to home_url if logged_in? - end - - def access_denied - respond_to do |format| - format.html do - if logged_in? - redirect_to home_url, :alert => t(:not_authorized) - else - redirect_to login_url, :alert => t(:not_authorized_login) - end - end - format.json do - render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity - end - end - end - - def admin? - current_user && current_user.is_admin? - end - - def require_admin - access_denied unless admin? - end - - def authentication_errors - return unless attempted_login? - errors = get_warden_errors - errors.inject({}) do |translated,err| - translated[err.first] = I18n.t(err.last) - translated - end - end - - def get_warden_errors - if strategy = warden.winning_strategy - message = strategy.message - # in case we get back the default message to fail! - message.respond_to?(:inject) ? message : { base: message } - else - { login: :all_strategies_failed } - end - end - - def attempted_login? - request.env['warden.options'] && - request.env['warden.options'][:attempted_path] - end -end diff --git a/users/app/controllers/controller_extension/token_authentication.rb b/users/app/controllers/controller_extension/token_authentication.rb deleted file mode 100644 index 6e0a6ce..0000000 --- a/users/app/controllers/controller_extension/token_authentication.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ControllerExtension::TokenAuthentication - extend ActiveSupport::Concern - - def token - @token ||= authenticate_with_http_token do |token_id, options| - Token.find(token_id) - end - end - - def token_authenticate - @token_authenticated ||= token.authenticate if token - end - - def require_token - access_denied unless token_authenticate - end - - def logout - super - clear_token - end - - def clear_token - token.destroy if token - end -end - diff --git a/users/app/controllers/keys_controller.rb b/users/app/controllers/keys_controller.rb deleted file mode 100644 index fb28901..0000000 --- a/users/app/controllers/keys_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -class KeysController < ApplicationController - - # - # Render the user's key as plain text, without a layout. - # - # We will show blank page if user doesn't have key (which shouldn't generally occur) - # and a 404 error if user doesn't exist - # - def show - user = User.find_by_login(params[:login]) - if user - render text: user.public_key, content_type: 'text/text' - else - raise ActionController::RoutingError.new('Not Found') - end - end - -end diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb deleted file mode 100644 index 8919a4d..0000000 --- a/users/app/controllers/sessions_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -class SessionsController < ApplicationController - - before_filter :redirect_if_logged_in, :only => [:new] - - def new - @session = Session.new - if authentication_errors - @errors = authentication_errors - render :status => 422 - end - end - - def destroy - logout - redirect_to home_url - end - - # - # this is a bad hack, but user_url(user) is not available - # also, this doesn't work because the redirect happens as a PUT. no idea why. - # - #Warden::Manager.after_authentication do |user, auth, opts| - # response = Rack::Response.new - # response.redirect "/users/#{user.id}" - # throw :warden, response.finish - #end - -end diff --git a/users/app/controllers/users_base_controller.rb b/users/app/controllers/users_base_controller.rb deleted file mode 100644 index 9becf0d..0000000 --- a/users/app/controllers/users_base_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# -# common base class for all user related controllers -# - -class UsersBaseController < ApplicationController - - protected - - def fetch_user - @user = User.find(params[:user_id] || params[:id]) - if !@user && admin? - redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') - elsif !admin? && @user != current_user - access_denied - end - end - -end diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb deleted file mode 100644 index c8e09b6..0000000 --- a/users/app/controllers/users_controller.rb +++ /dev/null @@ -1,69 +0,0 @@ -# -# This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb -# - -class UsersController < UsersBaseController - - before_filter :require_login, :except => [:new] - before_filter :redirect_if_logged_in, :only => [:new] - before_filter :require_admin, :only => [:index, :deactivate, :enable] - before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable] - - respond_to :html - - def index - if params[:query] - if @user = User.find_by_login(params[:query]) - redirect_to @user - return - else - @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) - end - else - @users = User.by_created_at.descending - end - @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]) - respond_with @user - end - - def deactivate - @user.enabled = false - @user.save - respond_with @user - end - - def enable - @user.enabled = true - @user.save - respond_with @user - end - - def destroy - @user.account.destroy - flash[:notice] = I18n.t(:account_destroyed) - # admins can destroy other users - if @user != current_user - redirect_to users_url - else - # let's remove the invalid session - logout - redirect_to bye_url - end - end - -end diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb deleted file mode 100644 index f71d0f1..0000000 --- a/users/app/controllers/v1/messages_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -module V1 - class MessagesController < ApplicationController - - skip_before_filter :verify_authenticity_token - before_filter :require_token - - respond_to :json - - def index - render json: (current_user ? current_user.messages : [] ) - end - - def update - message = Message.find(params[:id]) - if (message and current_user) - message.mark_as_read_by(current_user) - message.save - render json: true - else - render json: false - end - end - - end -end diff --git a/users/app/controllers/v1/sessions_controller.rb b/users/app/controllers/v1/sessions_controller.rb deleted file mode 100644 index eae3a1e..0000000 --- a/users/app/controllers/v1/sessions_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -module V1 - class SessionsController < ApplicationController - - skip_before_filter :verify_authenticity_token - before_filter :require_token, only: :destroy - - def new - @session = Session.new - if authentication_errors - @errors = authentication_errors - render :status => 422 - end - end - - def create - logout if logged_in? - if params['A'] - authenticate! - else - @user = User.find_by_login(params['login']) - render :json => {salt: @user.salt} - end - end - - def update - authenticate! - @token = Token.create(:user_id => current_user.id) - session[:token] = @token.id - render :json => login_response - end - - def destroy - logout - head :no_content - end - - protected - - def login_response - handshake = session.delete(:handshake) || {} - handshake.to_hash.merge(:id => current_user.id, :token => @token.id) - end - - end -end diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb deleted file mode 100644 index 8897d01..0000000 --- a/users/app/controllers/v1/users_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -module V1 - class UsersController < UsersBaseController - - skip_before_filter :verify_authenticity_token - before_filter :fetch_user, :only => [:update] - before_filter :require_admin, :only => [:index] - before_filter :require_token, :only => [:update] - - respond_to :json - - # used for autocomplete for admins in the web ui - def index - if params[:query] - @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) - respond_with @users.map(&:login).sort - else - render :json => {'error' => 'query required', 'status' => :unprocessable_entity} - end - end - - def create - @user = Account.create(params[:user]) - respond_with @user # return ID instead? - end - - def update - @user.account.update params[:user] - respond_with @user - end - - end -end diff --git a/users/app/controllers/webfinger_controller.rb b/users/app/controllers/webfinger_controller.rb deleted file mode 100644 index 8872802..0000000 --- a/users/app/controllers/webfinger_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -class WebfingerController < ApplicationController - - respond_to :xml, :json - layout false - - def host_meta - @host_meta = Webfinger::HostMetaPresenter.new(request) - respond_with @host_meta - end - - def search - username = params[:q].split('@')[0].to_s.downcase - user = User.find_by_login(username) - raise RECORD_NOT_FOUND, 'User not found' unless user.present? - @presenter = Webfinger::UserPresenter.new(user, request) - respond_with @presenter - end - -end diff --git a/users/app/designs/message/by_user_ids_to_show.js b/users/app/designs/message/by_user_ids_to_show.js deleted file mode 100644 index e33566b..0000000 --- a/users/app/designs/message/by_user_ids_to_show.js +++ /dev/null @@ -1,7 +0,0 @@ -function (doc) { - if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { - doc.user_ids_to_show.forEach(function (userId) { - emit(userId, 1); - }); - } -} diff --git a/users/app/designs/message/by_user_ids_to_show_and_created_at.js b/users/app/designs/message/by_user_ids_to_show_and_created_at.js deleted file mode 100644 index 54e4604..0000000 --- a/users/app/designs/message/by_user_ids_to_show_and_created_at.js +++ /dev/null @@ -1,9 +0,0 @@ -// not using at moment -// call with something like Message.by_user_ids_to_show_and_created_at.startkey([user_id, start_date]).endkey([user_id,end_date]) -function (doc) { - if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { - doc.user_ids_to_show.forEach(function (userId) { - emit([userId, doc.created_at], 1); - }); - } -} diff --git a/users/app/designs/user/by_created_at_and_one_month_warning_not_sent.js b/users/app/designs/user/by_created_at_and_one_month_warning_not_sent.js deleted file mode 100644 index 53a95de..0000000 --- a/users/app/designs/user/by_created_at_and_one_month_warning_not_sent.js +++ /dev/null @@ -1,5 +0,0 @@ -function (doc) { - if ((doc['type'] == 'User') && (doc['created_at'] != null) && (doc['one_month_warning_sent'] == null)) { - emit(doc['created_at'], 1); - } -} diff --git a/users/app/helpers/.gitkeep b/users/app/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/helpers/email_aliases_helper.rb b/users/app/helpers/email_aliases_helper.rb deleted file mode 100644 index b56b068..0000000 --- a/users/app/helpers/email_aliases_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module EmailAliasesHelper - - def email_alias_form(options = {}) - simple_form_for [@user, EmailAlias.new()], - :html => {:class => "form-horizontal email-alias form"}, - :validate => true do |f| - yield f - end - end - -end diff --git a/users/app/helpers/sessions_helper.rb b/users/app/helpers/sessions_helper.rb deleted file mode 100644 index 309f8b2..0000000 --- a/users/app/helpers/sessions_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module SessionsHelper -end diff --git a/users/app/helpers/users_helper.rb b/users/app/helpers/users_helper.rb deleted file mode 100644 index f56faab..0000000 --- a/users/app/helpers/users_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -module UsersHelper - - def user_form_class(*classes) - (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') - end - - def wrapped(item, options = {}) - options[:as] ||= :div - content_tag options[:as], :class => dom_class(item), :id => dom_id(item) do - yield - end - end - -end diff --git a/users/app/mailers/.gitkeep b/users/app/mailers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/models/.gitkeep b/users/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/models/account.rb b/users/app/models/account.rb deleted file mode 100644 index cf998e4..0000000 --- a/users/app/models/account.rb +++ /dev/null @@ -1,68 +0,0 @@ -# -# The Account model takes care of the livecycle of a user. -# It composes a User record and it's identity records. -# It also allows for other engines to hook into the livecycle by -# monkeypatching the create, update and destroy methods. -# There's an ActiveSupport load_hook at the end of this file to -# make this more easy. -# -class Account - - attr_reader :user - - def initialize(user = nil) - @user = user - end - - # Returns the user record so it can be used in views. - def self.create(attrs) - @user = User.create(attrs).tap do |user| - Identity.create_for user - end - end - - def update(attrs) - if attrs[:password_verifier].present? - update_login(attrs[:login]) - @user.update_attributes attrs.slice(:password_verifier, :password_salt) - end - # TODO: move into identity controller - key = update_pgp_key(attrs[:public_key]) - @user.errors.set :public_key, key.errors.full_messages - @user.save && save_identities - @user.refresh_identity - end - - def destroy - return unless @user - Identity.disable_all_for(@user) - @user.destroy - end - - protected - - def update_login(login) - return unless login.present? - @old_identity = Identity.for(@user) - @user.login = login - @new_identity = Identity.for(@user) # based on the new login - @old_identity.destination = @user.email_address # alias old -> new - end - - def update_pgp_key(key) - PgpKey.new(key).tap do |key| - if key.present? && key.valid? - @new_identity ||= Identity.for(@user) - @new_identity.set_key(:pgp, key) - end - end - end - - def save_identities - @new_identity.try(:save) && @old_identity.try(:save) - end - - # You can hook into the account lifecycle from different engines using - # ActiveSupport.on_load(:account) do ... - ActiveSupport.run_load_hooks(:account, self) -end diff --git a/users/app/models/email.rb b/users/app/models/email.rb deleted file mode 100644 index a9a503f..0000000 --- a/users/app/models/email.rb +++ /dev/null @@ -1,26 +0,0 @@ -class Email < String - include ActiveModel::Validations - - validates :email, - :format => { - :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, #local part of email is case-sensitive, so allow uppercase letter. - :message => "needs to be a valid email address" - } - - def to_partial_path - "emails/email" - end - - def to_param - to_s - end - - def email - self - end - - def handle - self.split('@').first - end - -end diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb deleted file mode 100644 index 9b97b51..0000000 --- a/users/app/models/identity.rb +++ /dev/null @@ -1,136 +0,0 @@ -class Identity < CouchRest::Model::Base - include LoginFormatValidation - - use_database :identities - - belongs_to :user - - property :address, LocalEmail - property :destination, Email - property :keys, HashWithIndifferentAccess - - validate :unique_forward - validate :alias_available - validate :address_local_email - validate :destination_email - - design do - view :by_user_id - view :by_address_and_destination - view :by_address - view :pgp_key_by_email, - map: <<-EOJS - function(doc) { - if (doc.type != 'Identity') { - return; - } - if (typeof doc.keys === "object") { - emit(doc.address, doc.keys["pgp"]); - } - } - EOJS - view :disabled, - map: <<-EOJS - function(doc) { - if (doc.type != 'Identity') { - return; - } - if (typeof doc.user_id === "undefined") { - emit(doc._id, 1); - } - } - EOJS - - end - - def self.for(user, attributes = {}) - find_for(user, attributes) || build_for(user, attributes) - end - - def self.find_for(user, attributes = {}) - attributes.reverse_merge! attributes_from_user(user) - find_by_address_and_destination [attributes[:address], attributes[:destination]] - end - - def self.build_for(user, attributes = {}) - attributes.reverse_merge! attributes_from_user(user) - Identity.new(attributes) - end - - def self.create_for(user, attributes = {}) - identity = build_for(user, attributes) - identity.save - identity - end - - def self.disable_all_for(user) - Identity.by_user_id.key(user.id).each do |identity| - identity.disable - identity.save - end - end - - def self.destroy_all_disabled - Identity.disabled.each do |identity| - identity.destroy - end - end - - def self.attributes_from_user(user) - { user_id: user.id, - address: user.email_address, - destination: user.email_address - } - end - - def enabled? - self.destination && self.user_id - end - - def disable - self.destination = nil - self.user_id = nil - end - - def keys - read_attribute('keys') || HashWithIndifferentAccess.new - end - - def set_key(type, key) - return if keys[type] == key.to_s - write_attribute('keys', keys.merge(type => key.to_s)) - end - - # for LoginFormatValidation - def login - self.address.handle - end - - protected - - def unique_forward - same = Identity.find_by_address_and_destination([address, destination]) - if same && same != self - errors.add :base, "This alias already exists" - end - end - - def alias_available - same = Identity.find_by_address(address) - if same && same.user != self.user - errors.add :base, "This email has already been taken" - end - end - - def address_local_email - return if address.valid? #this ensures it is LocalEmail - self.errors.add(:address, address.errors.messages[:email].first) #assumes only one error - end - - def destination_email - return if destination.nil? # this identity is disabled - return if destination.valid? # this ensures it is Email - self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO - end - -end diff --git a/users/app/models/local_email.rb b/users/app/models/local_email.rb deleted file mode 100644 index 2b4c65e..0000000 --- a/users/app/models/local_email.rb +++ /dev/null @@ -1,68 +0,0 @@ -class LocalEmail < Email - - BLACKLIST_FROM_RFC2142 = [ - 'postmaster', 'hostmaster', 'domainadmin', 'webmaster', 'www', - 'abuse', 'noc', 'security', 'usenet', 'news', 'uucp', - 'ftp', 'sales', 'marketing', 'support', 'info' - ] - - def self.domain - APP_CONFIG[:domain] - end - - validates :email, - :format => { - :with => /@#{domain}\Z/i, - :message => "needs to end in @#{domain}" - } - - validate :handle_allowed - - def initialize(s) - super - append_domain_if_needed - end - - def to_key - [handle] - end - - def domain - LocalEmail.domain - end - - protected - - def append_domain_if_needed - unless self.index('@') - self << '@' + domain - end - end - - def handle_allowed - errors.add(:handle, "is reserved.") if handle_reserved? - end - - def handle_reserved? - # *ARRAY in a case statement tests if ARRAY includes the handle. - case handle - when *APP_CONFIG[:handle_blacklist] - true - when *APP_CONFIG[:handle_whitelist] - false - when *BLACKLIST_FROM_RFC2142 - true - else - handle_in_passwd? - end - end - - def handle_in_passwd? - begin - !!Etc.getpwnam(handle) - rescue ArgumentError - # handle was not found - return false - end - end -end diff --git a/users/app/models/login_format_validation.rb b/users/app/models/login_format_validation.rb deleted file mode 100644 index c1fcf70..0000000 --- a/users/app/models/login_format_validation.rb +++ /dev/null @@ -1,21 +0,0 @@ -module LoginFormatValidation - extend ActiveSupport::Concern - - #TODO: Probably will replace this. Playing with using it for aliases too, but won't want it connected to login field. - - included do - # Have multiple regular expression validations so we can get specific error messages: - validates :login, - :format => { :with => /\A.{2,}\z/, - :message => "Must have at least two characters"} - validates :login, - :format => { :with => /\A[a-z\d_\.-]+\z/, - :message => "Only lowercase letters, digits, . - and _ allowed."} - validates :login, - :format => { :with => /\A[a-z].*\z/, - :message => "Must begin with a lowercase letter"} - validates :login, - :format => { :with => /\A.*[a-z\d]\z/, - :message => "Must end with a letter or digit"} - end -end diff --git a/users/app/models/message.rb b/users/app/models/message.rb deleted file mode 100644 index 424f094..0000000 --- a/users/app/models/message.rb +++ /dev/null @@ -1,29 +0,0 @@ -class Message < CouchRest::Model::Base - - use_database :messages - - property :text, String - property :user_ids_to_show, [String] - property :user_ids_have_shown, [String] # is this necessary to store? - - timestamps! - - design do - own_path = Pathname.new(File.dirname(__FILE__)) - load_views(own_path.join('..', 'designs', 'message')) - end - - def mark_as_read_by(user) - user_ids_to_show.delete(user.id) - # is it necessary to keep track of what users have already seen it? - user_ids_have_shown << user.id unless read_by?(user) - end - - def read_by?(user) - user_ids_have_shown.include?(user.id) - end - - def unread_by?(user) - user_ids_to_show.include?(user.id) - end -end diff --git a/users/app/models/pgp_key.rb b/users/app/models/pgp_key.rb deleted file mode 100644 index 66f8660..0000000 --- a/users/app/models/pgp_key.rb +++ /dev/null @@ -1,48 +0,0 @@ -class PgpKey - include ActiveModel::Validations - - KEYBLOCK_IDENTIFIERS = [ - '-----BEGIN PGP PUBLIC KEY BLOCK-----', - '-----END PGP PUBLIC KEY BLOCK-----', - ] - - # mostly for testing. - attr_accessor :keyblock - - validate :validate_keyblock_format - - def initialize(keyblock = nil) - @keyblock = keyblock - end - - def to_s - @keyblock - end - - def present? - @keyblock.present? - end - - # allow comparison with plain keyblock strings. - def ==(other) - self.equal?(other) or - # relax the comparison on line ends. - self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '') - end - - protected - - def validate_keyblock_format - if keyblock_identifier_missing? - errors.add :public_key_block, - "does not look like an armored pgp public key block" - end - end - - def keyblock_identifier_missing? - KEYBLOCK_IDENTIFIERS.find do |identify| - !@keyblock.include?(identify) - end - end - -end diff --git a/users/app/models/service_level.rb b/users/app/models/service_level.rb deleted file mode 100644 index 299aaf1..0000000 --- a/users/app/models/service_level.rb +++ /dev/null @@ -1,19 +0,0 @@ -class ServiceLevel - - def initialize(attributes = {}) - @id = attributes[:id] || APP_CONFIG[:default_service_level] - end - - def self.authenticated_select_options - APP_CONFIG[:service_levels].map { |id,config_hash| [config_hash[:description], id] if config_hash[:name] != 'anonymous'}.compact - end - - def id - @id - end - - def config_hash - APP_CONFIG[:service_levels][@id] - end - -end diff --git a/users/app/models/session.rb b/users/app/models/session.rb deleted file mode 100644 index 0d7e10e..0000000 --- a/users/app/models/session.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Session < SRP::Session - include ActiveModel::Validations - include LoginFormatValidation - - attr_accessor :login - - validates :login, :presence => true - - def initialize(user = nil, aa = nil) - super(user, aa) if user - end - - def persisted? - false - end - - def new_record? - true - end - - def to_model - self - end - - def to_key - [object_id] - end - - def to_param - nil - end -end diff --git a/users/app/models/token.rb b/users/app/models/token.rb deleted file mode 100644 index 4856c31..0000000 --- a/users/app/models/token.rb +++ /dev/null @@ -1,69 +0,0 @@ -class Token < CouchRest::Model::Base - - use_database :tokens - - belongs_to :user - - # timestamps! does not create setters and only sets updated_at - # if the object has changed and been saved. Instead of triggering - # that we rather use our own property we have control over: - property :last_seen_at, Time, accessible: false - - validates :user_id, presence: true - - design do - view :by_last_seen_at - end - - def self.expires_after - APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] - end - - def self.expired - return [] unless expires_after - by_last_seen_at.endkey(expires_after.minutes.ago) - end - - def self.destroy_all_expired - self.expired.each do |token| - token.destroy - end - end - - def authenticate - if expired? - destroy - return nil - else - touch - return user - end - end - - # Tokens can be cleaned up in different ways. - # So let's make sure we don't crash if they disappeared - def destroy_with_rescue - destroy_without_rescue - rescue RestClient::ResourceNotFound - end - alias_method_chain :destroy, :rescue - - def touch - self.last_seen_at = Time.now - save - end - - def expired? - Token.expires_after and - last_seen_at < Token.expires_after.minutes.ago - end - - def initialize(*args) - super - if new_record? - self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') - self.last_seen_at = Time.now - end - end -end - diff --git a/users/app/models/unauthenticated_user.rb b/users/app/models/unauthenticated_user.rb deleted file mode 100644 index 0fc17d2..0000000 --- a/users/app/models/unauthenticated_user.rb +++ /dev/null @@ -1,6 +0,0 @@ -# The nil object for the user class -class UnauthenticatedUser < Object - - # will probably want something here to return service level as APP_CONFIG[:service_levels][0] but not sure how will be accessing. - -end diff --git a/users/app/models/user.rb b/users/app/models/user.rb deleted file mode 100644 index c297ac8..0000000 --- a/users/app/models/user.rb +++ /dev/null @@ -1,179 +0,0 @@ -class User < CouchRest::Model::Base - include LoginFormatValidation - - use_database :users - - property :login, String, :accessible => true - property :password_verifier, String, :accessible => true - property :password_salt, String, :accessible => true - - property :enabled, TrueClass, :default => true - - # these will be null by default but we shouldn't ever pull them directly, but only via the methods that will return the full ServiceLevel - property :desired_service_level_code, Integer, :accessible => true - property :effective_service_level_code, Integer, :accessible => true - - property :one_month_warning_sent, TrueClass - - before_save :update_effective_service_level - - validates :login, :password_salt, :password_verifier, - :presence => true - - validates :login, - :uniqueness => true, - :if => :serverside? - - validate :login_is_unique_alias - - validates :password_salt, :password_verifier, - :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } - - validates :password, :presence => true, - :confirmation => true, - :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } - - timestamps! - - design do - own_path = Pathname.new(File.dirname(__FILE__)) - load_views(own_path.join('..', 'designs', 'user')) - view :by_login - view :by_created_at - end # end of design - - def to_json(options={}) - { - :login => login, - :ok => valid? - }.to_json(options) - end - - def salt - password_salt.hex - end - - def verifier - password_verifier.hex - end - - def username - login - end - - def email_address - LocalEmail.new(login) - end - - # Since we are storing admins by login, we cannot allow admins to change their login. - def is_admin? - APP_CONFIG['admins'].include? self.login - end - - def most_recent_tickets(count=3) - Ticket.for_user(self).limit(count).all #defaults to having most recent updated first - end - - def messages(unseen = true) - #TODO for now this only shows unseen messages. Will we ever want seen ones? Is it necessary to store? - - # we don't want to emit all the userids associated with a message, so only emit id and text. - Message.by_user_ids_to_show.key(self.id).map { |message| [message.id, message.text] } - - end - - # DEPRECATED - # - # Please set the key on the identity directly - # WARNING: This will not be serialized with the user record! - # It is only a workaround for the key form. - def public_key=(value) - identity.set_key(:pgp, value) - end - - # DEPRECATED - # - # Please access identity.keys[:pgp] directly - def public_key - identity.keys[:pgp] - end - - def account - Account.new(self) - end - - def identity - @identity ||= Identity.for(self) - end - - def refresh_identity - @identity = Identity.for(self) - end - - def desired_service_level - code = self.desired_service_level_code || APP_CONFIG[:default_service_level] - ServiceLevel.new({id: code}) - end - - def effective_service_level - code = self.effective_service_level_code || self.desired_service_level.id - ServiceLevel.new({id: code}) - end - - - def self.send_one_month_warnings - - # To determine warnings to send, need to get all users where one_month_warning_sent is not set, and where it was created greater than or equal to 1 month ago. - # TODO: might want to further limit to enabled accounts, and, based on provider's service level configuration, for particular service levels. - users_to_warn = User.by_created_at_and_one_month_warning_not_sent.endkey(Time.now-1.month) - - users_to_warn.each do |user| - # instead of loop could use something like: - # message.user_ids_to_show = users_to_warn.map(&:id) - # but would still need to loop through users to store one_month_warning_sent - - if !@message - # create a message for today's date - # only want to create once, and only if it will be used. - @message = Message.new(:text => I18n.t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) - end - - @message.user_ids_to_show << user.id - user.one_month_warning_sent = true - user.save - end - @message.save if @message - - end - - protected - - ## - # Validation Functions - ## - - def login_is_unique_alias - alias_identity = Identity.find_by_address(self.email_address) - return if alias_identity.blank? - if alias_identity.user != self - errors.add(:login, "has already been taken") - end - end - - def password - password_verifier - end - - # used as a condition for validations that are server side only - def serverside? - true - end - - def update_effective_service_level - # TODO: Is this always the case? Might there be a situation where the admin has set the effective service level and we don't want it changed to match the desired one? - if self.desired_service_level_code_changed? - self.effective_service_level_code = self.desired_service_level_code - end - end - -end diff --git a/users/app/views/.gitkeep b/users/app/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/app/views/emails/_email.html.haml b/users/app/views/emails/_email.html.haml deleted file mode 100644 index ea59cec..0000000 --- a/users/app/views/emails/_email.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -= wrapped(email, local_assigns) do - = email - - if local_assigns[:with].try(:include?, :delete) - = link_to(user_email_alias_path(@user, email), :method => :delete) do - %i.icon-remove - diff --git a/users/app/views/sessions/new.html.haml b/users/app/views/sessions/new.html.haml deleted file mode 100644 index 771dc97..0000000 --- a/users/app/views/sessions/new.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -.span1 -.span9 - = render :partial => 'users/warnings' - %h2=t :login - = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } 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 } - .form-actions - = f.button :submit, :value => t(:login), :class => 'btn-primary' - = link_to t(:cancel), home_path, :class => 'btn' diff --git a/users/app/views/sessions/new.json.erb b/users/app/views/sessions/new.json.erb deleted file mode 100644 index 36154b8..0000000 --- a/users/app/views/sessions/new.json.erb +++ /dev/null @@ -1,3 +0,0 @@ -{ -"errors": <%= raw @errors.to_json %> -} diff --git a/users/app/views/users/_change_password.html.haml b/users/app/views/users/_change_password.html.haml deleted file mode 100644 index 425e3ee..0000000 --- a/users/app/views/users/_change_password.html.haml +++ /dev/null @@ -1,21 +0,0 @@ --# --# CHANGE PASSWORD --# --# * everything about this form is handled with javascript. So take care when changing any ids. --# * the login is required when changing the password because it is used as part of the salt when calculating the password verifier. --# however, we don't want the user to change their login without generating a new key, so we hide the ui for this --# (although it works perfectly fine to change username if the field was visible). --# - -- form_options = {:url => '/not-used', :html => {:class => user_form_class('form-horizontal'), :id => 'update_login_and_password', :data => {token: session[:token]}}, :validate => true} -= simple_form_for @user, form_options do |f| - %legend= t(:change_password) - = hidden_field_tag 'user_param', @user.to_param - .hidden - = 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 - .controls - = f.submit t(:save), :class => 'btn btn-primary' - diff --git a/users/app/views/users/_change_pgp_key.html.haml b/users/app/views/users/_change_pgp_key.html.haml deleted file mode 100644 index e465125..0000000 --- a/users/app/views/users/_change_pgp_key.html.haml +++ /dev/null @@ -1,13 +0,0 @@ --# --# CHANGE PGP KEY --# --# this will be replaced by a identities controller/view at some point --# - -- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_pgp_key', :data => {token: session[:token]}}, :validate => true} -= 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 - .controls - = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/users/app/views/users/_change_service_level.html.haml b/users/app/views/users/_change_service_level.html.haml deleted file mode 100644 index 61e67d9..0000000 --- a/users/app/views/users/_change_service_level.html.haml +++ /dev/null @@ -1,18 +0,0 @@ --# 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] - - 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) - - if @user != current_user - = t(:desired_service_level) - = f.select :desired_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.desired_service_level.id - - if @user != current_user - %p - = t(:effective_service_level) - = f.select :effective_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.effective_service_level.id - .control-group - .controls - = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/users/app/views/users/_destroy_account.html.haml b/users/app/views/users/_destroy_account.html.haml deleted file mode 100644 index 445f3c4..0000000 --- a/users/app/views/users/_destroy_account.html.haml +++ /dev/null @@ -1,27 +0,0 @@ --# --# DESTROY ACCOUNT --# - -%legend - - if @user == current_user - = t(:destroy_my_account) - - else - = t(:admin_destroy_account, :username => @user.login) -%p= t(:destroy_account_info) -= link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do - %i.icon-remove.icon-white - = t(:destroy_my_account) -- if @user != current_user and @user.enabled? - %legend - = t(:deactivate_account, :username => @user.login) - %p= t(:deactivate_description) - = link_to deactivate_user_path(@user), :method => :post, :class => "btn btn-warning" do - %i.icon-pause.icon-white - = t(:deactivate) -- elsif @user != current_user and !@user.enabled? - %legend - = t(:enable_account, :username => @user.login) - %p= t(:enable_description) - = link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do - %i.icon-ok.icon-white - = t(:enable) diff --git a/users/app/views/users/_edit.html.haml b/users/app/views/users/_edit.html.haml deleted file mode 100644 index 1d2b68a..0000000 --- a/users/app/views/users/_edit.html.haml +++ /dev/null @@ -1,14 +0,0 @@ --# --# edit user form, used by both show and edit actions. --# --# We render a bunch of forms here. Which we use depends upon config settings --# user_actions and admin_actions. They both include an array of actions --# allowed to users and admins. --# Possible forms are: --# 'change_password' --# 'change_pgp_key' --# 'change_service_level' --# 'destroy_account' -- actions = APP_CONFIG[admin? ? :admin_actions : :user_actions] || [] -- actions.each do |action| - = render action diff --git a/users/app/views/users/_user.html.haml b/users/app/views/users/_user.html.haml deleted file mode 100644 index 583d22f..0000000 --- a/users/app/views/users/_user.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%tr - %td= link_to user.login, user - %td= l(user.created_at, :format => :short) - %td= l(user.updated_at, :format => :short) diff --git a/users/app/views/users/_warnings.html.haml b/users/app/views/users/_warnings.html.haml deleted file mode 100644 index 79ab103..0000000 --- a/users/app/views/users/_warnings.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%noscript - %div.alert.alert-error=t :js_required_html -#cookie_warning.alert.alert-error{:style => "display:none"} - =t :cookie_disabled_warning -:javascript - document.cookie = "testing=cookies_enabled; path=/"; - if(document.cookie.indexOf("testing=cookies_enabled") < 0) - { - document.getElementById('cookie_warning').style.display = 'block'; - } else { - document.getElementById('cookie_warning').style.display = 'none'; - } \ No newline at end of file diff --git a/users/app/views/users/edit.html.haml b/users/app/views/users/edit.html.haml deleted file mode 100644 index 434c025..0000000 --- a/users/app/views/users/edit.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'edit' diff --git a/users/app/views/users/index.html.haml b/users/app/views/users/index.html.haml deleted file mode 100644 index fc1001e..0000000 --- a/users/app/views/users/index.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- @show_navigation = false - -= form_tag users_path, :method => :get, :class => "form-search" do - .input-append - = text_field_tag :query, params[:query], :id => 'user-typeahead', :class => "search-query", :autocomplete => :off - %button.btn{:type => :submit}= t(:search) - -%table.table.table-striped - %tr - %th= t(:username) - %th= t(:created) - %th= t(:updated) - = render @users.all diff --git a/users/app/views/users/new.html.haml b/users/app/views/users/new.html.haml deleted file mode 100644 index aecf831..0000000 --- a/users/app/views/users/new.html.haml +++ /dev/null @@ -1,19 +0,0 @@ --# --# This form is handled entirely by javascript, so take care when changing element ids. --# - -- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} - -.span1 -.span9 - = render :partial => 'warnings' - %h2=t :signup - = simple_form_for(@user, form_options) do |f| - %legend= t(:signup_message) - = 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, :validate => true, :input_html => { :id => :srp_password_confirmation } - .form-actions - = f.button :submit, :value => t(:signup), :class => 'btn btn-primary' - = link_to t(:cancel), home_path, :class => 'btn' - diff --git a/users/app/views/users/show.html.haml b/users/app/views/users/show.html.haml deleted file mode 100644 index c587017..0000000 --- a/users/app/views/users/show.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -.overview - - %h2.first= t(:overview_welcome, :username => @user.login) - - - if admin? - %p - = t(:created) - = @user.created_at - %br - = t(:updated) - = @user.updated_at - %br - = t(:enabled) - = @user.enabled? - - %p= t(:overview_intro) - - %ul.unstyled - %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user)) - - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly} - %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user)) - %li= icon('shopping-cart') + link_to(t(:overview_billing), billing_top_link(@user)) if APP_CONFIG[:billing] - .container-fluid - .row-fluid - = home_page_buttons(true) \ No newline at end of file diff --git a/users/app/views/v1/sessions/new.json.erb b/users/app/views/v1/sessions/new.json.erb deleted file mode 100644 index 36154b8..0000000 --- a/users/app/views/v1/sessions/new.json.erb +++ /dev/null @@ -1,3 +0,0 @@ -{ -"errors": <%= raw @errors.to_json %> -} diff --git a/users/app/views/webfinger/host_meta.xml.erb b/users/app/views/webfinger/host_meta.xml.erb deleted file mode 100644 index cfcbcc0..0000000 --- a/users/app/views/webfinger/host_meta.xml.erb +++ /dev/null @@ -1,11 +0,0 @@ - - - - <%= @host_meta.subject %> - - <%- @host_meta.links.each do |rel, link| %> - - <%- end %> - diff --git a/users/app/views/webfinger/search.xml.erb b/users/app/views/webfinger/search.xml.erb deleted file mode 100644 index 7328552..0000000 --- a/users/app/views/webfinger/search.xml.erb +++ /dev/null @@ -1,7 +0,0 @@ - - - <%= @presenter.subject %> - <%- @presenter.links.each do |rel, link| %> - type=<%=link[:type]%> href="<%= link[:key] %>"/> - <% end %> - diff --git a/users/test/factories.rb b/users/test/factories.rb deleted file mode 100644 index ae00d43..0000000 --- a/users/test/factories.rb +++ /dev/null @@ -1,34 +0,0 @@ -FactoryGirl.define do - - factory :user do - login { Faker::Internet.user_name } - password_verifier "1234ABCD" - password_salt "4321AB" - - factory :user_with_settings do - email_forward { Faker::Internet.email } - email_aliases_attributes do - {:a => Faker::Internet.user_name + '@' + APP_CONFIG[:domain]} - end - end - - factory :admin_user do - after(:build) do |admin| - admin.stubs(:is_admin?).returns(true) - end - end - end - - factory :token do - user - end - - factory :pgp_key do - keyblock <<-EOPGP ------BEGIN PGP PUBLIC KEY BLOCK----- -+Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+ -#{SecureRandom.base64(4032)} ------END PGP PUBLIC KEY BLOCK----- - EOPGP - end -end diff --git a/users/test/fixtures/.gitkeep b/users/test/fixtures/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/test/functional/.gitkeep b/users/test/functional/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/test/functional/application_controller_test.rb b/users/test/functional/application_controller_test.rb deleted file mode 100644 index c4c922b..0000000 --- a/users/test/functional/application_controller_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'test_helper' - -class ApplicationControllerTest < ActionController::TestCase - - def setup - # so we can test the effect on the response - @controller.response = @response - end - - def test_require_login_redirect - @controller.send(:require_login) - assert_access_denied(true, false) - end - - def test_require_login - login - @controller.send(:require_login) - assert_access_denied(false) - end - - def test_require_admin - login - @current_user.expects(:is_admin?).returns(false) - @controller.send(:require_admin) - assert_access_denied - end - -end diff --git a/users/test/functional/helper_methods_test.rb b/users/test/functional/helper_methods_test.rb deleted file mode 100644 index 44226ae..0000000 --- a/users/test/functional/helper_methods_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -# -# Testing and documenting the helper methods available from -# ApplicationController -# - -require 'test_helper' - -class HelperMethodsTest < ActionController::TestCase - tests ApplicationController - - # we test them right in here... - include ApplicationController._helpers - - # the helpers all reference the controller. - def controller - @controller - end - - def test_current_user - login - assert_equal @current_user, current_user - end - - def test_logged_in - login - assert logged_in? - end - - def test_logged_out - assert !logged_in? - end - - def test_admin - login - @current_user.expects(:is_admin?).returns(bool = stub) - assert_equal bool, admin? - end - -end diff --git a/users/test/functional/keys_controller_test.rb b/users/test/functional/keys_controller_test.rb deleted file mode 100644 index 863be93..0000000 --- a/users/test/functional/keys_controller_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'test_helper' - -class KeysControllerTest < ActionController::TestCase - - test "get existing public key" do - public_key = 'my public key' - @user = stub_record :user, :public_key => public_key - User.stubs(:find_by_login).with(@user.login).returns(@user) - get :show, :login => @user.login - assert_response :success - assert_equal "text/text", response.content_type - assert_equal public_key, response.body - end - - test "get non-existing public key for user" do - # this isn't a scenerio that should generally occur. - @user = stub_record :user - User.stubs(:find_by_login).with(@user.login).returns(@user) - get :show, :login => @user.login - assert_response :success - assert_equal "text/text", response.content_type - assert_equal '', response.body.strip - end - - test "get public key for non-existing user" do - # raise 404 error if user doesn't exist (doesn't need to be this routing error, but seems fine to assume for now): - assert_raise(ActionController::RoutingError) { - get :show, :login => 'asdkljslksjfdlskfj' - } - end - -end diff --git a/users/test/functional/sessions_controller_test.rb b/users/test/functional/sessions_controller_test.rb deleted file mode 100644 index fe7903f..0000000 --- a/users/test/functional/sessions_controller_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -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 SessionsControllerTest < ActionController::TestCase - - setup do - @user = stub :login => "me", :id => 123 - @client_hex = 'a123' - end - - test "should get login screen" do - get :new - assert_response :success - assert_equal "text/html", response.content_type - assert_template "sessions/new" - end - - test "redirect to home_url if logged in" do - login - get :new - assert_response :redirect - assert_redirected_to home_url - end - - test "renders json" do - get :new, :format => :json - assert_response :success - assert_json_error nil - end - - test "renders warden errors" do - request.env['warden.options'] = {attempted_path: '/1/sessions/asdf.json'} - 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 - assert_response 422 - assert_json_error :field => "translation stub" - end - - test "renders failed attempt message" do - request.env['warden.options'] = {attempted_path: '/1/sessions/asdf.json'} - request.env['warden'].stubs(:winning_strategy).returns(nil) - get :new, :format => :json - assert_response 422 - assert_json_error :login => I18n.t(:all_strategies_failed) - end - - test "destory should logout" do - login - expect_logout - delete :destroy - assert_response :redirect - assert_redirected_to home_url - end - -end diff --git a/users/test/functional/test_helpers_test.rb b/users/test/functional/test_helpers_test.rb deleted file mode 100644 index 845e516..0000000 --- a/users/test/functional/test_helpers_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -# There are a few test helpers for dealing with login etc. -# We test them here and also document their behaviour. -# - -require 'test_helper' - -class TestHelpersTest < ActionController::TestCase - tests ApplicationController # testing no controller in particular - - def test_login_stubs_warden - login - assert_equal @current_user, request.env['warden'].user - end - - def test_login_token_authenticates - login - assert_equal @current_user, @controller.send(:token_authenticate) - end - - def test_login_stubs_token - login - assert @token - assert_equal @current_user, @token.authenticate - end - - def test_login_adds_token_header - login - token_present = @controller.authenticate_with_http_token do |token, options| - assert_equal @token.id, token - end - # authenticate_with_http_token just returns nil and does not - # execute the block if there is no token. So we have to also - # ensure it was run: - assert token_present - end -end - diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb deleted file mode 100644 index 0713836..0000000 --- a/users/test/functional/users_controller_test.rb +++ /dev/null @@ -1,165 +0,0 @@ -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 - assert_response :redirect - assert_redirected_to login_path - end - - test "user can see user" do - user = find_record :user, - :most_recent_tickets => [] - login user - get :show, :id => user.id - assert_response :success - end - - test "admin can see other user" do - user = find_record :user, - :most_recent_tickets => [] - login :is_admin? => true - get :show, :id => user.id - assert_response :success - - end - - test "user cannot see other user" do - user = find_record :user, - :most_recent_tickets => [] - login - get :show, :id => user.id - assert_response :redirect - assert_access_denied - end - - test "may not show non-existing user without auth" do - nonid = 'thisisnotanexistinguserid' - - get :show, :id => nonid - assert_access_denied(true, false) - end - - test "may not show non-existing user without admin" do - nonid = 'thisisnotanexistinguserid' - login - - get :show, :id => nonid - assert_access_denied - end - - test "redirect admin to user list for non-existing user" do - nonid = 'thisisnotanexistinguserid' - login :is_admin? => true - get :show, :id => nonid - assert_response :redirect - assert_equal({:alert => "No such user."}, flash.to_hash) - assert_redirected_to users_path - end - - test "should get edit view" do - user = find_record :user - - login user - get :edit, :id => user.id - - assert_equal user, assigns[:user] - end - - test "admin can destroy user" do - user = find_record :user - - # we destroy the user record and the associated data... - user.expects(:destroy) - Identity.expects(:disable_all_for).with(user) - Ticket.expects(:destroy_all_from).with(user) - - login :is_admin? => true - delete :destroy, :id => user.id - - assert_response :redirect - assert_redirected_to users_path - end - - test "user can cancel account" do - user = find_record :user - - # we destroy the user record and the associated data... - user.expects(:destroy) - Identity.expects(:disable_all_for).with(user) - Ticket.expects(:destroy_all_from).with(user) - - login user - expect_logout - delete :destroy, :id => @current_user.id - - assert_response :redirect - assert_redirected_to bye_url - end - - test "non-admin can't destroy user" do - user = find_record :user - - login - delete :destroy, :id => user.id - - assert_access_denied - end - - test "admin can list users" do - login :is_admin? => true - get :index - - assert_response :success - assert assigns(:users) - end - - test "non-admin can't list users" do - login - get :index - - assert_access_denied - end - - test "admin can search users" do - login :is_admin? => true - get :index, :query => "a" - - assert_response :success - assert assigns(:users) - end - - test "user cannot enable own account" do - user = find_record :user - login - post :enable, :id => user.id - assert_access_denied - end - - test "admin can deactivate user" do - user = find_record :user - assert user.enabled? - user.expects(:save).returns(true) - - login :is_admin? => true - - post :deactivate, :id => user.id - assert !assigns(:user).enabled? - end - -end diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb deleted file mode 100644 index 24a5b1f..0000000 --- a/users/test/functional/v1/messages_controller_test.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'test_helper' - -class V1::MessagesControllerTest < ActionController::TestCase - - setup do - @user = FactoryGirl.build(:user) - @user.save - @message = Message.new(:text => 'a test message') - @message.user_ids_to_show << @user.id - @message.save - end - - teardown do - @message.destroy - @user.destroy - end - - test "get messages for user" do - login @user - get :index - assert response.body.include? @message.text - assert response.body.include? @message.id - end - - test "mark message read for user" do - login @user - assert @message.user_ids_to_show.include?(@user.id) - assert !@message.user_ids_have_shown.include?(@user.id) - put :update, :id => @message.id - @message.reload - assert !@message.user_ids_to_show.include?(@user.id) - assert @message.user_ids_have_shown.include?(@user.id) - assert_json_response true - end - - test "do not get seen messages" do - login @user - put :update, :id => @message.id - @message.reload - get :index - assert !(response.body.include? @message.text) - assert !(response.body.include? @message.id) - end - - - test "mark read responds even with bad inputs" do - login @user - put :update, :id => 'more nonsense' - assert_json_response false - end - - test "fails if not authenticated" do - get :index, :format => :json - assert_access_denied - end - -end diff --git a/users/test/functional/v1/sessions_controller_test.rb b/users/test/functional/v1/sessions_controller_test.rb deleted file mode 100644 index df0d681..0000000 --- a/users/test/functional/v1/sessions_controller_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -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 - - setup do - @request.env['HTTP_HOST'] = 'api.lvh.me' - @user = stub_record :user, {}, true - @client_hex = 'a123' - end - - test "renders json" do - get :new, :format => :json - assert_response :success - assert_json_error nil - end - - test "renders warden errors" do - request.env['warden.options'] = {attempted_path: 'path/to/controller'} - 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 - assert_response 422 - assert_json_error :field => "translation stub" - end - - # Warden takes care of parsing the params and - # rendering the response. So not much to test here. - test "should perform handshake" do - 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 - end - - test "should authenticate" do - request.env['warden'].expects(:authenticate!) - @controller.stubs(:current_user).returns(@user) - handshake = stub(:to_hash => {h: "ash"}) - session[:handshake] = handshake - - post :update, :id => @user.login, :client_auth => @client_hex - - assert_nil session[:handshake] - assert_response :success - assert json_response.keys.include?("id") - assert json_response.keys.include?("token") - assert token = Token.find(json_response['token']) - assert_equal @user.id, token.user_id - end - - test "destroy should logout" do - login - expect_logout - delete :destroy - assert_response 204 - end - -end diff --git a/users/test/functional/v1/users_controller_test.rb b/users/test/functional/v1/users_controller_test.rb deleted file mode 100644 index 7cd9b0c..0000000 --- a/users/test/functional/v1/users_controller_test.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'test_helper' - -class V1::UsersControllerTest < ActionController::TestCase - - test "user can change settings" do - user = find_record :user - changed_attribs = record_attributes_for :user_with_settings - account_settings = stub - account_settings.expects(:update).with(changed_attribs) - Account.expects(:new).with(user).returns(account_settings) - - login user - put :update, :user => changed_attribs, :id => user.id, :format => :json - - assert_equal user, assigns[:user] - assert_response 204 - assert_equal " ", @response.body - end - - test "admin can update user" do - user = find_record :user - changed_attribs = record_attributes_for :user_with_settings - account_settings = stub - account_settings.expects(:update).with(changed_attribs) - Account.expects(:new).with(user).returns(account_settings) - - login :is_admin? => true - put :update, :user => changed_attribs, :id => user.id, :format => :json - - assert_equal user, assigns[:user] - assert_response 204 - end - - test "user cannot update other user" do - user = find_record :user - login - put :update, :user => record_attributes_for(:user_with_settings), :id => user.id, :format => :json - assert_access_denied - end - - test "should create new user" do - user_attribs = record_attributes_for :user - user = User.new(user_attribs) - Account.expects(:create).with(user_attribs).returns(user) - - post :create, :user => user_attribs, :format => :json - - assert_nil session[:user_id] - assert_json_response user - assert_response :success - end - - test "should redirect to signup form on failed attempt" do - user_attribs = record_attributes_for :user - user_attribs.slice!('login') - user = User.new(user_attribs) - assert !user.valid? - Account.expects(:create).with(user_attribs).returns(user) - - post :create, :user => user_attribs, :format => :json - - assert_json_error user.errors.messages - assert_response 422 - end - - test "admin can autocomplete users" do - login :is_admin? => true - get :index, :query => 'a', :format => :json - - assert_response :success - assert assigns(:users) - end - -end diff --git a/users/test/functional/webfinger_controller_test.rb b/users/test/functional/webfinger_controller_test.rb deleted file mode 100644 index 6597b69..0000000 --- a/users/test/functional/webfinger_controller_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'test_helper' - -class WebfingerControllerTest < ActionController::TestCase - - test "get host meta xml" do - get :host_meta, :format => :xml - assert_response :success - assert_equal "application/xml", response.content_type - end - - test "get host meta json" do - get :host_meta, :format => :json - assert_response :success - assert_equal "application/json", response.content_type - end - - test "get user webfinger xml" do - @user = stub_record :user, :public_key => 'my public key' - User.stubs(:find_by_login).with(@user.login).returns(@user) - get :search, :q => @user.email_address.to_s, :format => :xml - assert_response :success - assert_equal "application/xml", response.content_type - end - - test "get user webfinger json" do - @user = stub_record :user, :public_key => 'my public key' - User.stubs(:find_by_login).with(@user.login).returns(@user) - get :search, :q => @user.email_address.to_s, :format => :json - assert_response :success - assert_equal "application/json", response.content_type - end - -end diff --git a/users/test/integration/.gitkeep b/users/test/integration/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/users/test/integration/api/Readme.md b/users/test/integration/api/Readme.md deleted file mode 100644 index 04363bd..0000000 --- a/users/test/integration/api/Readme.md +++ /dev/null @@ -1,23 +0,0 @@ -API tests -========== - - -Testing the restful api from a simple python client as that's what we'll be using. - -This test so far mostly demoes the API. We have no SRP calc in there. - -TODO: keep track of the cookies during login. The server uses the session to keep track of the random numbers A and B. - -The output of signup_and_login_wrong_password pretty well describes the SRP API: - -``` -POST: http://localhost:9292/users.json - {"user[password_salt]": "54321", "user[password_verifier]": "12345", "user[login]": "SWQ055"} - -> {"password_salt":"54321","login":"SWQ055"} -POST: http://localhost:9292/sessions - {"A": "12345", "login": "SWQ055"} - -> {"B":"1778367531e93a4c7713c76f67649f35a4211ebc520926ae8c3848cd66171651"} -PUT: http://localhost:9292/sessions/SWQ055 - {"M": "123ABC"} - -> {"errors":[{"login":"Not a valid username/password combination"},{"password":"Not a valid username/password combination"}]} -``` diff --git a/users/test/integration/api/login_test.rb b/users/test/integration/api/login_test.rb deleted file mode 100644 index 92d153f..0000000 --- a/users/test/integration/api/login_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'test_helper' -require_relative 'srp_test' - -class LoginTest < SrpTest - - setup do - register_user - end - - test "requires handshake before validation" do - validate("bla") - assert_json_error login: I18n.t(:all_strategies_failed) - end - - test "login with srp" do - authenticate - assert_equal ["M2", "id", "token"], server_auth.keys - assert last_response.successful? - assert_nil server_auth["errors"] - assert server_auth["M2"] - end - - test "wrong password login attempt" do - authenticate password: "wrong password" - assert_json_error "base" => "Not a valid username/password combination" - assert !last_response.successful? - assert_nil server_auth["M2"] - end - - test "wrong username login attempt" do - assert_raises RECORD_NOT_FOUND do - authenticate login: "wrong login" - end - assert_json_error "base" => "Not a valid username/password combination" - assert !last_response.successful? - assert_nil server_auth - end - - test "logout" do - authenticate - logout - assert_equal 204, last_response.status - end - - test "logout requires token" do - authenticate - logout(nil, {}) - assert_equal 422, last_response.status - end -end diff --git a/users/test/integration/api/pgp_key_test.rb b/users/test/integration/api/pgp_key_test.rb deleted file mode 100644 index 4c7fb4c..0000000 --- a/users/test/integration/api/pgp_key_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'test_helper' -require_relative 'srp_test' - -class PgpKeyTest < SrpTest - - setup do - # todo: prepare user and login without doing the srp dance - register_user - authenticate - end - - test "upload pgp key" do - update_user public_key: key - assert_equal key, Identity.for(@user).keys[:pgp] - end - - # eventually probably want to remove most of this into a non-integration - # functional test - test "prevent uploading invalid key" do - update_user public_key: "invalid key" - assert_nil Identity.for(@user).keys[:pgp] - end - - test "prevent emptying public key" do - update_user public_key: key - update_user public_key: "" - assert_equal key, Identity.for(@user).keys[:pgp] - end - - protected - - def key - @key ||= FactoryGirl.build :pgp_key - end -end diff --git a/users/test/integration/api/python/flow_with_srp.py b/users/test/integration/api/python/flow_with_srp.py deleted file mode 100755 index 9fc168b..0000000 --- a/users/test/integration/api/python/flow_with_srp.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python - -# under development - -import requests -import json -import string -import random -import srp._pysrp as srp -import binascii - -safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) - -# using globals for now -# server = 'https://dev.bitmask.net/1' -server = 'http://api.lvh.me:3000/1' - -def run_tests(): - login = 'test_' + id_generator() - password = id_generator() + id_generator() - usr = srp.User( login, password, srp.SHA256, srp.NG_1024 ) - print_and_parse(signup(login, password)) - - auth = print_and_parse(authenticate(usr)) - verify_or_debug(auth, usr) - assert usr.authenticated() - - usr = change_password(auth['id'], login, auth['token']) - - auth = print_and_parse(authenticate(usr)) - verify_or_debug(auth, usr) - # At this point the authentication process is complete. - assert usr.authenticated() - -# let's have some random name -def id_generator(size=6, chars=string.ascii_lowercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -# log the server communication -def print_and_parse(response): - request = response.request - print request.method + ': ' + response.url - if hasattr(request, 'data'): - print " " + json.dumps(response.request.data) - print " -> " + response.text - try: - return json.loads(response.text) - except ValueError: - return None - -def signup(login, password): - salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) - user_params = { - 'user[login]': login, - 'user[password_verifier]': binascii.hexlify(vkey), - 'user[password_salt]': binascii.hexlify(salt) - } - return requests.post(server + '/users.json', data = user_params, verify = False) - -def change_password(user_id, login, token): - password = id_generator() + id_generator() - salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) - user_params = { - 'user[password_verifier]': binascii.hexlify(vkey), - 'user[password_salt]': binascii.hexlify(salt) - } - auth_headers = { 'Authorization': 'Token token="' + token + '"'} - print user_params - print_and_parse(requests.put(server + '/users/' + user_id + '.json', data = user_params, verify = False, headers = auth_headers)) - return srp.User( login, password, srp.SHA256, srp.NG_1024 ) - - -def authenticate(usr): - session = requests.session() - uname, A = usr.start_authentication() - params = { - 'login': uname, - 'A': binascii.hexlify(A) - } - init = print_and_parse(session.post(server + '/sessions', data = params, verify=False)) - M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) - return session.put(server + '/sessions/' + uname, verify = False, - data = {'client_auth': binascii.hexlify(M)}) - -def verify_or_debug(auth, usr): - if ( 'errors' in auth ): - print ' u = "%x"' % usr.u - print ' x = "%x"' % usr.x - print ' v = "%x"' % usr.v - print ' S = "%x"' % usr.S - print ' K = "' + binascii.hexlify(usr.K) + '"' - print ' M = "' + binascii.hexlify(usr.M) + '"' - else: - usr.verify_session( safe_unhexlify(auth["M2"]) ) - -run_tests() diff --git a/users/test/integration/api/python/login_wrong_username.py b/users/test/integration/api/python/login_wrong_username.py deleted file mode 100755 index 390f250..0000000 --- a/users/test/integration/api/python/login_wrong_username.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python - -server = 'http://localhost:3000' - -import requests -import json -import string -import random - -def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -params = { - 'login': 'python_test_user_'+id_generator(), - 'A': '12345', - } -r = requests.post(server + '/sessions', data = params) -print r.url -print r.text diff --git a/users/test/integration/api/python/signup.py b/users/test/integration/api/python/signup.py deleted file mode 100755 index 0d3a4e0..0000000 --- a/users/test/integration/api/python/signup.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python - -server = 'http://localhost:3000' - -import requests -import json -import string -import random - -def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -user_params = { - 'user[login]': 'python_test_user_'+id_generator(), - 'user[password_verifier]': '12345', - 'user[password_salt]': '54321' - } -r = requests.post(server + '/users.json', data = user_params) -print r.url -print r.text diff --git a/users/test/integration/api/python/signup_and_login.py b/users/test/integration/api/python/signup_and_login.py deleted file mode 100755 index ac611d7..0000000 --- a/users/test/integration/api/python/signup_and_login.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python - -# FAILS -# -# This test is currently failing for me because the session is not kept. -# Played with it a bunch - is probably messed up right now as well. - - -server = 'http://localhost:3000' - -import requests -import json -import string -import random - -def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -def print_and_parse(response): - print response.request.method + ': ' + response.url - print " " + json.dumps(response.request.data) - print " -> " + response.text - return json.loads(response.text) - -def signup(session): - user_params = { - 'user[login]': id_generator(), - 'user[password_verifier]': '12345', - 'user[password_salt]': 'AB54321' - } - return session.post(server + '/users.json', data = user_params) - -def authenticate(session, login): - params = { - 'login': login, - 'A': '12345', - } - init = print_and_parse(session.post(server + '/sessions', data = params)) - return session.put(server + '/sessions/' + login, data = {'client_auth': '123'}) - -session = requests.session() -user = print_and_parse(signup(session)) -# SRP signup would happen here and calculate M hex -auth = print_and_parse(authenticate(session, user['login'])) diff --git a/users/test/integration/api/python/signup_and_login_wrong_password.py b/users/test/integration/api/python/signup_and_login_wrong_password.py deleted file mode 100755 index 9efffa1..0000000 --- a/users/test/integration/api/python/signup_and_login_wrong_password.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python - -server = 'http://localhost:9292' - -import requests -import json -import string -import random - -def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -def print_and_parse(response): - print response.request.method + ': ' + response.url - print " " + json.dumps(response.request.data) - print " -> " + response.text -# print " () " + json.dumps(requests.utils.dict_from_cookiejar(response.cookies)) - return json.loads(response.text) - -def signup(): - user_params = { - 'user[login]': id_generator(), - 'user[password_verifier]': '12345', - 'user[password_salt]': '54321' - } - return requests.post(server + '/users.json', data = user_params) - -def handshake(login): - params = { - 'login': login, - 'A': '12345', - } - return requests.post(server + '/sessions', data = params) - -def authenticate(login, M): - return requests.put(server + '/sessions/' + login, data = {'M': M}) - - -user = print_and_parse(signup()) -handshake = print_and_parse(handshake(user['login'])) -# SRP signup would happen here and calculate M hex -M = '123ABC' -auth = print_and_parse(authenticate(user['login'], M)) diff --git a/users/test/integration/api/python/umlauts.py b/users/test/integration/api/python/umlauts.py deleted file mode 100755 index 96fecbf..0000000 --- a/users/test/integration/api/python/umlauts.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -# under development - -import requests -import json -import string -import random -import srp._pysrp as srp -import binascii - -safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) - -# using globals for now -# server = 'https://dev.bitmask.net/1' -server = 'http://api.lvh.me:3000/1' - -def run_tests(): - login = 'test_' + id_generator() - password = id_generator() + "äöì" + id_generator() - usr = srp.User( login, password, srp.SHA256, srp.NG_1024 ) - print_and_parse(signup(login, password)) - - auth = print_and_parse(authenticate(usr)) - verify_or_debug(auth, usr) - assert usr.authenticated() - - -# let's have some random name -def id_generator(size=6, chars=string.ascii_lowercase + string.digits): - return ''.join(random.choice(chars) for x in range(size)) - -# log the server communication -def print_and_parse(response): - request = response.request - print request.method + ': ' + response.url - if hasattr(request, 'data'): - print " " + json.dumps(response.request.data) - print " -> " + response.text - try: - return json.loads(response.text) - except ValueError: - return None - -def signup(login, password): - salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 ) - user_params = { - 'user[login]': login, - 'user[password_verifier]': binascii.hexlify(vkey), - 'user[password_salt]': binascii.hexlify(salt) - } - print json.dumps(user_params) - return requests.post(server + '/users.json', data = user_params, verify = False) - -def authenticate(usr): - session = requests.session() - uname, A = usr.start_authentication() - params = { - 'login': uname, - 'A': binascii.hexlify(A) - } - init = print_and_parse(session.post(server + '/sessions', data = params, verify=False)) - M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) - return session.put(server + '/sessions/' + uname, verify = False, - data = {'client_auth': binascii.hexlify(M)}) - -def verify_or_debug(auth, usr): - if ( 'errors' in auth ): - print ' u = "%x"' % usr.u - print ' x = "%x"' % usr.x - print ' v = "%x"' % usr.v - print ' S = "%x"' % usr.S - print ' K = "' + binascii.hexlify(usr.K) + '"' - print ' M = "' + binascii.hexlify(usr.M) + '"' - else: - usr.verify_session( safe_unhexlify(auth["M2"]) ) - -run_tests() diff --git a/users/test/integration/api/signup_test.rb b/users/test/integration/api/signup_test.rb deleted file mode 100644 index 236c547..0000000 --- a/users/test/integration/api/signup_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'test_helper' -require_relative 'srp_test' - -class SignupTest < SrpTest - - setup do - register_user - end - - test "signup response" do - assert_json_response :login => @login, :ok => true - assert last_response.successful? - end - - test "signup creates user" do - assert @user - assert_equal @login, @user.login - end -end - diff --git a/users/test/integration/api/srp_test.rb b/users/test/integration/api/srp_test.rb deleted file mode 100644 index 946450e..0000000 --- a/users/test/integration/api/srp_test.rb +++ /dev/null @@ -1,104 +0,0 @@ -class SrpTest < RackTest - - teardown do - if @user - cleanup_user - end - Warden.test_reset! - end - - # 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", - :login => login, - 'A' => aa, - :format => :json - response = JSON.parse(last_response.body) - if response['errors'] - raise RECORD_NOT_FOUND.new(response['errors']) - else - return response['B'] - end - end - - def validate(m) - put "http://api.lvh.me:3000/1/sessions/" + @login + '.json', - :client_auth => m, - :format => :json - return JSON.parse(last_response.body) - end - - protected - - attr_reader :server_auth - - def register_user(login = "integration_test_user", password = 'srp, verify me!') - cleanup_user(login) - post 'http://api.lvh.me:3000/1/users.json', - user_params(login: login, password: password) - @user = User.find_by_login(login) - @login = login - @password = password - end - - def update_user(params) - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', - user_params(params), - auth_headers - end - - def authenticate(params = nil) - @server_auth = srp(params).authenticate(self) - end - - def auth_headers - return {} if @server_auth.nil? - { - "HTTP_AUTHORIZATION" => encoded_token - } - end - - def encoded_token - ActionController::HttpAuthentication::Token.encode_credentials(server_auth["token"]) - end - - def logout(params=nil, headers=nil) - delete "http://api.lvh.me:3000/1/logout.json", - params || {format: :json}, - headers || auth_headers - end - - def cleanup_user(login = nil) - login ||= @user.login - Identity.by_address.key(login + '@' + APP_CONFIG[:domain]).each do |identity| - identity.destroy - end - if user = User.find_by_login(login) - user.destroy - end - end - - def user_params(params) - if params.keys.include?(:password) - srp_process_password(params) - end - return { user: params, format: :json } - end - - def srp_process_password(params) - params.reverse_merge! login: @login, salt: @salt - @srp = SRP::Client.new params[:login], password: params.delete(:password) - @salt = srp.salt.to_s(16) - params.merge! :password_verifier => srp.verifier.to_s(16), - :password_salt => @salt - end - - def srp(params = nil) - if params.nil? - @srp - else - params.reverse_merge! password: @password - SRP::Client.new(params.delete(:login) || @login, params) - end - end -end diff --git a/users/test/integration/api/update_account_test.rb b/users/test/integration/api/update_account_test.rb deleted file mode 100644 index 63429e7..0000000 --- a/users/test/integration/api/update_account_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'test_helper' -require_relative 'srp_test' - -class UpdateAccountTest < SrpTest - - setup do - register_user - end - - test "require authentication" do - update_user password: "No! Verify me instead." - assert_access_denied - end - - test "require token" do - authenticate - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', - user_params(password: "No! Verify me instead.") - assert_access_denied - end - - test "update password via api" do - authenticate - update_user password: "No! Verify me instead." - authenticate - assert last_response.successful? - assert_nil server_auth["errors"] - assert server_auth["M2"] - end - - test "change login with password_verifier" do - authenticate - new_login = 'zaph' - cleanup_user new_login - update_user login: new_login, password: @password - authenticate - assert last_response.successful? - assert_equal new_login, @user.reload.login - end - - test "prevent changing login without changing password_verifier" do - authenticate - original_login = @user.login - new_login = 'zaph' - cleanup_user new_login - update_user login: new_login - assert last_response.successful? - # does not change login if no password_verifier is present - assert_equal original_login, @user.reload.login - end -end diff --git a/users/test/integration/browser/account_test.rb b/users/test/integration/browser/account_test.rb deleted file mode 100644 index a5677ad..0000000 --- a/users/test/integration/browser/account_test.rb +++ /dev/null @@ -1,147 +0,0 @@ -require 'test_helper' - -class AccountTest < BrowserIntegrationTest - - teardown do - Identity.destroy_all_disabled - end - - test "normal account workflow" do - username, password = submit_signup - assert page.has_content?("Welcome #{username}") - click_on 'Logout' - assert page.has_content?("Log In") - assert_equal '/', current_path - assert user = User.find_by_login(username) - user.account.destroy - end - - test "successful login" do - username, password = submit_signup - click_on 'Logout' - 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')) - 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 "default user actions" do - username, password = submit_signup - 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 - username, password = submit_signup - with_config admins: [username] 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 password" do - with_config user_actions: ['change_password'] do - username, password = submit_signup - click_on "Account Settings" - within('#update_login_and_password') do - fill_in 'Password', with: "other password" - fill_in 'Password confirmation', with: "other password" - click_on 'Save' - end - click_on 'Logout' - attempt_login(username, "other password") - assert page.has_content?("Welcome #{username}") - User.find_by_login(username).account.destroy - end - end - - test "change pgp key" do - with_config user_actions: ['change_pgp_key'] do - pgp_key = FactoryGirl.build :pgp_key - username, password = submit_signup - 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 = User.find_by_login(username) - assert_equal pgp_key, user.public_key - user.account.destroy - end - end - - - # trying to seed an invalid A for srp login - test "detects attempt to circumvent SRP" do - 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 - V1::UsersController.any_instance.stubs(:create).raises - submit_signup - assert page.has_content?("server failed") - 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? 'input.btn-primary.disabled' - assert page.has_content? I18n.t(:invalid_user_pass) - assert page.has_no_selector? 'input.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/users/test/integration/browser/session_test.rb b/users/test/integration/browser/session_test.rb deleted file mode 100644 index 3a41b3a..0000000 --- a/users/test/integration/browser/session_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'test_helper' - -class SessionTest < BrowserIntegrationTest - - setup do - @username, password = submit_signup - end - - teardown do - user = User.find_by_login(@username) - id = user.identity - id.destroy - user.destroy - end - - test "valid session" do - assert page.has_content?("Welcome #{@username}") - end - - test "expired session" do - assert page.has_content?("Welcome #{@username}") - pretend_now_is(Time.now + 40.minutes) do - visit '/' - assert page.has_no_content?("Welcome #{@username}") - end - end -end diff --git a/users/test/integration/navigation_test.rb b/users/test/integration/navigation_test.rb deleted file mode 100644 index eec8c0e..0000000 --- a/users/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/users/test/leap_web_users_test.rb b/users/test/leap_web_users_test.rb deleted file mode 100644 index f142e54..0000000 --- a/users/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/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb deleted file mode 100644 index 57f9f9b..0000000 --- a/users/test/support/auth_test_helper.rb +++ /dev/null @@ -1,65 +0,0 @@ -module AuthTestHelper - extend ActiveSupport::Concern - - # Controller will fetch current user from warden. - # Make it pick up our current_user - included do - setup do - request.env['warden'] ||= stub :user => nil - end - end - - def login(user_or_method_hash = {}) - if user_or_method_hash.respond_to?(:reverse_merge) - user_or_method_hash.reverse_merge! :is_admin? => false - end - @current_user = stub_record(:user, user_or_method_hash) - request.env['warden'] = stub :user => @current_user - request.env['HTTP_AUTHORIZATION'] = header_for_token_auth - return @current_user - end - - def assert_access_denied(denied = true, logged_in = true) - if denied - if @response.content_type == 'application/json' - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity - else - if logged_in - assert_equal({:alert => I18n.t(:not_authorized)}, flash.to_hash) - assert_redirected_to home_url - else - assert_equal({:alert => I18n.t(:not_authorized_login)}, flash.to_hash) - assert_redirected_to login_url - end - end - else - assert flash[:alert].blank? - end - end - - def expect_logout - expect_warden_logout - @token.expects(:destroy) if @token - end - - protected - - def header_for_token_auth - @token = find_record(:token, :authenticate => @current_user) - ActionController::HttpAuthentication::Token.encode_credentials @token.id - end - - def expect_warden_logout - raw = mock('raw session') do - expects(:inspect) - end - request.env['warden'].expects(:raw_session).returns(raw) - request.env['warden'].expects(:logout) - end - -end - -class ActionController::TestCase - include AuthTestHelper -end diff --git a/users/test/support/stub_record_helper.rb b/users/test/support/stub_record_helper.rb deleted file mode 100644 index 25138a0..0000000 --- a/users/test/support/stub_record_helper.rb +++ /dev/null @@ -1,53 +0,0 @@ -module StubRecordHelper - - # - # We will stub find when called on the records class and - # return the record given. - # - # If no record is given but a hash or nil will create a stub based on - # that instead and returns the stub. - # - def find_record(factory, record_or_attribs_hash = {}) - record = stub_record factory, record_or_attribs_hash, true - klass = record.class - # find is just an alias for get with CouchRest Model - klass.stubs(:get).with(record.to_param.to_s).returns(record) - klass.stubs(:find).with(record.to_param.to_s).returns(record) - return record - end - - # Create a stub that has the usual functions of a database record. - # It won't fail on rendering a form for example. - # - # If the second parameter is a record we return the record itself. - # This way you can build functions that either take a record or a - # method hash to stub from. See find_record for an example. - def stub_record(factory, record_or_method_hash = {}, persisted=false) - if record_or_method_hash && !record_or_method_hash.is_a?(Hash) - return record_or_method_hash - end - FactoryGirl.build_stubbed(factory).tap do |record| - if persisted or record.persisted? - record_or_method_hash.reverse_merge! :created_at => Time.now, - :updated_at => Time.now, :id => Random.rand(100000).to_s - end - record.stubs(record_or_method_hash) if record_or_method_hash.present? - end - end - - # returns deep stringified attributes so they can be compared to - # what the controller receives as params - def record_attributes_for(factory, attribs_hash = nil) - FactoryGirl.attributes_for(factory, attribs_hash).tap do |attribs| - attribs.keys.each do |key| - val = attribs.delete(key) - attribs[key.to_s] = val.is_a?(Hash) ? val.stringify_keys! : val - end - end - end - -end - -class ActionController::TestCase - include StubRecordHelper -end diff --git a/users/test/support/time_test_helper.rb b/users/test/support/time_test_helper.rb deleted file mode 100644 index f673f12..0000000 --- a/users/test/support/time_test_helper.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Extend the Time class so that we can offset the time that 'now' -# returns. This should allow us to effectively time warp for functional -# tests that require limits per hour, what not. -class Time #:nodoc: - class < 0 - Identity.destroy_all_disabled - assert_equal 0, Identity.disabled.count - end - - def alias_name - @alias_name ||= Faker::Internet.user_name - end - - def forward_address - @forward_address ||= Faker::Internet.email - end - - def pgp_key_string - @pgp_key ||= "DUMMY PGP KEY ... "+SecureRandom.base64(4096) - end -end diff --git a/users/test/unit/local_email_test.rb b/users/test/unit/local_email_test.rb deleted file mode 100644 index 20ee7f1..0000000 --- a/users/test/unit/local_email_test.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'test_helper' - -class LocalEmailTest < ActiveSupport::TestCase - - test "appends domain" do - local = LocalEmail.new(handle) - assert_equal LocalEmail.new(email), local - assert local.valid? - end - - test "returns handle" do - local = LocalEmail.new(email) - assert_equal handle, local.handle - end - - test "prints full email" do - local = LocalEmail.new(handle) - assert_equal email, "#{local}" - end - - test "validates domain" do - local = LocalEmail.new(Faker::Internet.email) - assert !local.valid? - assert_equal ["needs to end in @#{LocalEmail.domain}"], local.errors[:email] - end - - test "blacklists rfc2142" do - black_listed = LocalEmail.new('hostmaster') - assert !black_listed.valid? - end - - test "blacklists etc passwd" do - black_listed = LocalEmail.new('nobody') - assert !black_listed.valid? - end - - test "whitelist overwrites automatic blacklists" do - with_config handle_whitelist: ['nobody', 'hostmaster'] do - white_listed = LocalEmail.new('nobody') - assert white_listed.valid? - white_listed = LocalEmail.new('hostmaster') - assert white_listed.valid? - end - end - - test "blacklists from config" do - black_listed = LocalEmail.new('www-data') - assert !black_listed.valid? - end - - test "blacklist from config overwrites whitelist" do - with_config handle_whitelist: ['www-data'] do - black_listed = LocalEmail.new('www-data') - assert !black_listed.valid? - end - end - - def handle - @handle ||= Faker::Internet.user_name - end - - def email - handle + "@" + APP_CONFIG[:domain] - end -end diff --git a/users/test/unit/token_test.rb b/users/test/unit/token_test.rb deleted file mode 100644 index a3c6cf6..0000000 --- a/users/test/unit/token_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'test_helper' - -class ClientCertificateTest < ActiveSupport::TestCase - include StubRecordHelper - - setup do - @user = find_record :user - end - - test "new token for user" do - sample = Token.new(:user_id => @user.id) - assert sample.valid? - assert_equal @user.id, sample.user_id - assert_equal @user, sample.authenticate - end - - test "token id is secure" do - sample = Token.new(:user_id => @user.id) - other = Token.new(:user_id => @user.id) - assert sample.id, - "id is set on initialization" - assert sample.id[0..10] != other.id[0..10], - "token id prefixes should not repeat" - assert /[g-zG-Z]/.match(sample.id), - "should use non hex chars in the token id" - assert sample.id.size > 16, - "token id should be more than 16 chars long" - end - - test "token checks for user" do - sample = Token.new - assert !sample.valid?, "Token should require a user record" - end - - test "token updates timestamps" do - sample = Token.new(user_id: @user.id) - sample.last_seen_at = 1.minute.ago - sample.expects(:save) - assert_equal @user, sample.authenticate - assert Time.now - sample.last_seen_at < 1.minute, "last_seen_at has not been updated" - end - - test "token will not expire if token_expires_after is not set" do - sample = Token.new(user_id: @user.id) - sample.last_seen_at = 2.years.ago - with_config auth: {} do - sample.expects(:save) - assert_equal @user, sample.authenticate - end - end - - test "expired token returns nil on authenticate" do - sample = Token.new(user_id: @user.id) - sample.last_seen_at = 2.hours.ago - with_config auth: {token_expires_after: 60} do - sample.expects(:destroy) - assert_nil sample.authenticate - end - end - - test "Token.destroy_all_expired is noop if no expiry is set" do - expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago - with_config auth: {} do - Token.destroy_all_expired - end - assert_equal expired, Token.find(expired.id) - end - - test "Token.destroy_all_expired cleans up expired tokens only" do - expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago - fresh = FactoryGirl.create :token - with_config auth: {token_expires_after: 60} do - Token.destroy_all_expired - end - assert_nil Token.find(expired.id) - assert_equal fresh, Token.find(fresh.id) - fresh.destroy - end - - - test "Token.destroy_all_expired does not interfere with expired.authenticate" do - expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago - with_config auth: {token_expires_after: 60} do - Token.destroy_all_expired - end - assert_nil expired.authenticate - end - -end diff --git a/users/test/unit/unauthenticated_user_test.rb b/users/test/unit/unauthenticated_user_test.rb deleted file mode 100644 index e5fafb8..0000000 --- a/users/test/unit/unauthenticated_user_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class UnauthenticatedUserTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/users/test/unit/user_test.rb b/users/test/unit/user_test.rb deleted file mode 100644 index ffbb7d8..0000000 --- a/users/test/unit/user_test.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'test_helper' - -class UserTest < ActiveSupport::TestCase - - include SRP::Util - setup do - @user = FactoryGirl.build(:user) - end - - test "design docs in database are authorative" do - assert !User.design_doc.auto_update, - "Automatic update of design docs should be disabled" - end - - test "test set of attributes should be valid" do - @user.valid? - assert_equal Hash.new, @user.errors.messages - end - - test "test require hex for password_verifier" do - @user.password_verifier = "QWER" - assert !@user.valid? - end - - test "test require alphanumerical for login" do - @user.login = "qw#r" - assert !@user.valid? - end - - test "verifier returns number for the hex in password_verifier" do - assert_equal @user.password_verifier.hex, @user.verifier - end - - test "salt returns number for the hex in password_salt" do - assert_equal @user.password_salt.hex, @user.salt - end - - test 'normal user is no admin' do - assert !@user.is_admin? - end - - test 'user with login in APP_CONFIG is an admin' do - admin_login = APP_CONFIG['admins'].first - @user.login = admin_login - assert @user.is_admin? - end - - test "login needs to be unique" do - other_user = FactoryGirl.create :user, login: @user.login - assert !@user.valid? - other_user.destroy - end - - test "login needs to be unique amongst aliases" do - other_user = FactoryGirl.create :user - id = Identity.create_for other_user, address: @user.login - assert !@user.valid? - id.destroy - other_user.destroy - end - - test "deprecated public key api still works" do - key = SecureRandom.base64(4096) - @user.public_key = key - assert_equal key, @user.public_key - end - -end diff --git a/users/test/unit/warden_strategy_secure_remote_password_test.rb b/users/test/unit/warden_strategy_secure_remote_password_test.rb deleted file mode 100644 index e6fcfbe..0000000 --- a/users/test/unit/warden_strategy_secure_remote_password_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -class WardenStrategySecureRemotePasswordTest < ActiveSupport::TestCase - -# TODO : turn this into sth. real -=begin - setup do - @user = stub :login => "me", :id => 123 - @client_hex = 'a123' - @client_rnd = @client_hex.hex - @server_hex = 'b123' - @server_rnd = @server_hex.hex - @server_rnd_exp = 'e123'.hex - @salt = 'stub user salt' - @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp - @server_auth = 'adfe' - end - - - test "should perform handshake" do - @user.expects(:initialize_auth). - with(@client_rnd). - returns(@server_handshake) - @server_handshake.expects(:to_json). - returns({'B' => @server_hex, 'salt' => @salt}.to_json) - User.expects(:find).with(@user.login).returns(@user) - assert_equal @server_handshake, session[:handshake] - assert_response :success - assert_json_response :B => @server_hex, :salt => @salt - end - - test "should report user not found" do - unknown = "login_that_does_not_exist" - User.expects(:find).with(unknown).raises(RECORD_NOT_FOUND) - post :create, :login => unknown - assert_response :success - assert_json_error "login" => ["unknown user"] - end - - test "should authorize" do - session[:handshake] = @server_handshake - @server_handshake.expects(:authenticate!). - with(@client_rnd). - returns(@user) - @server_handshake.expects(:to_json). - returns({:M2 => @server_auth}.to_json) - post :update, :id => @user.login, :client_auth => @client_hex - assert_nil session[:handshake] - assert_json_response :M2 => @server_auth - assert_equal @user.id, session[:user_id] - end - - test "should report wrong password" do - session[:handshake] = @server_handshake - @server_handshake.expects(:authenticate!). - with(@client_rnd). - raises(WRONG_PASSWORD) - post :update, :id => @user.login, :client_auth => @client_hex - assert_nil session[:handshake] - assert_nil session[:user_id] - assert_json_error "password" => ["wrong password"] - end - -=end -end diff --git a/users/test/unit/webfinger/host_meta_presenter_test.rb b/users/test/unit/webfinger/host_meta_presenter_test.rb deleted file mode 100644 index af86404..0000000 --- a/users/test/unit/webfinger/host_meta_presenter_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'test_helper' -require 'webfinger' -require 'json' - -class Webfinger::HostMetaPresenterTest < ActiveSupport::TestCase - - setup do - @request = stub( - url: "https://#{APP_CONFIG[:domain]}/.well-known/host-meta" - ) - @meta = Webfinger::HostMetaPresenter.new(@request) - end - - test "creates proper json" do - hash = JSON.parse @meta.to_json - assert_equal ["subject", "links"].sort, hash.keys.sort - hash.each do |key, value| - assert_equal @meta.send(key.to_sym).to_json, value.to_json - end - end - -end - - diff --git a/users/test/unit/webfinger/user_presenter_test.rb b/users/test/unit/webfinger/user_presenter_test.rb deleted file mode 100644 index 04aeb22..0000000 --- a/users/test/unit/webfinger/user_presenter_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'test_helper' -require 'webfinger' -require 'json' - -class Webfinger::UserPresenterTest < ActiveSupport::TestCase - - - setup do - @user = stub( - username: 'testuser', - email_address: "testuser@#{APP_CONFIG[:domain]}" - ) - @request = stub( - host: APP_CONFIG[:domain] - ) - end - - test "user without key has no links" do - @user.stubs :public_key => nil - presenter = Webfinger::UserPresenter.new(@user, @request) - assert_equal Hash.new, presenter.links - end - - test "user with key has corresponding link" do - @user.stubs :public_key => "here's a key" - presenter = Webfinger::UserPresenter.new(@user, @request) - assert_equal [:public_key], presenter.links.keys - assert_equal "PGP", presenter.links[:public_key][:type] - assert_equal presenter.send(:key), presenter.links[:public_key][:href] - end - - test "key is base64 encoded" do - @user.stubs :public_key => "here's a key" - presenter = Webfinger::UserPresenter.new(@user, @request) - assert_equal Base64.encode64(@user.public_key), presenter.send(:key) - end - - test "creates proper json representation" do - @user.stubs :public_key => "here's a key" - presenter = Webfinger::UserPresenter.new(@user, @request) - hash = JSON.parse presenter.to_json - assert_equal ["subject", "links"].sort, hash.keys.sort - hash.each do |key, value| - assert_equal presenter.send(key.to_sym).to_json, value.to_json - end - end - - -end -- cgit v1.2.3 From 9776dd10fc60efde38946588d0955c406f6c1f5e Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 14:22:56 +0200 Subject: move users: config and lib --- Gemfile.lock | 10 +-- config/initializers/add_controller_methods.rb | 4 ++ config/initializers/error_constants.rb | 3 + config/initializers/warden.rb | 10 +++ config/initializers/webfinger.rb | 1 + config/locales/users.en.yml | 73 +++++++++++++++++++ config/routes.rb | 27 ++++++++ lib/tasks/leap_web_users_tasks.rake | 10 +++ lib/warden/session_serializer.rb | 13 ++++ lib/warden/strategies/secure_remote_password.rb | 81 ++++++++++++++++++++++ lib/webfinger.rb | 6 ++ lib/webfinger/host_meta_presenter.rb | 30 ++++++++ lib/webfinger/user_presenter.rb | 35 ++++++++++ .../config/initializers/add_controller_methods.rb | 4 -- users/config/initializers/error_constants.rb | 1 - users/config/initializers/warden.rb | 7 -- users/config/locales/en.yml | 73 ------------------- users/config/routes.rb | 29 -------- users/lib/leap_web_users.rb | 4 -- users/lib/leap_web_users/engine.rb | 14 ---- users/lib/tasks/leap_web_users_tasks.rake | 10 --- users/lib/warden/session_serializer.rb | 13 ---- .../warden/strategies/secure_remote_password.rb | 81 ---------------------- users/lib/webfinger.rb | 6 -- users/lib/webfinger/host_meta_presenter.rb | 30 -------- users/lib/webfinger/user_presenter.rb | 35 ---------- users/script/rails | 8 --- 27 files changed, 295 insertions(+), 323 deletions(-) create mode 100644 config/initializers/add_controller_methods.rb create mode 100644 config/initializers/error_constants.rb create mode 100644 config/initializers/warden.rb create mode 100644 config/initializers/webfinger.rb create mode 100644 config/locales/users.en.yml create mode 100644 lib/tasks/leap_web_users_tasks.rake create mode 100644 lib/warden/session_serializer.rb create mode 100644 lib/warden/strategies/secure_remote_password.rb create mode 100644 lib/webfinger.rb create mode 100644 lib/webfinger/host_meta_presenter.rb create mode 100644 lib/webfinger/user_presenter.rb delete mode 100644 users/config/initializers/add_controller_methods.rb delete mode 100644 users/config/initializers/error_constants.rb delete mode 100644 users/config/initializers/warden.rb delete mode 100644 users/config/locales/en.yml delete mode 100644 users/config/routes.rb delete mode 100644 users/lib/leap_web_users.rb delete mode 100644 users/lib/leap_web_users/engine.rb delete mode 100644 users/lib/tasks/leap_web_users_tasks.rake delete mode 100644 users/lib/warden/session_serializer.rb delete mode 100644 users/lib/warden/strategies/secure_remote_password.rb delete mode 100644 users/lib/webfinger.rb delete mode 100644 users/lib/webfinger/host_meta_presenter.rb delete mode 100644 users/lib/webfinger/user_presenter.rb delete mode 100755 users/script/rails diff --git a/Gemfile.lock b/Gemfile.lock index 6c43a9f..0967bd6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,13 +22,6 @@ PATH specs: leap_web_help (0.5.0) -PATH - remote: users - specs: - leap_web_users (0.5.0) - rails_warden - ruby-srp (~> 0.2.1) - GEM remote: https://rubygems.org/ specs: @@ -274,7 +267,6 @@ DEPENDENCIES leap_web_billing! leap_web_certs! leap_web_help! - leap_web_users! minitest-stub-const mocha (~> 0.13.0) phantomjs-binaries @@ -282,7 +274,9 @@ DEPENDENCIES quiet_assets rails (~> 3.2.11) rails-i18n + rails_warden rdiscount + ruby-srp (~> 0.2.1) sass-rails (~> 3.2.5) simple_form therubyracer (~> 0.10.2) diff --git a/config/initializers/add_controller_methods.rb b/config/initializers/add_controller_methods.rb new file mode 100644 index 0000000..f572ecb --- /dev/null +++ b/config/initializers/add_controller_methods.rb @@ -0,0 +1,4 @@ +ActiveSupport.on_load(:application_controller) do + include ControllerExtension::Authentication + include ControllerExtension::TokenAuthentication +end diff --git a/config/initializers/error_constants.rb b/config/initializers/error_constants.rb new file mode 100644 index 0000000..fdd3624 --- /dev/null +++ b/config/initializers/error_constants.rb @@ -0,0 +1,3 @@ +require 'ruby-srp' + +WRONG_PASSWORD = SRP::WrongPassword diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb new file mode 100644 index 0000000..22892b3 --- /dev/null +++ b/config/initializers/warden.rb @@ -0,0 +1,10 @@ +require "warden/session_serializer" +require "warden/strategies/secure_remote_password" + +Rails.configuration.middleware.use RailsWarden::Manager do |config| + config.default_strategies :secure_remote_password + config.failure_app = SessionsController +end + +RailsWarden.unauthenticated_action = :new + diff --git a/config/initializers/webfinger.rb b/config/initializers/webfinger.rb new file mode 100644 index 0000000..197062c --- /dev/null +++ b/config/initializers/webfinger.rb @@ -0,0 +1 @@ +require 'webfinger' diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml new file mode 100644 index 0000000..ed6653a --- /dev/null +++ b/config/locales/users.en.yml @@ -0,0 +1,73 @@ +en: + account_settings: "Account Settings" + logout: "Logout" + none: "None" + signup: "Sign Up" + signup_message: "Please create an account." + cancel: "Cancel" + login: "Log In" + username: "Username" + password: "Password" + change_password: "Change Password" + login_message: "Please log in with your account." + invalid_user_pass: "Not a valid username/password combination" + invalid_ephemeral: "Invalid random key used. This looked like an attempt to hack the site to us. If it wasn't please contact support so we can look into the issue." + all_strategies_failed: "Could not understand your login attempt. Please first send your login and a SRP ephemeral value A and then send the client_auth in the same session (using cookies)." + update_login_and_password: "Update Login and Password" + destroy_my_account: "Destroy my account" + destroy_account_info: "This will permanently destroy your account and all the data associated with it. Proceed with caution!" + admin_destroy_account: "Destroy the account %{username}" + account_destroyed: "The account has been destroyed successfully." + set_email_address: "Set email address" + forward_email: "Forward Email" + email_aliases: "Email Aliases" + public_key: "Public Key" + add_email_alias: "Add Email Alias" + user_updated_successfully: "Settings have been updated successfully." + user_created_successfully: "Successfully created your account." + email_alias_destroyed_successfully: "Removed email alias %{alias}." + use_ascii_key: "OpenPGP public key. Do not change this value unless you know what you are doing." + advanced_options: "Advanced Options" + not_authorized: "Sorry, but you are not authorized to perform that action." + not_authorized_login: "Please log in to perform that action." + search: "Search" + cookie_disabled_warning: "You have cookies disabled. You will not be able to login until you enable cookies." + js_required_html: "We are sorry, but this doesn't work without javascript enabled. This is because the authentication system used, SRP, requires javascript." + enable_account: "Enable the account %{username}" + enable_description: "This will restore the account to full functionality" + deactivate_account: "Deactivate the account %{username}" + deactivate_description: "This will temporarily deactivate some account functionality." #todo detail exact functionality. can receive email but not send or renew client certificate? + payment_one_month_warning: "We hope you have been enjoying this service this past month. Please sign up to pay within the next month, by %{date_in_one_month}. Directions for payment are available at INSERT_URL" + bye: "Goodbye!" + bye_message: "So long and thanks for all the fish." + + # + # overview + # + overview_welcome: "Welcome %{username}." + overview_intro: "From this user control panel, you can:" + overview_tickets: "Create and check support tickets." + overview_email: "Modify email settings." + overview_account: "Delete your account." + + # + # rails + # + activemodel: + models: + user: + one: User + other: "%{count} Users" + simple_form: + labels: + user: + email_forward: "Email Forward" + hints: + user: + email_forward: > + Forward all email messages to this address. Messages will be encrypted before being forwarded. + This is an option for advanced users who are familar with OpenPGP. + placeholders: + user: + email_forward: "my_other_email@domain.net" + diff --git a/config/routes.rb b/config/routes.rb index f8bb8fb..05eca9b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,4 +16,31 @@ LeapWeb::Application.routes.draw do end get '/provider.json' => 'static_config#provider' + + namespace "api", { module: "v1", + path: "/1/", + defaults: {format: 'json'} } do + resources :sessions, :only => [:new, :create, :update] + delete "logout" => "sessions#destroy", :as => "logout" + resources :users, :only => [:create, :update, :destroy, :index] + resources :messages, :only => [:index, :update] + end + + scope "(:locale)", :locale => MATCH_LOCALE 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 + # resource :email_settings, :only => [:edit, :update] + # resources :email_aliases, :only => [:destroy], :id => /.*/ + post 'deactivate', on: :member + post 'enable', on: :member + end + end + + get "/.well-known/host-meta" => 'webfinger#host_meta' + get "/webfinger" => 'webfinger#search' + get "/key/:login" => 'keys#show' + end diff --git a/lib/tasks/leap_web_users_tasks.rake b/lib/tasks/leap_web_users_tasks.rake new file mode 100644 index 0000000..62bcbe9 --- /dev/null +++ b/lib/tasks/leap_web_users_tasks.rake @@ -0,0 +1,10 @@ +# desc "Explaining what the task does" +# task :leap_web_users do +# # Task goes here +# end + +# recommended that for our setup, we should have this triggered from a cron job in puppet rather than using whenever gem +desc "Send one month warning messages" +task :leap_web_users do + User.send_one_month_warnings +end diff --git a/lib/warden/session_serializer.rb b/lib/warden/session_serializer.rb new file mode 100644 index 0000000..81d7076 --- /dev/null +++ b/lib/warden/session_serializer.rb @@ -0,0 +1,13 @@ +module Warden + # Setup Session Serialization + class SessionSerializer + def serialize(record) + [record.class.name, record.id] + end + + def deserialize(keys) + klass, id = keys + klass.constantize.find(id) + end + end +end diff --git a/lib/warden/strategies/secure_remote_password.rb b/lib/warden/strategies/secure_remote_password.rb new file mode 100644 index 0000000..2c334c6 --- /dev/null +++ b/lib/warden/strategies/secure_remote_password.rb @@ -0,0 +1,81 @@ +module Warden + module Strategies + class SecureRemotePassword < Warden::Strategies::Base + + def valid? + handshake? || authentication? + end + + def authenticate! + if authentication? + validate! + else # handshake + initialize! + end + end + + protected + + def handshake? + params['A'] && params['login'] + end + + def authentication? + params['client_auth'] && session[:handshake] + end + + def validate! + if client = validate + success!(User.find_by_login(client.username)) + else + Rails.logger.warn "Login attempt failed." + Rails.logger.debug debug_info + Rails.logger.debug "Received: #{params['client_auth']}" + session.delete(:handshake) + fail!(:base => "invalid_user_pass") + end + end + + def validate + session[:handshake].authenticate(params['client_auth']) + end + + def initialize! + if user = User.find_by_login(id) + client = SRP::Client.new user.username, + :verifier => user.verifier, + :salt => user.salt + session[:handshake] = SRP::Session.new(client, params['A']) + custom! json_response(session[:handshake]) + else + fail! :base => 'invalid_user_pass' + end + rescue SRP::InvalidEphemeral + fail!(:base => "invalid_ephemeral") + end + + def json_response(object) + [ 200, + {"Content-Type" => "application/json; charset=utf-8"}, + [object.to_json] + ] + end + + def id + params["id"] || params["login"] + end + + protected + + def debug_info + JSON.pretty_generate(session[:handshake].internal_state) + end + + end + end + Warden::Strategies.add :secure_remote_password, + Warden::Strategies::SecureRemotePassword + +end + + diff --git a/lib/webfinger.rb b/lib/webfinger.rb new file mode 100644 index 0000000..dd49b41 --- /dev/null +++ b/lib/webfinger.rb @@ -0,0 +1,6 @@ +module Webfinger + + autoload :HostMetaPresenter, 'webfinger/host_meta_presenter' + autoload :UserPresenter, 'webfinger/user_presenter' + +end diff --git a/lib/webfinger/host_meta_presenter.rb b/lib/webfinger/host_meta_presenter.rb new file mode 100644 index 0000000..84ab7a9 --- /dev/null +++ b/lib/webfinger/host_meta_presenter.rb @@ -0,0 +1,30 @@ +require 'uri' + +class Webfinger::HostMetaPresenter + def initialize(request) + @request = request + end + + def to_json(options = {}) + { + subject: subject, + links: links + }.to_json(options) + end + + def subject + url = URI.parse(@request.url) + url.path = '' + url.to_s + end + + def links + { lrdd: { type: 'application/xrd+xml', template: webfinger_template } } + end + + protected + + def webfinger_template(path = 'webfinger', query_param='q') + "#{subject}/#{path}?#{query_param}={uri}" + end +end diff --git a/lib/webfinger/user_presenter.rb b/lib/webfinger/user_presenter.rb new file mode 100644 index 0000000..329f477 --- /dev/null +++ b/lib/webfinger/user_presenter.rb @@ -0,0 +1,35 @@ +class Webfinger::UserPresenter + include Rails.application.routes.url_helpers + attr_accessor :user + + def initialize(user, request) + @user = user + @request = request + end + + def to_json(options = {}) + { + subject: subject, + links: links + }.to_json(options) + end + + def subject + "acct:#{@user.email_address}" + end + + def links + links = {} + links[:public_key] = { type: 'PGP', href: key } if key + return links + end + + protected + + def key + if @user.public_key.present? + Base64.encode64(@user.public_key.to_s) + end + end + +end diff --git a/users/config/initializers/add_controller_methods.rb b/users/config/initializers/add_controller_methods.rb deleted file mode 100644 index f572ecb..0000000 --- a/users/config/initializers/add_controller_methods.rb +++ /dev/null @@ -1,4 +0,0 @@ -ActiveSupport.on_load(:application_controller) do - include ControllerExtension::Authentication - include ControllerExtension::TokenAuthentication -end diff --git a/users/config/initializers/error_constants.rb b/users/config/initializers/error_constants.rb deleted file mode 100644 index d4a0f52..0000000 --- a/users/config/initializers/error_constants.rb +++ /dev/null @@ -1 +0,0 @@ -WRONG_PASSWORD = SRP::WrongPassword diff --git a/users/config/initializers/warden.rb b/users/config/initializers/warden.rb deleted file mode 100644 index 45feb6c..0000000 --- a/users/config/initializers/warden.rb +++ /dev/null @@ -1,7 +0,0 @@ -Rails.configuration.middleware.use RailsWarden::Manager do |config| - config.default_strategies :secure_remote_password - config.failure_app = SessionsController -end - -RailsWarden.unauthenticated_action = :new - diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml deleted file mode 100644 index ed6653a..0000000 --- a/users/config/locales/en.yml +++ /dev/null @@ -1,73 +0,0 @@ -en: - account_settings: "Account Settings" - logout: "Logout" - none: "None" - signup: "Sign Up" - signup_message: "Please create an account." - cancel: "Cancel" - login: "Log In" - username: "Username" - password: "Password" - change_password: "Change Password" - login_message: "Please log in with your account." - invalid_user_pass: "Not a valid username/password combination" - invalid_ephemeral: "Invalid random key used. This looked like an attempt to hack the site to us. If it wasn't please contact support so we can look into the issue." - all_strategies_failed: "Could not understand your login attempt. Please first send your login and a SRP ephemeral value A and then send the client_auth in the same session (using cookies)." - update_login_and_password: "Update Login and Password" - destroy_my_account: "Destroy my account" - destroy_account_info: "This will permanently destroy your account and all the data associated with it. Proceed with caution!" - admin_destroy_account: "Destroy the account %{username}" - account_destroyed: "The account has been destroyed successfully." - set_email_address: "Set email address" - forward_email: "Forward Email" - email_aliases: "Email Aliases" - public_key: "Public Key" - add_email_alias: "Add Email Alias" - user_updated_successfully: "Settings have been updated successfully." - user_created_successfully: "Successfully created your account." - email_alias_destroyed_successfully: "Removed email alias %{alias}." - use_ascii_key: "OpenPGP public key. Do not change this value unless you know what you are doing." - advanced_options: "Advanced Options" - not_authorized: "Sorry, but you are not authorized to perform that action." - not_authorized_login: "Please log in to perform that action." - search: "Search" - cookie_disabled_warning: "You have cookies disabled. You will not be able to login until you enable cookies." - js_required_html: "We are sorry, but this doesn't work without javascript enabled. This is because the authentication system used, SRP, requires javascript." - enable_account: "Enable the account %{username}" - enable_description: "This will restore the account to full functionality" - deactivate_account: "Deactivate the account %{username}" - deactivate_description: "This will temporarily deactivate some account functionality." #todo detail exact functionality. can receive email but not send or renew client certificate? - payment_one_month_warning: "We hope you have been enjoying this service this past month. Please sign up to pay within the next month, by %{date_in_one_month}. Directions for payment are available at INSERT_URL" - bye: "Goodbye!" - bye_message: "So long and thanks for all the fish." - - # - # overview - # - overview_welcome: "Welcome %{username}." - overview_intro: "From this user control panel, you can:" - overview_tickets: "Create and check support tickets." - overview_email: "Modify email settings." - overview_account: "Delete your account." - - # - # rails - # - activemodel: - models: - user: - one: User - other: "%{count} Users" - simple_form: - labels: - user: - email_forward: "Email Forward" - hints: - user: - email_forward: > - Forward all email messages to this address. Messages will be encrypted before being forwarded. - This is an option for advanced users who are familar with OpenPGP. - placeholders: - user: - email_forward: "my_other_email@domain.net" - diff --git a/users/config/routes.rb b/users/config/routes.rb deleted file mode 100644 index 2819fa9..0000000 --- a/users/config/routes.rb +++ /dev/null @@ -1,29 +0,0 @@ -Rails.application.routes.draw do - - namespace "api", { module: "v1", - path: "/1/", - defaults: {format: 'json'} } do - resources :sessions, :only => [:new, :create, :update] - delete "logout" => "sessions#destroy", :as => "logout" - resources :users, :only => [:create, :update, :destroy, :index] - resources :messages, :only => [:index, :update] - end - - scope "(:locale)", :locale => MATCH_LOCALE 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 - # resource :email_settings, :only => [:edit, :update] - # resources :email_aliases, :only => [:destroy], :id => /.*/ - post 'deactivate', on: :member - post 'enable', on: :member - end - end - - get "/.well-known/host-meta" => 'webfinger#host_meta' - get "/webfinger" => 'webfinger#search' - get "/key/:login" => 'keys#show' - -end diff --git a/users/lib/leap_web_users.rb b/users/lib/leap_web_users.rb deleted file mode 100644 index e1b7b1f..0000000 --- a/users/lib/leap_web_users.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "leap_web_users/engine" - -module LeapWebUsers -end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb deleted file mode 100644 index a2be168..0000000 --- a/users/lib/leap_web_users/engine.rb +++ /dev/null @@ -1,14 +0,0 @@ -# thou shall require all your dependencies in an engine. -require "rails_warden" -require "ruby-srp" - -require "warden/session_serializer" -require "warden/strategies/secure_remote_password" - -require "webfinger" - -module LeapWebUsers - class Engine < ::Rails::Engine - - end -end diff --git a/users/lib/tasks/leap_web_users_tasks.rake b/users/lib/tasks/leap_web_users_tasks.rake deleted file mode 100644 index 62bcbe9..0000000 --- a/users/lib/tasks/leap_web_users_tasks.rake +++ /dev/null @@ -1,10 +0,0 @@ -# desc "Explaining what the task does" -# task :leap_web_users do -# # Task goes here -# end - -# recommended that for our setup, we should have this triggered from a cron job in puppet rather than using whenever gem -desc "Send one month warning messages" -task :leap_web_users do - User.send_one_month_warnings -end diff --git a/users/lib/warden/session_serializer.rb b/users/lib/warden/session_serializer.rb deleted file mode 100644 index 81d7076..0000000 --- a/users/lib/warden/session_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Warden - # Setup Session Serialization - class SessionSerializer - def serialize(record) - [record.class.name, record.id] - end - - def deserialize(keys) - klass, id = keys - klass.constantize.find(id) - end - end -end diff --git a/users/lib/warden/strategies/secure_remote_password.rb b/users/lib/warden/strategies/secure_remote_password.rb deleted file mode 100644 index 2c334c6..0000000 --- a/users/lib/warden/strategies/secure_remote_password.rb +++ /dev/null @@ -1,81 +0,0 @@ -module Warden - module Strategies - class SecureRemotePassword < Warden::Strategies::Base - - def valid? - handshake? || authentication? - end - - def authenticate! - if authentication? - validate! - else # handshake - initialize! - end - end - - protected - - def handshake? - params['A'] && params['login'] - end - - def authentication? - params['client_auth'] && session[:handshake] - end - - def validate! - if client = validate - success!(User.find_by_login(client.username)) - else - Rails.logger.warn "Login attempt failed." - Rails.logger.debug debug_info - Rails.logger.debug "Received: #{params['client_auth']}" - session.delete(:handshake) - fail!(:base => "invalid_user_pass") - end - end - - def validate - session[:handshake].authenticate(params['client_auth']) - end - - def initialize! - if user = User.find_by_login(id) - client = SRP::Client.new user.username, - :verifier => user.verifier, - :salt => user.salt - session[:handshake] = SRP::Session.new(client, params['A']) - custom! json_response(session[:handshake]) - else - fail! :base => 'invalid_user_pass' - end - rescue SRP::InvalidEphemeral - fail!(:base => "invalid_ephemeral") - end - - def json_response(object) - [ 200, - {"Content-Type" => "application/json; charset=utf-8"}, - [object.to_json] - ] - end - - def id - params["id"] || params["login"] - end - - protected - - def debug_info - JSON.pretty_generate(session[:handshake].internal_state) - end - - end - end - Warden::Strategies.add :secure_remote_password, - Warden::Strategies::SecureRemotePassword - -end - - diff --git a/users/lib/webfinger.rb b/users/lib/webfinger.rb deleted file mode 100644 index dd49b41..0000000 --- a/users/lib/webfinger.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Webfinger - - autoload :HostMetaPresenter, 'webfinger/host_meta_presenter' - autoload :UserPresenter, 'webfinger/user_presenter' - -end diff --git a/users/lib/webfinger/host_meta_presenter.rb b/users/lib/webfinger/host_meta_presenter.rb deleted file mode 100644 index 84ab7a9..0000000 --- a/users/lib/webfinger/host_meta_presenter.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'uri' - -class Webfinger::HostMetaPresenter - def initialize(request) - @request = request - end - - def to_json(options = {}) - { - subject: subject, - links: links - }.to_json(options) - end - - def subject - url = URI.parse(@request.url) - url.path = '' - url.to_s - end - - def links - { lrdd: { type: 'application/xrd+xml', template: webfinger_template } } - end - - protected - - def webfinger_template(path = 'webfinger', query_param='q') - "#{subject}/#{path}?#{query_param}={uri}" - end -end diff --git a/users/lib/webfinger/user_presenter.rb b/users/lib/webfinger/user_presenter.rb deleted file mode 100644 index 329f477..0000000 --- a/users/lib/webfinger/user_presenter.rb +++ /dev/null @@ -1,35 +0,0 @@ -class Webfinger::UserPresenter - include Rails.application.routes.url_helpers - attr_accessor :user - - def initialize(user, request) - @user = user - @request = request - end - - def to_json(options = {}) - { - subject: subject, - links: links - }.to_json(options) - end - - def subject - "acct:#{@user.email_address}" - end - - def links - links = {} - links[:public_key] = { type: 'PGP', href: key } if key - return links - end - - protected - - def key - if @user.public_key.present? - Base64.encode64(@user.public_key.to_s) - end - end - -end diff --git a/users/script/rails b/users/script/rails deleted file mode 100755 index ee08520..0000000 --- a/users/script/rails +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby1.8 -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/leap_web_users/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' -- cgit v1.2.3 From 20197129459d90642c50c27e601ef13ece4a873b Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 14:30:57 +0200 Subject: only load */test/factories from test/factories prevent recursive loadign of test/factories.rb ** can be empty. --- test/factories.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/factories.rb b/test/factories.rb index 98bb39b..980e2aa 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -1,4 +1,4 @@ -Dir.glob(Rails.root.join('**','test','factories.rb')) do |factory_file| +Dir.glob(Rails.root.join('*','test','factories.rb')) do |factory_file| require factory_file end FactoryGirl.define do -- cgit v1.2.3 From c1486cb9688d53c5ae266ff22ab279ead12eaa36 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Apr 2014 12:45:21 +0200 Subject: move certs into toplevel cleaned up all the engine stuff that was never really used. Afterwards there is not that much left that makes it into the toplevel. --- Gemfile | 1 - Gemfile.lock | 7 -- app/controllers/v1/certs_controller.rb | 50 +++++++++ app/models/client_certificate.rb | 113 +++++++++++++++++++++ certs/Gemfile | 14 --- certs/Rakefile | 44 -------- certs/Readme.md | 9 -- certs/app/assets/images/leap_web_certs/.gitkeep | 0 .../app/assets/javascripts/leap_web_certs/.gitkeep | 0 .../app/assets/stylesheets/leap_web_certs/.gitkeep | 0 certs/app/controllers/.gitkeep | 0 certs/app/controllers/certs_controller.rb | 50 --------- certs/app/helpers/.gitkeep | 0 certs/app/helpers/certs_helper.rb | 2 - certs/app/mailers/.gitkeep | 0 certs/app/models/.gitkeep | 0 certs/app/models/client_certificate.rb | 113 --------------------- certs/app/views/.gitkeep | 0 certs/config/locales/en.yml | 2 - certs/config/routes.rb | 5 - certs/leap_web_certs.gemspec | 20 ---- certs/lib/leap_web_certs.rb | 4 - certs/lib/leap_web_certs/engine.rb | 5 - certs/lib/tasks/leap_web_certs_tasks.rake | 4 - certs/script/rails | 8 -- certs/test/files/ca.crt | 15 --- certs/test/files/ca.key | 16 --- certs/test/functional/certs_controller_test.rb | 44 -------- certs/test/integration/navigation_test.rb | 9 -- certs/test/leap_web_certs_test.rb | 7 -- certs/test/test_helper.rb | 10 -- certs/test/unit/client_certificate_test.rb | 24 ----- config/defaults.yml | 4 +- config/routes.rb | 1 + leap_web.gemspec | 3 - test/files/ca.crt | 15 +++ test/files/ca.key | 16 +++ test/functional/v1/certs_controller_test.rb | 44 ++++++++ test/unit/client_certificate_test.rb | 24 +++++ 39 files changed, 265 insertions(+), 418 deletions(-) create mode 100644 app/controllers/v1/certs_controller.rb create mode 100644 app/models/client_certificate.rb delete mode 100644 certs/Gemfile delete mode 100644 certs/Rakefile delete mode 100644 certs/Readme.md delete mode 100644 certs/app/assets/images/leap_web_certs/.gitkeep delete mode 100644 certs/app/assets/javascripts/leap_web_certs/.gitkeep delete mode 100644 certs/app/assets/stylesheets/leap_web_certs/.gitkeep delete mode 100644 certs/app/controllers/.gitkeep delete mode 100644 certs/app/controllers/certs_controller.rb delete mode 100644 certs/app/helpers/.gitkeep delete mode 100644 certs/app/helpers/certs_helper.rb delete mode 100644 certs/app/mailers/.gitkeep delete mode 100644 certs/app/models/.gitkeep delete mode 100644 certs/app/models/client_certificate.rb delete mode 100644 certs/app/views/.gitkeep delete mode 100644 certs/config/locales/en.yml delete mode 100644 certs/config/routes.rb delete mode 100644 certs/leap_web_certs.gemspec delete mode 100644 certs/lib/leap_web_certs.rb delete mode 100644 certs/lib/leap_web_certs/engine.rb delete mode 100644 certs/lib/tasks/leap_web_certs_tasks.rake delete mode 100755 certs/script/rails delete mode 100644 certs/test/files/ca.crt delete mode 100644 certs/test/files/ca.key delete mode 100644 certs/test/functional/certs_controller_test.rb delete mode 100644 certs/test/integration/navigation_test.rb delete mode 100644 certs/test/leap_web_certs_test.rb delete mode 100644 certs/test/test_helper.rb delete mode 100644 certs/test/unit/client_certificate_test.rb create mode 100644 test/files/ca.crt create mode 100644 test/files/ca.key create mode 100644 test/functional/v1/certs_controller_test.rb create mode 100644 test/unit/client_certificate_test.rb diff --git a/Gemfile b/Gemfile index b19d119..50c4b00 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,6 @@ gem "json" gem "ruby-srp", "~> 0.2.1" gem "rails_warden" -gem 'leap_web_certs', :path => 'certs' gem 'leap_web_help', :path => 'help' gem 'leap_web_billing', :path => 'billing' diff --git a/Gemfile.lock b/Gemfile.lock index 0967bd6..537dafd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,12 +11,6 @@ PATH leap_web_billing (0.5.0) braintree -PATH - remote: certs - specs: - leap_web_certs (0.5.0) - certificate_authority (>= 0.2.0) - PATH remote: help specs: @@ -265,7 +259,6 @@ DEPENDENCIES kaminari (= 0.13.0) launchy leap_web_billing! - leap_web_certs! leap_web_help! minitest-stub-const mocha (~> 0.13.0) diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb new file mode 100644 index 0000000..64cfa7f --- /dev/null +++ b/app/controllers/v1/certs_controller.rb @@ -0,0 +1,50 @@ +class V1::CertsController < ApplicationController + + before_filter :require_login, :unless => :anonymous_certs_allowed? + + # GET /cert + def show + @cert = ClientCertificate.new(:prefix => certificate_prefix) + render text: @cert.to_s, content_type: 'text/plain' + end + + protected + + def anonymous_certs_allowed? + APP_CONFIG[:allow_anonymous_certs] + end + # + # this is some temporary logic until we store the service level in the user db. + # + # better logic might look like this: + # + # if logged_in? + # service_level = user.service_level + # elsif allow_anonymous? + # service_level = service_levels[:anonymous] + # else + # service_level = nil + # end + # + # if service_level.bandwidth == 'limited' && allow_limited? + # prefix = limited + # elsif allow_unlimited? + # prefix = unlimited + # else + # prefix = nil + # end + # + def certificate_prefix + if logged_in? + if APP_CONFIG[:allow_unlimited_certs] + APP_CONFIG[:unlimited_cert_prefix] + elsif APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:limited_cert_prefix] + end + elsif !APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:unlimited_cert_prefix] + else + APP_CONFIG[:limited_cert_prefix] + end + end +end diff --git a/app/models/client_certificate.rb b/app/models/client_certificate.rb new file mode 100644 index 0000000..76b07a2 --- /dev/null +++ b/app/models/client_certificate.rb @@ -0,0 +1,113 @@ +# +# Model for certificates +# +# This file must be loaded after Config has been loaded. +# +require 'base64' +require 'digest/md5' +require 'openssl' +require 'certificate_authority' +require 'date' + +class ClientCertificate + + attr_accessor :key # the client private RSA key + attr_accessor :cert # the client x509 certificate, signed by the CA + + # + # generate the private key and client certificate + # + def initialize(options = {}) + cert = CertificateAuthority::Certificate.new + + # set subject + cert.subject.common_name = common_name(options[:prefix]) + + # set expiration + cert.not_before = yesterday + cert.not_after = months_from_yesterday(APP_CONFIG[:client_cert_lifespan]) + + # generate key + cert.serial_number.number = cert_serial_number + cert.key_material.generate_key(APP_CONFIG[:client_cert_bit_size]) + + # sign + cert.parent = ClientCertificate.root_ca + cert.sign! client_signing_profile + + self.key = cert.key_material.private_key + self.cert = cert + end + + def to_s + self.key.to_pem + self.cert.to_pem + end + + private + + def self.root_ca + @root_ca ||= begin + crt = File.read(APP_CONFIG[:client_ca_cert]) + key = File.read(APP_CONFIG[:client_ca_key]) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, APP_CONFIG[:ca_key_password]) + cert + end + end + + # + # For cert serial numbers, we need a non-colliding number less than 160 bits. + # md5 will do nicely, since there is no need for a secure hash, just a short one. + # (md5 is 128 bits) + # + def cert_serial_number + Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16) + end + + def common_name(prefix = nil) + [prefix, random_common_name].join + end + + # + # for the random common name, we need a text string that will be unique across all certs. + # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid + # + def random_common_name + cert_serial_number.to_s(36) + end + + def client_signing_profile + { + "digest" => APP_CONFIG[:client_cert_hash], + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature"] + }, + "extendedKeyUsage" => { + "usage" => ["clientAuth"] + } + } + } + end + + ## + ## TIME HELPERS + ## + ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet + ## are behind UTC. + ## + + def yesterday + t = Time.now - 24*60*60 + Time.utc t.year, t.month, t.day + end + + def months_from_yesterday(num) + t = yesterday + date = Date.new t.year, t.month, t.day + date = date >> num # >> is months in the future operator + Time.utc date.year, date.month, date.day + end + +end diff --git a/certs/Gemfile b/certs/Gemfile deleted file mode 100644 index 992f236..0000000 --- a/certs/Gemfile +++ /dev/null @@ -1,14 +0,0 @@ -source "https://rubygems.org" - -eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) - -# We require leap_web_core from here so we can use the path option. -gem "leap_web_core", :path => '../core' - -# Declare your gem's dependencies in leap_web_users.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. -gemspec - -# To use debugger -# gem 'ruby-debug' diff --git a/certs/Rakefile b/certs/Rakefile deleted file mode 100644 index 54ed86d..0000000 --- a/certs/Rakefile +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env rake - -require 'rake/packagetask' -require 'rubygems/package_task' - -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end -begin - require 'rdoc/task' -rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' - RDoc::Task = Rake::RDocTask -end - -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebCerts' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = eval(File.read('leap_web_certs.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - - -task :default => :test diff --git a/certs/Readme.md b/certs/Readme.md deleted file mode 100644 index 4ea8d9d..0000000 --- a/certs/Readme.md +++ /dev/null @@ -1,9 +0,0 @@ -LeapWebCerts -========= - - -Configuration -------------- - - -Currently LeapWebCerts falls back to handing out a cert in /config/cert if the cert pool is empty. You need to add that file in the application that includes this engine. diff --git a/certs/app/assets/images/leap_web_certs/.gitkeep b/certs/app/assets/images/leap_web_certs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/assets/javascripts/leap_web_certs/.gitkeep b/certs/app/assets/javascripts/leap_web_certs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/assets/stylesheets/leap_web_certs/.gitkeep b/certs/app/assets/stylesheets/leap_web_certs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/controllers/.gitkeep b/certs/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/controllers/certs_controller.rb b/certs/app/controllers/certs_controller.rb deleted file mode 100644 index 82cbc44..0000000 --- a/certs/app/controllers/certs_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -class CertsController < ApplicationController - - before_filter :require_login, :unless => :anonymous_certs_allowed? - - # GET /cert - def show - @cert = ClientCertificate.new(:prefix => certificate_prefix) - render text: @cert.to_s, content_type: 'text/plain' - end - - protected - - def anonymous_certs_allowed? - APP_CONFIG[:allow_anonymous_certs] - end - # - # this is some temporary logic until we store the service level in the user db. - # - # better logic might look like this: - # - # if logged_in? - # service_level = user.service_level - # elsif allow_anonymous? - # service_level = service_levels[:anonymous] - # else - # service_level = nil - # end - # - # if service_level.bandwidth == 'limited' && allow_limited? - # prefix = limited - # elsif allow_unlimited? - # prefix = unlimited - # else - # prefix = nil - # end - # - def certificate_prefix - if logged_in? - if APP_CONFIG[:allow_unlimited_certs] - APP_CONFIG[:unlimited_cert_prefix] - elsif APP_CONFIG[:allow_limited_certs] - APP_CONFIG[:limited_cert_prefix] - end - elsif !APP_CONFIG[:allow_limited_certs] - APP_CONFIG[:unlimited_cert_prefix] - else - APP_CONFIG[:limited_cert_prefix] - end - end -end diff --git a/certs/app/helpers/.gitkeep b/certs/app/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/helpers/certs_helper.rb b/certs/app/helpers/certs_helper.rb deleted file mode 100644 index 94e76b8..0000000 --- a/certs/app/helpers/certs_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module CertsHelper -end diff --git a/certs/app/mailers/.gitkeep b/certs/app/mailers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/models/.gitkeep b/certs/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/app/models/client_certificate.rb b/certs/app/models/client_certificate.rb deleted file mode 100644 index 76b07a2..0000000 --- a/certs/app/models/client_certificate.rb +++ /dev/null @@ -1,113 +0,0 @@ -# -# Model for certificates -# -# This file must be loaded after Config has been loaded. -# -require 'base64' -require 'digest/md5' -require 'openssl' -require 'certificate_authority' -require 'date' - -class ClientCertificate - - attr_accessor :key # the client private RSA key - attr_accessor :cert # the client x509 certificate, signed by the CA - - # - # generate the private key and client certificate - # - def initialize(options = {}) - cert = CertificateAuthority::Certificate.new - - # set subject - cert.subject.common_name = common_name(options[:prefix]) - - # set expiration - cert.not_before = yesterday - cert.not_after = months_from_yesterday(APP_CONFIG[:client_cert_lifespan]) - - # generate key - cert.serial_number.number = cert_serial_number - cert.key_material.generate_key(APP_CONFIG[:client_cert_bit_size]) - - # sign - cert.parent = ClientCertificate.root_ca - cert.sign! client_signing_profile - - self.key = cert.key_material.private_key - self.cert = cert - end - - def to_s - self.key.to_pem + self.cert.to_pem - end - - private - - def self.root_ca - @root_ca ||= begin - crt = File.read(APP_CONFIG[:client_ca_cert]) - key = File.read(APP_CONFIG[:client_ca_key]) - openssl_cert = OpenSSL::X509::Certificate.new(crt) - cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) - cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, APP_CONFIG[:ca_key_password]) - cert - end - end - - # - # For cert serial numbers, we need a non-colliding number less than 160 bits. - # md5 will do nicely, since there is no need for a secure hash, just a short one. - # (md5 is 128 bits) - # - def cert_serial_number - Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16) - end - - def common_name(prefix = nil) - [prefix, random_common_name].join - end - - # - # for the random common name, we need a text string that will be unique across all certs. - # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid - # - def random_common_name - cert_serial_number.to_s(36) - end - - def client_signing_profile - { - "digest" => APP_CONFIG[:client_cert_hash], - "extensions" => { - "keyUsage" => { - "usage" => ["digitalSignature"] - }, - "extendedKeyUsage" => { - "usage" => ["clientAuth"] - } - } - } - end - - ## - ## TIME HELPERS - ## - ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet - ## are behind UTC. - ## - - def yesterday - t = Time.now - 24*60*60 - Time.utc t.year, t.month, t.day - end - - def months_from_yesterday(num) - t = yesterday - date = Date.new t.year, t.month, t.day - date = date >> num # >> is months in the future operator - Time.utc date.year, date.month, date.day - end - -end diff --git a/certs/app/views/.gitkeep b/certs/app/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/certs/config/locales/en.yml b/certs/config/locales/en.yml deleted file mode 100644 index 18e4f47..0000000 --- a/certs/config/locales/en.yml +++ /dev/null @@ -1,2 +0,0 @@ -en: - cert_pool_empty: "Sorry the Cert pool is empty, please check back later." diff --git a/certs/config/routes.rb b/certs/config/routes.rb deleted file mode 100644 index cb97757..0000000 --- a/certs/config/routes.rb +++ /dev/null @@ -1,5 +0,0 @@ -Rails.application.routes.draw do - scope '/1' do - resource :cert, :only => [:show] - end -end diff --git a/certs/leap_web_certs.gemspec b/certs/leap_web_certs.gemspec deleted file mode 100644 index 87b5be4..0000000 --- a/certs/leap_web_certs.gemspec +++ /dev/null @@ -1,20 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "leap_web_certs" - s.version = LeapWeb::VERSION - s.authors = ["Azul"] - s.email = ["azul@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "Cert distribution for the leap platform" - s.description = "This plugin for the leap platform distributes certs for the EIP client. It fetches the certs from a pool in CouchDB that is filled by leap-ca." - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"] - s.test_files = Dir["test/**/*"] - - s.add_dependency "certificate_authority", [">= 0.2.0"] - -end diff --git a/certs/lib/leap_web_certs.rb b/certs/lib/leap_web_certs.rb deleted file mode 100644 index beb683d..0000000 --- a/certs/lib/leap_web_certs.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "leap_web_certs/engine" - -module LeapWebCerts -end diff --git a/certs/lib/leap_web_certs/engine.rb b/certs/lib/leap_web_certs/engine.rb deleted file mode 100644 index 33a446e..0000000 --- a/certs/lib/leap_web_certs/engine.rb +++ /dev/null @@ -1,5 +0,0 @@ -module LeapWebCerts - class Engine < ::Rails::Engine - - end -end diff --git a/certs/lib/tasks/leap_web_certs_tasks.rake b/certs/lib/tasks/leap_web_certs_tasks.rake deleted file mode 100644 index e8fb7ff..0000000 --- a/certs/lib/tasks/leap_web_certs_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :leap_web_certs do -# # Task goes here -# end diff --git a/certs/script/rails b/certs/script/rails deleted file mode 100755 index 616d3c9..0000000 --- a/certs/script/rails +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby1.8 -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/leap_web_certs/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' diff --git a/certs/test/files/ca.crt b/certs/test/files/ca.crt deleted file mode 100644 index 8393eee..0000000 --- a/certs/test/files/ca.crt +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICYDCCAcCgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MREwDwYDVQQKDAh0ZXN0 -IG9yZzESMBAGA1UECwwJdGVzdCB1bml0MRIwEAYDVQQDDAl0ZXN0IG5hbWUwIBcN -MTMwMjA1MDAwMDAwWhgPMjExMzAyMDUwMDAwMDBaMDsxETAPBgNVBAoMCHRlc3Qg -b3JnMRIwEAYDVQQLDAl0ZXN0IHVuaXQxEjAQBgNVBAMMCXRlc3QgbmFtZTCBqDAN -BgkqhkiG9w0BAQEFAAOBlgAwgZICgYoAx076Dz8zswvCLuz0HP3Y3PWOgFDo9+8o -H4uXRcTpd+yw+5B79xjtQ7ojQy2465Jq00nkzHI6V1otM2uvVVIOcNk0t1HEjmK0 -T/r96dDHc59YvVQ+XPrzuQ4t3iREy8IAPNbc3r29PVZkMdGpeSYxyY1mUKza4DcY -My4SVko9pcP8zJBD4bHgEa0CAwEAAaNgMF4wHQYDVR0OBBYEFOQ+d2EUwBpi93TJ -9AX4Okew5/UIMA4GA1UdDwEB/wQEAwICBDAMBgNVHRMEBTADAQH/MB8GA1UdIwQY -MBaAFOQ+d2EUwBpi93TJ9AX4Okew5/UIMA0GCSqGSIb3DQEBDQUAA4GKAJW9/39P -VbVjH9C7F0XMOpd9nWBe9NUoiw36ZFZw95dqfUm6j5f3nejWG4lEtyMFu5i5rAw6 -GdDSXmq4sUqWTaJmQmZyY+WggQR4UGWJ0I18HRDiPxuA++OfkGzA20Gmvk+CIw/J -QLHlVjLyyUwaA+EO88rEcdc9VnGL/Xgjh8C/PYH2DpWw/kJa ------END CERTIFICATE----- diff --git a/certs/test/files/ca.key b/certs/test/files/ca.key deleted file mode 100644 index 125997f..0000000 --- a/certs/test/files/ca.key +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIChAIBAAKBigDHTvoPPzOzC8Iu7PQc/djc9Y6AUOj37ygfi5dFxOl37LD7kHv3 -GO1DuiNDLbjrkmrTSeTMcjpXWi0za69VUg5w2TS3UcSOYrRP+v3p0Mdzn1i9VD5c -+vO5Di3eJETLwgA81tzevb09VmQx0al5JjHJjWZQrNrgNxgzLhJWSj2lw/zMkEPh -seARrQIDAQABAoGJIvn0HircOsaMfEmvCUtu/E/HgzMvvxrkMqz/jgnhYt9Rq8QO -TS29rY4D1C0473ZRcuTb1xkQrfWwSv7R1SpCSIGFo8obtGb0NjNaYGyQ0IrYDjk8 -H5kYFEY4X4oqFhgy3owewaZZLxLD336ARRj2HhsLzA+4nD/wF7Q+bggpuMdkM2Uj -tn12rIECRQ/XqIGF8jLw9IDMkr9kkfT+n03p8sOd4g7iSw0sknlzaZZpIDvibkyN -SDKM7VX4VQa7u58+sCF4ylwi0UQu7/VT7Smp4QJFDJSoEOKplBvaT9fTfdVKjE4P -QyCAWEsb6Up8KKswhtDqiWeFtktIvx1Mkxn25erLms3cUEBde//rwNB+6ItBR/N8 -4RlNAkUPLsc3Gn+7gmFQ7r3U3zViboON0B/wiWcUjJsQzR6zdoBCvg0+VwsOIniG -ubjbI1uZUGHHg/SYn4KQOm4DwlgF7aDkxQECRQjVZMEedlXxzLOdZvoHBuZHdT38 -F0Jn0rxXOaDQuy0eimBamS+r4vOWngr4Az3jRH15KMYMu9dyllX3z/R2uyrLVBc2 -TQJFBEHIjoMVgP2h+N6VUDgPOhnxnnLvowOtX23J1y2foKwfZrHH38LNcWmuaGUi -fz6EYeUO20D174GfhqB0j6yR50ejPjYD ------END RSA PRIVATE KEY----- diff --git a/certs/test/functional/certs_controller_test.rb b/certs/test/functional/certs_controller_test.rb deleted file mode 100644 index 503e74b..0000000 --- a/certs/test/functional/certs_controller_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'test_helper' - -class CertsControllerTest < ActionController::TestCase - - test "send limited cert without login" do - with_config allow_limited_certs: true, allow_anonymous_certs: true do - cert = stub :to_s => "limited cert" - ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:limited_cert_prefix]).returns(cert) - get :show - assert_response :success - assert_equal cert.to_s, @response.body - end - end - - test "send unlimited cert" do - with_config allow_unlimited_certs: true do - login - cert = stub :to_s => "unlimited cert" - ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:unlimited_cert_prefix]).returns(cert) - get :show - assert_response :success - assert_equal cert.to_s, @response.body - end - end - - test "login required if anonymous certs disabled" do - with_config allow_anonymous_certs: false do - get :show - assert_response :redirect - end - end - - test "send limited cert" do - with_config allow_limited_certs: true, allow_unlimited_certs: false do - login - cert = stub :to_s => "real cert" - ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:limited_cert_prefix]).returns(cert) - get :show - assert_response :success - assert_equal cert.to_s, @response.body - end - end - -end diff --git a/certs/test/integration/navigation_test.rb b/certs/test/integration/navigation_test.rb deleted file mode 100644 index eec8c0e..0000000 --- a/certs/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/certs/test/leap_web_certs_test.rb b/certs/test/leap_web_certs_test.rb deleted file mode 100644 index ee2058b..0000000 --- a/certs/test/leap_web_certs_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class LeapWebCertsTest < ActiveSupport::TestCase - test "truth" do - assert_kind_of Module, LeapWebCerts - end -end diff --git a/certs/test/test_helper.rb b/certs/test/test_helper.rb deleted file mode 100644 index f6b4eb8..0000000 --- a/certs/test/test_helper.rb +++ /dev/null @@ -1,10 +0,0 @@ -ENV["RAILS_ENV"] = "test" -require File.expand_path('../../../test/dummy/config/environment', __FILE__) -require 'rails/test_help' -require 'mocha/setup' - -Rails.backtrace_cleaner.remove_silencers! - -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - diff --git a/certs/test/unit/client_certificate_test.rb b/certs/test/unit/client_certificate_test.rb deleted file mode 100644 index 036e724..0000000 --- a/certs/test/unit/client_certificate_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'test_helper' - -class ClientCertificateTest < ActiveSupport::TestCase - - test "new cert has all we need" do - sample = ClientCertificate.new - assert sample.key - assert sample.cert - assert sample.to_s - end - - test "cert has configured prefix" do - prefix = "PREFIX" - sample = ClientCertificate.new(:prefix => prefix) - assert sample.cert.subject.common_name.starts_with?(prefix) - end - - test "cert issuer matches ca subject" do - sample = ClientCertificate.new - cert = OpenSSL::X509::Certificate.new(sample.cert.to_pem) - assert_equal ClientCertificate.root_ca.openssl_body.subject, cert.issuer - end - -end diff --git a/config/defaults.yml b/config/defaults.yml index cb18795..e7d0f5e 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -1,6 +1,6 @@ dev_ca: &dev_ca - client_ca_key: "./certs/test/files/ca.key" - client_ca_cert: "./certs/test/files/ca.crt" + client_ca_key: "./test/files/ca.key" + client_ca_cert: "./test/files/ca.crt" ca_key_password: nil cert_options: &cert_options diff --git a/config/routes.rb b/config/routes.rb index 05eca9b..b930bd1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ LeapWeb::Application.routes.draw do delete "logout" => "sessions#destroy", :as => "logout" resources :users, :only => [:create, :update, :destroy, :index] resources :messages, :only => [:index, :update] + resource :cert, :only => [:show] end scope "(:locale)", :locale => MATCH_LOCALE do diff --git a/leap_web.gemspec b/leap_web.gemspec index 44a30e0..125c930 100644 --- a/leap_web.gemspec +++ b/leap_web.gemspec @@ -19,7 +19,4 @@ Gem::Specification.new do |s| s.email = 'azul@leap.se' s.homepage = 'http://leap.se' - # s.add_dependency 'leap_web_core' - s.add_dependency 'leap_web_certs', LeapWeb::VERSION - s.add_dependency 'leap_web_users', LeapWeb::VERSION end diff --git a/test/files/ca.crt b/test/files/ca.crt new file mode 100644 index 0000000..8393eee --- /dev/null +++ b/test/files/ca.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICYDCCAcCgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MREwDwYDVQQKDAh0ZXN0 +IG9yZzESMBAGA1UECwwJdGVzdCB1bml0MRIwEAYDVQQDDAl0ZXN0IG5hbWUwIBcN +MTMwMjA1MDAwMDAwWhgPMjExMzAyMDUwMDAwMDBaMDsxETAPBgNVBAoMCHRlc3Qg +b3JnMRIwEAYDVQQLDAl0ZXN0IHVuaXQxEjAQBgNVBAMMCXRlc3QgbmFtZTCBqDAN +BgkqhkiG9w0BAQEFAAOBlgAwgZICgYoAx076Dz8zswvCLuz0HP3Y3PWOgFDo9+8o +H4uXRcTpd+yw+5B79xjtQ7ojQy2465Jq00nkzHI6V1otM2uvVVIOcNk0t1HEjmK0 +T/r96dDHc59YvVQ+XPrzuQ4t3iREy8IAPNbc3r29PVZkMdGpeSYxyY1mUKza4DcY +My4SVko9pcP8zJBD4bHgEa0CAwEAAaNgMF4wHQYDVR0OBBYEFOQ+d2EUwBpi93TJ +9AX4Okew5/UIMA4GA1UdDwEB/wQEAwICBDAMBgNVHRMEBTADAQH/MB8GA1UdIwQY +MBaAFOQ+d2EUwBpi93TJ9AX4Okew5/UIMA0GCSqGSIb3DQEBDQUAA4GKAJW9/39P +VbVjH9C7F0XMOpd9nWBe9NUoiw36ZFZw95dqfUm6j5f3nejWG4lEtyMFu5i5rAw6 +GdDSXmq4sUqWTaJmQmZyY+WggQR4UGWJ0I18HRDiPxuA++OfkGzA20Gmvk+CIw/J +QLHlVjLyyUwaA+EO88rEcdc9VnGL/Xgjh8C/PYH2DpWw/kJa +-----END CERTIFICATE----- diff --git a/test/files/ca.key b/test/files/ca.key new file mode 100644 index 0000000..125997f --- /dev/null +++ b/test/files/ca.key @@ -0,0 +1,16 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIChAIBAAKBigDHTvoPPzOzC8Iu7PQc/djc9Y6AUOj37ygfi5dFxOl37LD7kHv3 +GO1DuiNDLbjrkmrTSeTMcjpXWi0za69VUg5w2TS3UcSOYrRP+v3p0Mdzn1i9VD5c ++vO5Di3eJETLwgA81tzevb09VmQx0al5JjHJjWZQrNrgNxgzLhJWSj2lw/zMkEPh +seARrQIDAQABAoGJIvn0HircOsaMfEmvCUtu/E/HgzMvvxrkMqz/jgnhYt9Rq8QO +TS29rY4D1C0473ZRcuTb1xkQrfWwSv7R1SpCSIGFo8obtGb0NjNaYGyQ0IrYDjk8 +H5kYFEY4X4oqFhgy3owewaZZLxLD336ARRj2HhsLzA+4nD/wF7Q+bggpuMdkM2Uj +tn12rIECRQ/XqIGF8jLw9IDMkr9kkfT+n03p8sOd4g7iSw0sknlzaZZpIDvibkyN +SDKM7VX4VQa7u58+sCF4ylwi0UQu7/VT7Smp4QJFDJSoEOKplBvaT9fTfdVKjE4P +QyCAWEsb6Up8KKswhtDqiWeFtktIvx1Mkxn25erLms3cUEBde//rwNB+6ItBR/N8 +4RlNAkUPLsc3Gn+7gmFQ7r3U3zViboON0B/wiWcUjJsQzR6zdoBCvg0+VwsOIniG +ubjbI1uZUGHHg/SYn4KQOm4DwlgF7aDkxQECRQjVZMEedlXxzLOdZvoHBuZHdT38 +F0Jn0rxXOaDQuy0eimBamS+r4vOWngr4Az3jRH15KMYMu9dyllX3z/R2uyrLVBc2 +TQJFBEHIjoMVgP2h+N6VUDgPOhnxnnLvowOtX23J1y2foKwfZrHH38LNcWmuaGUi +fz6EYeUO20D174GfhqB0j6yR50ejPjYD +-----END RSA PRIVATE KEY----- diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/v1/certs_controller_test.rb new file mode 100644 index 0000000..2c70e52 --- /dev/null +++ b/test/functional/v1/certs_controller_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +class V1::CertsControllerTest < ActionController::TestCase + + test "send limited cert without login" do + with_config allow_limited_certs: true, allow_anonymous_certs: true do + cert = stub :to_s => "limited cert" + ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:limited_cert_prefix]).returns(cert) + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end + end + + test "send unlimited cert" do + with_config allow_unlimited_certs: true do + login + cert = stub :to_s => "unlimited cert" + ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:unlimited_cert_prefix]).returns(cert) + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end + end + + test "login required if anonymous certs disabled" do + with_config allow_anonymous_certs: false do + get :show + assert_response :redirect + end + end + + test "send limited cert" do + with_config allow_limited_certs: true, allow_unlimited_certs: false do + login + cert = stub :to_s => "real cert" + ClientCertificate.expects(:new).with(:prefix => APP_CONFIG[:limited_cert_prefix]).returns(cert) + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end + end + +end diff --git a/test/unit/client_certificate_test.rb b/test/unit/client_certificate_test.rb new file mode 100644 index 0000000..036e724 --- /dev/null +++ b/test/unit/client_certificate_test.rb @@ -0,0 +1,24 @@ +require 'test_helper' + +class ClientCertificateTest < ActiveSupport::TestCase + + test "new cert has all we need" do + sample = ClientCertificate.new + assert sample.key + assert sample.cert + assert sample.to_s + end + + test "cert has configured prefix" do + prefix = "PREFIX" + sample = ClientCertificate.new(:prefix => prefix) + assert sample.cert.subject.common_name.starts_with?(prefix) + end + + test "cert issuer matches ca subject" do + sample = ClientCertificate.new + cert = OpenSSL::X509::Certificate.new(sample.cert.to_pem) + assert_equal ClientCertificate.root_ca.openssl_body.subject, cert.issuer + end + +end -- cgit v1.2.3 From c6a22158c5bfb18fcd83434f92c55436fb15af23 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Apr 2014 17:22:22 +0200 Subject: bringing back srp js --- .gitmodules | 4 ++-- app/assets/javascripts/application.js | 1 - app/assets/javascripts/leap_web_users/.gitkeep | 0 app/assets/javascripts/srp | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 app/assets/javascripts/leap_web_users/.gitkeep create mode 160000 app/assets/javascripts/srp diff --git a/.gitmodules b/.gitmodules index 7884a42..0c28cd5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "users/app/assets/javascripts/srp"] - path = users/app/assets/javascripts/srp +[submodule "app/assets/javascripts/srp"] + path = app/assets/javascripts/srp url = https://leap.se/git/srp_js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 03a40da..ab07e1f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -20,4 +20,3 @@ //= require platform //= require tickets //= require users -//= require_tree . diff --git a/app/assets/javascripts/leap_web_users/.gitkeep b/app/assets/javascripts/leap_web_users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/assets/javascripts/srp b/app/assets/javascripts/srp new file mode 160000 index 0000000..8f33d32 --- /dev/null +++ b/app/assets/javascripts/srp @@ -0,0 +1 @@ +Subproject commit 8f33d32d40b1e21ae7fb9a92c78a275422af4217 -- cgit v1.2.3 From 361cdbbacc57b17c198489238282e786cc827efa Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 11 Apr 2014 09:31:16 +0200 Subject: make sure billing tests do not interfere with others they are still broken though. --- billing/config/initializers/braintree.rb | 8 -------- billing/test/integration/admin_customer_test.rb | 7 +------ billing/test/integration/customer_creation_test.rb | 7 +------ billing/test/integration/subscription_test.rb | 6 +----- billing/test/support/braintree_integration_test.rb | 18 ++++++++++++++++++ 5 files changed, 21 insertions(+), 25 deletions(-) create mode 100644 billing/test/support/braintree_integration_test.rb diff --git a/billing/config/initializers/braintree.rb b/billing/config/initializers/braintree.rb index c0c89e2..3d87f4c 100644 --- a/billing/config/initializers/braintree.rb +++ b/billing/config/initializers/braintree.rb @@ -8,14 +8,6 @@ else Braintree::Configuration.logger = Logger.new('log/braintree.log') end -# -# we use fake braintree in tests -# -if Rails.env.test? - require 'braintree_test_app' - Rails.application.config.middleware.use BraintreeTestApp -end - # # You can set these per environment in config/config.yml: # diff --git a/billing/test/integration/admin_customer_test.rb b/billing/test/integration/admin_customer_test.rb index 1b9953f..df92a0d 100644 --- a/billing/test/integration/admin_customer_test.rb +++ b/billing/test/integration/admin_customer_test.rb @@ -1,19 +1,14 @@ require 'test_helper' require 'fake_braintree' -require 'capybara/rails' -class AdminCustomerTest < ActionDispatch::IntegrationTest - include Warden::Test::Helpers - include Capybara::DSL +class AdminCustomerTest < BraintreeIntegrationTest setup do - Warden.test_mode! @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') @user = FactoryGirl.create(:user) end teardown do - Warden.test_reset! @user.destroy if @user @admin.destroy if @admin end diff --git a/billing/test/integration/customer_creation_test.rb b/billing/test/integration/customer_creation_test.rb index aabd9b6..90319a9 100644 --- a/billing/test/integration/customer_creation_test.rb +++ b/billing/test/integration/customer_creation_test.rb @@ -1,20 +1,15 @@ require 'test_helper' require 'fake_braintree' -require 'capybara/rails' -class CustomerCreationTest < ActionDispatch::IntegrationTest - include Warden::Test::Helpers - include Capybara::DSL +class CustomerCreationTest < BraintreeIntegrationTest setup do - Warden.test_mode! @user = FactoryGirl.create(:user) login_as @user end teardown do @user.destroy - Warden.test_reset! end # Let's test both steps together with capybara diff --git a/billing/test/integration/subscription_test.rb b/billing/test/integration/subscription_test.rb index 1473eb0..cd010bd 100644 --- a/billing/test/integration/subscription_test.rb +++ b/billing/test/integration/subscription_test.rb @@ -1,14 +1,11 @@ require 'test_helper' require 'fake_braintree' -require 'capybara/rails' -class SubscriptionTest < BrowserIntegrationTest - include Warden::Test::Helpers +class SubscriptionTest < BraintreeIntegrationTest include CustomerTestHelper include StubRecordHelper setup do - Warden.test_mode! @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') @customer = stub_customer @braintree_customer = @customer.braintree_customer @@ -19,7 +16,6 @@ class SubscriptionTest < BrowserIntegrationTest end teardown do - Warden.test_reset! @admin.destroy end diff --git a/billing/test/support/braintree_integration_test.rb b/billing/test/support/braintree_integration_test.rb new file mode 100644 index 0000000..976c5a2 --- /dev/null +++ b/billing/test/support/braintree_integration_test.rb @@ -0,0 +1,18 @@ +require 'capybara/rails' +# require 'fake_braintree' - messes up other integration tests +require 'braintree_test_app' + +class BraintreeIntegrationTest < BrowserIntegrationTest + include Warden::Test::Helpers + + setup do + Warden.test_mode! + Rails.application.config.middleware.use BraintreeTestApp + end + + teardown do + Warden.test_reset! + Rails.application.config.middleware.delete "BraintreeTestApp" + end + +end -- cgit v1.2.3 From 32136605ddd405a0bf47f3b795b22fd4b49465b5 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 11 Apr 2014 09:38:50 +0200 Subject: moving broken billing integration tests out of the way They are currently using fake_braintree. I think this is not a good approach for integration tests. It's a fake - we should test against braintrees test api. However that requires getting an api key that we want to keep outside the repository. So these test can only run on travis if we manage to setup secret values in .travis.yml - which has been failing so far. So for now i moved the broken billing integration tests to billing/test/broken to move on. --- billing/test/broken/admin_customer_test.rb | 31 ++++++++ billing/test/broken/customer_creation_test.rb | 84 ++++++++++++++++++++++ billing/test/broken/subscription_test.rb | 49 +++++++++++++ billing/test/integration/admin_customer_test.rb | 31 -------- billing/test/integration/customer_creation_test.rb | 84 ---------------------- billing/test/integration/subscription_test.rb | 49 ------------- 6 files changed, 164 insertions(+), 164 deletions(-) create mode 100644 billing/test/broken/admin_customer_test.rb create mode 100644 billing/test/broken/customer_creation_test.rb create mode 100644 billing/test/broken/subscription_test.rb delete mode 100644 billing/test/integration/admin_customer_test.rb delete mode 100644 billing/test/integration/customer_creation_test.rb delete mode 100644 billing/test/integration/subscription_test.rb diff --git a/billing/test/broken/admin_customer_test.rb b/billing/test/broken/admin_customer_test.rb new file mode 100644 index 0000000..df92a0d --- /dev/null +++ b/billing/test/broken/admin_customer_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' +require 'fake_braintree' + +class AdminCustomerTest < BraintreeIntegrationTest + + setup do + @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') + @user = FactoryGirl.create(:user) + end + + teardown do + @user.destroy if @user + @admin.destroy if @admin + end + + test "check non customer as admin" do + login_as @admin + visit '/' + click_link 'Users' + click_link @user.login + click_link 'Billing Settings' + assert page.has_content? @user.email_address + assert page.has_content? 'No Saved Customer' + end + + test "check customer as admin" do + skip "cannot check customer as admin" + # it would be good to have a test where an admin tries to view the 'Billing Settings' for another user. + # However, partially due to limitations of FakeBraintree, this doesn't seem pursuing at this time. + end +end diff --git a/billing/test/broken/customer_creation_test.rb b/billing/test/broken/customer_creation_test.rb new file mode 100644 index 0000000..90319a9 --- /dev/null +++ b/billing/test/broken/customer_creation_test.rb @@ -0,0 +1,84 @@ +require 'test_helper' +require 'fake_braintree' + +class CustomerCreationTest < BraintreeIntegrationTest + + setup do + @user = FactoryGirl.create(:user) + login_as @user + end + + teardown do + @user.destroy + end + + # Let's test both steps together with capybara + # + # This test is nice and clean but also a bit fragile: + # RackTest assumes all requests to be local. So we need + # BraintreeTestApp for the braintree transparent redirect to work. + # + # this mystifies me why this works. when i type the click_button line (and the + # customer.braintree_customer line) in the debugger, it gives a timeout, + # but it works fine embedded in the test. + test "create customer with braintree" do + visit '/' + click_link 'Billing Settings' + # i am a bit unclear why this works, as it seems there will be validation errors + assert_difference("Customer.count") do + click_button 'Save Payment Info' # this gives me a timeout + end + assert customer = Customer.find_by_user_id(@user.id) + assert customer.braintree_customer + end + + # We only test the confirmation here. + # The request to Braintree is triggered outside of rails + # In skippped test below, we see this works even if the attributes are + # for a broken customer + test "successfully confirms customer creation" do + response = post_transparent_redirect :create_customer_data, + customer: FactoryGirl.attributes_for(:braintree_customer), + redirect_url: confirm_customer_url + + assert_difference("Customer.count") do + post response['Location'] + end + + assert_equal 200, status + assert customer = Customer.find_by_user_id(@user.id) + assert customer.braintree_customer + end + + + test "failed customer creation" do + skip "cannot get customer creation to fail" + + FakeBraintree.decline_all_cards! + + response = post_transparent_redirect :create_customer_data, + customer: FactoryGirl.attributes_for(:broken_customer), + redirect_url: confirm_customer_url + + assert FakeBraintree.decline_all_cards? + assert_no_difference("Customer.count") do + post response['Location'] #this gives me a timeout when run alone + end + assert_nil Customer.find_by_user_id(@user.id) + + end + + def post_transparent_redirect(type, data) + params = data.dup + params[:tr_data] = Braintree::TransparentRedirect.send(type, params) + post_transparent_redirect_params(params) + end + + def post_transparent_redirect_params(params) + uri = URI.parse(Braintree::TransparentRedirect.url) + Net::HTTP.start(uri.host, uri.port) do |http| + http.post(uri.path, Rack::Utils.build_nested_query(params)) + end + end + +end diff --git a/billing/test/broken/subscription_test.rb b/billing/test/broken/subscription_test.rb new file mode 100644 index 0000000..cd010bd --- /dev/null +++ b/billing/test/broken/subscription_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' +require 'fake_braintree' + +class SubscriptionTest < BraintreeIntegrationTest + include CustomerTestHelper + include StubRecordHelper + + setup do + @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') + @customer = stub_customer + @braintree_customer = @customer.braintree_customer + response = Braintree::Subscription.create plan_id: '5', + payment_method_token: @braintree_customer.credit_cards.first.token, + price: '10' + @subscription = response.subscription + end + + teardown do + @admin.destroy + end + + test "admin can see all subscriptions for another" do + login_as @admin + @customer.stubs(:subscriptions).returns([@subscription]) + @subscription.stubs(:balance).returns 0 + visit user_subscriptions_path(@customer.user_id, :locale => nil) + assert page.has_content?("Subscriptions") + assert page.has_content?("Status: Active") + end + + # test "user cannot see all subscriptions for other user" do + #end + + #test "admin cannot add subscription for another" do + #end + + #test "authenticated user can cancel own subscription" do + #end + + #test "user cannot add subscription if they have active one" do + #end + + #test "user can view own subscriptions" + #end + + #test "admin can view another user's subscriptions" do + #end + +end diff --git a/billing/test/integration/admin_customer_test.rb b/billing/test/integration/admin_customer_test.rb deleted file mode 100644 index df92a0d..0000000 --- a/billing/test/integration/admin_customer_test.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class AdminCustomerTest < BraintreeIntegrationTest - - setup do - @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') - @user = FactoryGirl.create(:user) - end - - teardown do - @user.destroy if @user - @admin.destroy if @admin - end - - test "check non customer as admin" do - login_as @admin - visit '/' - click_link 'Users' - click_link @user.login - click_link 'Billing Settings' - assert page.has_content? @user.email_address - assert page.has_content? 'No Saved Customer' - end - - test "check customer as admin" do - skip "cannot check customer as admin" - # it would be good to have a test where an admin tries to view the 'Billing Settings' for another user. - # However, partially due to limitations of FakeBraintree, this doesn't seem pursuing at this time. - end -end diff --git a/billing/test/integration/customer_creation_test.rb b/billing/test/integration/customer_creation_test.rb deleted file mode 100644 index 90319a9..0000000 --- a/billing/test/integration/customer_creation_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class CustomerCreationTest < BraintreeIntegrationTest - - setup do - @user = FactoryGirl.create(:user) - login_as @user - end - - teardown do - @user.destroy - end - - # Let's test both steps together with capybara - # - # This test is nice and clean but also a bit fragile: - # RackTest assumes all requests to be local. So we need - # BraintreeTestApp for the braintree transparent redirect to work. - # - # this mystifies me why this works. when i type the click_button line (and the - # customer.braintree_customer line) in the debugger, it gives a timeout, - # but it works fine embedded in the test. - test "create customer with braintree" do - visit '/' - click_link 'Billing Settings' - # i am a bit unclear why this works, as it seems there will be validation errors - assert_difference("Customer.count") do - click_button 'Save Payment Info' # this gives me a timeout - end - assert customer = Customer.find_by_user_id(@user.id) - assert customer.braintree_customer - end - - # We only test the confirmation here. - # The request to Braintree is triggered outside of rails - # In skippped test below, we see this works even if the attributes are - # for a broken customer - test "successfully confirms customer creation" do - response = post_transparent_redirect :create_customer_data, - customer: FactoryGirl.attributes_for(:braintree_customer), - redirect_url: confirm_customer_url - - assert_difference("Customer.count") do - post response['Location'] - end - - assert_equal 200, status - assert customer = Customer.find_by_user_id(@user.id) - assert customer.braintree_customer - end - - - test "failed customer creation" do - skip "cannot get customer creation to fail" - - FakeBraintree.decline_all_cards! - - response = post_transparent_redirect :create_customer_data, - customer: FactoryGirl.attributes_for(:broken_customer), - redirect_url: confirm_customer_url - - assert FakeBraintree.decline_all_cards? - assert_no_difference("Customer.count") do - post response['Location'] #this gives me a timeout when run alone - end - assert_nil Customer.find_by_user_id(@user.id) - - end - - def post_transparent_redirect(type, data) - params = data.dup - params[:tr_data] = Braintree::TransparentRedirect.send(type, params) - post_transparent_redirect_params(params) - end - - def post_transparent_redirect_params(params) - uri = URI.parse(Braintree::TransparentRedirect.url) - Net::HTTP.start(uri.host, uri.port) do |http| - http.post(uri.path, Rack::Utils.build_nested_query(params)) - end - end - -end diff --git a/billing/test/integration/subscription_test.rb b/billing/test/integration/subscription_test.rb deleted file mode 100644 index cd010bd..0000000 --- a/billing/test/integration/subscription_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class SubscriptionTest < BraintreeIntegrationTest - include CustomerTestHelper - include StubRecordHelper - - setup do - @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') - @customer = stub_customer - @braintree_customer = @customer.braintree_customer - response = Braintree::Subscription.create plan_id: '5', - payment_method_token: @braintree_customer.credit_cards.first.token, - price: '10' - @subscription = response.subscription - end - - teardown do - @admin.destroy - end - - test "admin can see all subscriptions for another" do - login_as @admin - @customer.stubs(:subscriptions).returns([@subscription]) - @subscription.stubs(:balance).returns 0 - visit user_subscriptions_path(@customer.user_id, :locale => nil) - assert page.has_content?("Subscriptions") - assert page.has_content?("Status: Active") - end - - # test "user cannot see all subscriptions for other user" do - #end - - #test "admin cannot add subscription for another" do - #end - - #test "authenticated user can cancel own subscription" do - #end - - #test "user cannot add subscription if they have active one" do - #end - - #test "user can view own subscriptions" - #end - - #test "admin can view another user's subscriptions" do - #end - -end -- cgit v1.2.3 From 636692f9921bd695d726695d2d46c91f5a6e56f3 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 11 Apr 2014 10:03:19 +0200 Subject: move engines into engines directory Also renamed help to support so it's harder to confuse it with documentation --- Gemfile | 4 +- Gemfile.lock | 4 +- billing/Gemfile | 23 -- billing/README.md | 49 ---- billing/Rakefile | 40 --- .../app/controllers/billing_admin_controller.rb | 29 --- billing/app/controllers/billing_base_controller.rb | 22 -- .../app/controllers/credit_card_info_controller.rb | 35 --- billing/app/controllers/customer_controller.rb | 64 ----- billing/app/controllers/payments_controller.rb | 34 --- .../app/controllers/subscriptions_controller.rb | 63 ----- billing/app/helpers/billing_helper.rb | 51 ---- billing/app/helpers/braintree_form_helper.rb | 64 ----- billing/app/helpers/braintree_helper.rb | 5 - billing/app/models/customer.rb | 58 ----- billing/app/views/billing_admin/show.html.haml | 7 - .../app/views/credit_card_info/confirm.html.haml | 5 - billing/app/views/credit_card_info/edit.html.haml | 17 -- .../app/views/customer/_customer_data.html.haml | 16 -- billing/app/views/customer/_transaction.html.haml | 0 billing/app/views/customer/confirm.html.haml | 14 -- billing/app/views/customer/edit.html.haml | 23 -- billing/app/views/customer/new.html.haml | 24 -- billing/app/views/customer/show.html.haml | 27 -- .../views/payments/_non_customer_fields.html.haml | 16 -- .../views/payments/_transaction_details.html.haml | 15 -- billing/app/views/payments/confirm.html.haml | 26 -- billing/app/views/payments/index.html.haml | 5 - billing/app/views/payments/new.html.haml | 17 -- .../subscriptions/_subscription_details.html.haml | 26 -- billing/app/views/subscriptions/create.html.haml | 9 - billing/app/views/subscriptions/destroy.html.haml | 7 - billing/app/views/subscriptions/index.html.haml | 8 - billing/app/views/subscriptions/new.html.haml | 15 -- billing/app/views/subscriptions/show.html.haml | 6 - billing/config/initializers/braintree.rb | 23 -- billing/config/locales/en.yml | 11 - billing/config/routes.rb | 25 -- billing/leap_web_billing.gemspec | 21 -- billing/lib/braintree_test_app.rb | 36 --- billing/lib/leap_web_billing.rb | 4 - billing/lib/leap_web_billing/engine.rb | 10 - billing/script/rails | 8 - billing/test/broken/admin_customer_test.rb | 31 --- billing/test/broken/customer_creation_test.rb | 84 ------- billing/test/broken/subscription_test.rb | 49 ---- billing/test/factories.rb | 25 -- .../test/functional/customer_controller_test.rb | 124 ---------- .../test/functional/customers_controller_test.rb | 61 ----- .../test/functional/payments_controller_test.rb | 50 ---- .../functional/subscriptions_controller_test.rb | 16 -- billing/test/support/braintree_integration_test.rb | 18 -- billing/test/support/customer_test_helper.rb | 11 - billing/test/test_helper.rb | 15 -- billing/test/unit/customer_test.rb | 38 --- .../test/unit/customer_with_payment_info_test.rb | 40 --- engines/billing/Gemfile | 23 ++ engines/billing/README.md | 49 ++++ engines/billing/Rakefile | 40 +++ .../app/controllers/billing_admin_controller.rb | 29 +++ .../app/controllers/billing_base_controller.rb | 22 ++ .../app/controllers/credit_card_info_controller.rb | 35 +++ .../billing/app/controllers/customer_controller.rb | 64 +++++ .../billing/app/controllers/payments_controller.rb | 34 +++ .../app/controllers/subscriptions_controller.rb | 63 +++++ engines/billing/app/helpers/billing_helper.rb | 51 ++++ .../billing/app/helpers/braintree_form_helper.rb | 64 +++++ engines/billing/app/helpers/braintree_helper.rb | 5 + engines/billing/app/models/customer.rb | 58 +++++ .../billing/app/views/billing_admin/show.html.haml | 7 + .../app/views/credit_card_info/confirm.html.haml | 5 + .../app/views/credit_card_info/edit.html.haml | 17 ++ .../app/views/customer/_customer_data.html.haml | 16 ++ .../app/views/customer/_transaction.html.haml | 0 .../billing/app/views/customer/confirm.html.haml | 14 ++ engines/billing/app/views/customer/edit.html.haml | 23 ++ engines/billing/app/views/customer/new.html.haml | 24 ++ engines/billing/app/views/customer/show.html.haml | 27 ++ .../views/payments/_non_customer_fields.html.haml | 16 ++ .../views/payments/_transaction_details.html.haml | 15 ++ .../billing/app/views/payments/confirm.html.haml | 26 ++ engines/billing/app/views/payments/index.html.haml | 5 + engines/billing/app/views/payments/new.html.haml | 17 ++ .../subscriptions/_subscription_details.html.haml | 26 ++ .../app/views/subscriptions/create.html.haml | 9 + .../app/views/subscriptions/destroy.html.haml | 7 + .../app/views/subscriptions/index.html.haml | 8 + .../billing/app/views/subscriptions/new.html.haml | 15 ++ .../billing/app/views/subscriptions/show.html.haml | 6 + engines/billing/config/initializers/braintree.rb | 23 ++ engines/billing/config/locales/en.yml | 11 + engines/billing/config/routes.rb | 25 ++ engines/billing/leap_web_billing.gemspec | 21 ++ engines/billing/lib/braintree_test_app.rb | 36 +++ engines/billing/lib/leap_web_billing.rb | 4 + engines/billing/lib/leap_web_billing/engine.rb | 10 + engines/billing/script/rails | 8 + engines/billing/test/broken/admin_customer_test.rb | 31 +++ .../billing/test/broken/customer_creation_test.rb | 84 +++++++ engines/billing/test/broken/subscription_test.rb | 49 ++++ engines/billing/test/factories.rb | 25 ++ .../test/functional/customer_controller_test.rb | 124 ++++++++++ .../test/functional/customers_controller_test.rb | 61 +++++ .../test/functional/payments_controller_test.rb | 50 ++++ .../functional/subscriptions_controller_test.rb | 16 ++ .../test/support/braintree_integration_test.rb | 18 ++ .../billing/test/support/customer_test_helper.rb | 11 + engines/billing/test/test_helper.rb | 15 ++ engines/billing/test/unit/customer_test.rb | 38 +++ .../test/unit/customer_with_payment_info_test.rb | 40 +++ engines/support/Gemfile | 15 ++ engines/support/README.md | 1 + engines/support/Rakefile | 44 ++++ engines/support/app/assets/javascripts/tickets.js | 4 + .../support/app/controllers/tickets_controller.rb | 153 ++++++++++++ .../app/designs/ticket/by_includes_post_by.js | 13 + .../ticket/by_includes_post_by_and_created_at.js | 12 + ..._includes_post_by_and_is_open_and_created_at.js | 12 + ..._includes_post_by_and_is_open_and_updated_at.js | 12 + .../ticket/by_includes_post_by_and_updated_at.js | 12 + .../app/helpers/auto_tickets_path_helper.rb | 53 ++++ engines/support/app/helpers/tickets_helper.rb | 76 ++++++ .../app/models/account_extension/tickets.rb | 13 + engines/support/app/models/ticket.rb | 105 ++++++++ engines/support/app/models/ticket_comment.rb | 43 ++++ engines/support/app/models/ticket_selection.rb | 71 ++++++ .../support/app/views/tickets/_comment.html.haml | 20 ++ .../support/app/views/tickets/_edit_form.html.haml | 48 ++++ .../app/views/tickets/_new_comment_form.html.haml | 13 + engines/support/app/views/tickets/_tabs.html.haml | 23 ++ .../support/app/views/tickets/_ticket.html.haml | 6 + engines/support/app/views/tickets/index.html.haml | 19 ++ engines/support/app/views/tickets/new.html.haml | 30 +++ engines/support/app/views/tickets/show.html.haml | 12 + .../config/initializers/account_lifecycle.rb | 3 + engines/support/config/locales/en.yml | 22 ++ engines/support/config/routes.rb | 8 + engines/support/leap_web_help.gemspec | 18 ++ engines/support/lib/leap_web_help.rb | 4 + engines/support/lib/leap_web_help/engine.rb | 4 + engines/support/lib/tasks/leap_web_help_tasks.rake | 4 + engines/support/script/rails | 8 + engines/support/test/factories.rb | 18 ++ .../test/functional/tickets_controller_test.rb | 273 +++++++++++++++++++++ .../support/test/integration/navigation_test.rb | 9 + engines/support/test/leap_web_help_test.rb | 7 + engines/support/test/test_helper.rb | 15 ++ .../support/test/unit/account_extension_test.rb | 12 + engines/support/test/unit/ticket_comment_test.rb | 59 +++++ engines/support/test/unit/ticket_test.rb | 88 +++++++ help/Gemfile | 15 -- help/README.md | 1 - help/Rakefile | 44 ---- help/app/assets/javascripts/tickets.js | 4 - help/app/controllers/tickets_controller.rb | 153 ------------ help/app/designs/ticket/by_includes_post_by.js | 13 - .../ticket/by_includes_post_by_and_created_at.js | 12 - ..._includes_post_by_and_is_open_and_created_at.js | 12 - ..._includes_post_by_and_is_open_and_updated_at.js | 12 - .../ticket/by_includes_post_by_and_updated_at.js | 12 - help/app/helpers/auto_tickets_path_helper.rb | 53 ---- help/app/helpers/tickets_helper.rb | 76 ------ help/app/models/account_extension/tickets.rb | 13 - help/app/models/ticket.rb | 105 -------- help/app/models/ticket_comment.rb | 43 ---- help/app/models/ticket_selection.rb | 71 ------ help/app/views/tickets/_comment.html.haml | 20 -- help/app/views/tickets/_edit_form.html.haml | 48 ---- help/app/views/tickets/_new_comment_form.html.haml | 13 - help/app/views/tickets/_tabs.html.haml | 23 -- help/app/views/tickets/_ticket.html.haml | 6 - help/app/views/tickets/index.html.haml | 19 -- help/app/views/tickets/new.html.haml | 30 --- help/app/views/tickets/show.html.haml | 12 - help/config/initializers/account_lifecycle.rb | 3 - help/config/locales/en.yml | 22 -- help/config/routes.rb | 8 - help/leap_web_help.gemspec | 18 -- help/lib/leap_web_help.rb | 4 - help/lib/leap_web_help/engine.rb | 4 - help/lib/tasks/leap_web_help_tasks.rake | 4 - help/script/rails | 8 - help/test/factories.rb | 18 -- help/test/functional/tickets_controller_test.rb | 273 --------------------- help/test/integration/navigation_test.rb | 9 - help/test/leap_web_help_test.rb | 7 - help/test/test_helper.rb | 15 -- help/test/unit/account_extension_test.rb | 12 - help/test/unit/ticket_comment_test.rb | 59 ----- help/test/unit/ticket_test.rb | 88 ------- lib/extensions/couchrest.rb | 2 +- lib/tasks/test.rake | 19 +- test/factories.rb | 4 +- test/test_helper.rb | 2 +- 194 files changed, 2897 insertions(+), 2902 deletions(-) delete mode 100644 billing/Gemfile delete mode 100644 billing/README.md delete mode 100644 billing/Rakefile delete mode 100644 billing/app/controllers/billing_admin_controller.rb delete mode 100644 billing/app/controllers/billing_base_controller.rb delete mode 100644 billing/app/controllers/credit_card_info_controller.rb delete mode 100644 billing/app/controllers/customer_controller.rb delete mode 100644 billing/app/controllers/payments_controller.rb delete mode 100644 billing/app/controllers/subscriptions_controller.rb delete mode 100644 billing/app/helpers/billing_helper.rb delete mode 100644 billing/app/helpers/braintree_form_helper.rb delete mode 100644 billing/app/helpers/braintree_helper.rb delete mode 100644 billing/app/models/customer.rb delete mode 100644 billing/app/views/billing_admin/show.html.haml delete mode 100644 billing/app/views/credit_card_info/confirm.html.haml delete mode 100644 billing/app/views/credit_card_info/edit.html.haml delete mode 100644 billing/app/views/customer/_customer_data.html.haml delete mode 100644 billing/app/views/customer/_transaction.html.haml delete mode 100644 billing/app/views/customer/confirm.html.haml delete mode 100644 billing/app/views/customer/edit.html.haml delete mode 100644 billing/app/views/customer/new.html.haml delete mode 100644 billing/app/views/customer/show.html.haml delete mode 100644 billing/app/views/payments/_non_customer_fields.html.haml delete mode 100644 billing/app/views/payments/_transaction_details.html.haml delete mode 100644 billing/app/views/payments/confirm.html.haml delete mode 100644 billing/app/views/payments/index.html.haml delete mode 100644 billing/app/views/payments/new.html.haml delete mode 100644 billing/app/views/subscriptions/_subscription_details.html.haml delete mode 100644 billing/app/views/subscriptions/create.html.haml delete mode 100644 billing/app/views/subscriptions/destroy.html.haml delete mode 100644 billing/app/views/subscriptions/index.html.haml delete mode 100644 billing/app/views/subscriptions/new.html.haml delete mode 100644 billing/app/views/subscriptions/show.html.haml delete mode 100644 billing/config/initializers/braintree.rb delete mode 100644 billing/config/locales/en.yml delete mode 100644 billing/config/routes.rb delete mode 100644 billing/leap_web_billing.gemspec delete mode 100644 billing/lib/braintree_test_app.rb delete mode 100644 billing/lib/leap_web_billing.rb delete mode 100644 billing/lib/leap_web_billing/engine.rb delete mode 100755 billing/script/rails delete mode 100644 billing/test/broken/admin_customer_test.rb delete mode 100644 billing/test/broken/customer_creation_test.rb delete mode 100644 billing/test/broken/subscription_test.rb delete mode 100644 billing/test/factories.rb delete mode 100644 billing/test/functional/customer_controller_test.rb delete mode 100644 billing/test/functional/customers_controller_test.rb delete mode 100644 billing/test/functional/payments_controller_test.rb delete mode 100644 billing/test/functional/subscriptions_controller_test.rb delete mode 100644 billing/test/support/braintree_integration_test.rb delete mode 100644 billing/test/support/customer_test_helper.rb delete mode 100644 billing/test/test_helper.rb delete mode 100644 billing/test/unit/customer_test.rb delete mode 100644 billing/test/unit/customer_with_payment_info_test.rb create mode 100644 engines/billing/Gemfile create mode 100644 engines/billing/README.md create mode 100644 engines/billing/Rakefile create mode 100644 engines/billing/app/controllers/billing_admin_controller.rb create mode 100644 engines/billing/app/controllers/billing_base_controller.rb create mode 100644 engines/billing/app/controllers/credit_card_info_controller.rb create mode 100644 engines/billing/app/controllers/customer_controller.rb create mode 100644 engines/billing/app/controllers/payments_controller.rb create mode 100644 engines/billing/app/controllers/subscriptions_controller.rb create mode 100644 engines/billing/app/helpers/billing_helper.rb create mode 100644 engines/billing/app/helpers/braintree_form_helper.rb create mode 100644 engines/billing/app/helpers/braintree_helper.rb create mode 100644 engines/billing/app/models/customer.rb create mode 100644 engines/billing/app/views/billing_admin/show.html.haml create mode 100644 engines/billing/app/views/credit_card_info/confirm.html.haml create mode 100644 engines/billing/app/views/credit_card_info/edit.html.haml create mode 100644 engines/billing/app/views/customer/_customer_data.html.haml create mode 100644 engines/billing/app/views/customer/_transaction.html.haml create mode 100644 engines/billing/app/views/customer/confirm.html.haml create mode 100644 engines/billing/app/views/customer/edit.html.haml create mode 100644 engines/billing/app/views/customer/new.html.haml create mode 100644 engines/billing/app/views/customer/show.html.haml create mode 100644 engines/billing/app/views/payments/_non_customer_fields.html.haml create mode 100644 engines/billing/app/views/payments/_transaction_details.html.haml create mode 100644 engines/billing/app/views/payments/confirm.html.haml create mode 100644 engines/billing/app/views/payments/index.html.haml create mode 100644 engines/billing/app/views/payments/new.html.haml create mode 100644 engines/billing/app/views/subscriptions/_subscription_details.html.haml create mode 100644 engines/billing/app/views/subscriptions/create.html.haml create mode 100644 engines/billing/app/views/subscriptions/destroy.html.haml create mode 100644 engines/billing/app/views/subscriptions/index.html.haml create mode 100644 engines/billing/app/views/subscriptions/new.html.haml create mode 100644 engines/billing/app/views/subscriptions/show.html.haml create mode 100644 engines/billing/config/initializers/braintree.rb create mode 100644 engines/billing/config/locales/en.yml create mode 100644 engines/billing/config/routes.rb create mode 100644 engines/billing/leap_web_billing.gemspec create mode 100644 engines/billing/lib/braintree_test_app.rb create mode 100644 engines/billing/lib/leap_web_billing.rb create mode 100644 engines/billing/lib/leap_web_billing/engine.rb create mode 100755 engines/billing/script/rails create mode 100644 engines/billing/test/broken/admin_customer_test.rb create mode 100644 engines/billing/test/broken/customer_creation_test.rb create mode 100644 engines/billing/test/broken/subscription_test.rb create mode 100644 engines/billing/test/factories.rb create mode 100644 engines/billing/test/functional/customer_controller_test.rb create mode 100644 engines/billing/test/functional/customers_controller_test.rb create mode 100644 engines/billing/test/functional/payments_controller_test.rb create mode 100644 engines/billing/test/functional/subscriptions_controller_test.rb create mode 100644 engines/billing/test/support/braintree_integration_test.rb create mode 100644 engines/billing/test/support/customer_test_helper.rb create mode 100644 engines/billing/test/test_helper.rb create mode 100644 engines/billing/test/unit/customer_test.rb create mode 100644 engines/billing/test/unit/customer_with_payment_info_test.rb create mode 100644 engines/support/Gemfile create mode 100644 engines/support/README.md create mode 100644 engines/support/Rakefile create mode 100644 engines/support/app/assets/javascripts/tickets.js create mode 100644 engines/support/app/controllers/tickets_controller.rb create mode 100644 engines/support/app/designs/ticket/by_includes_post_by.js create mode 100644 engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js create mode 100644 engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js create mode 100644 engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js create mode 100644 engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js create mode 100644 engines/support/app/helpers/auto_tickets_path_helper.rb create mode 100644 engines/support/app/helpers/tickets_helper.rb create mode 100644 engines/support/app/models/account_extension/tickets.rb create mode 100644 engines/support/app/models/ticket.rb create mode 100644 engines/support/app/models/ticket_comment.rb create mode 100644 engines/support/app/models/ticket_selection.rb create mode 100644 engines/support/app/views/tickets/_comment.html.haml create mode 100644 engines/support/app/views/tickets/_edit_form.html.haml create mode 100644 engines/support/app/views/tickets/_new_comment_form.html.haml create mode 100644 engines/support/app/views/tickets/_tabs.html.haml create mode 100644 engines/support/app/views/tickets/_ticket.html.haml create mode 100644 engines/support/app/views/tickets/index.html.haml create mode 100644 engines/support/app/views/tickets/new.html.haml create mode 100644 engines/support/app/views/tickets/show.html.haml create mode 100644 engines/support/config/initializers/account_lifecycle.rb create mode 100644 engines/support/config/locales/en.yml create mode 100644 engines/support/config/routes.rb create mode 100644 engines/support/leap_web_help.gemspec create mode 100644 engines/support/lib/leap_web_help.rb create mode 100644 engines/support/lib/leap_web_help/engine.rb create mode 100644 engines/support/lib/tasks/leap_web_help_tasks.rake create mode 100755 engines/support/script/rails create mode 100644 engines/support/test/factories.rb create mode 100644 engines/support/test/functional/tickets_controller_test.rb create mode 100644 engines/support/test/integration/navigation_test.rb create mode 100644 engines/support/test/leap_web_help_test.rb create mode 100644 engines/support/test/test_helper.rb create mode 100644 engines/support/test/unit/account_extension_test.rb create mode 100644 engines/support/test/unit/ticket_comment_test.rb create mode 100644 engines/support/test/unit/ticket_test.rb delete mode 100644 help/Gemfile delete mode 100644 help/README.md delete mode 100644 help/Rakefile delete mode 100644 help/app/assets/javascripts/tickets.js delete mode 100644 help/app/controllers/tickets_controller.rb delete mode 100644 help/app/designs/ticket/by_includes_post_by.js delete mode 100644 help/app/designs/ticket/by_includes_post_by_and_created_at.js delete mode 100644 help/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js delete mode 100644 help/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js delete mode 100644 help/app/designs/ticket/by_includes_post_by_and_updated_at.js delete mode 100644 help/app/helpers/auto_tickets_path_helper.rb delete mode 100644 help/app/helpers/tickets_helper.rb delete mode 100644 help/app/models/account_extension/tickets.rb delete mode 100644 help/app/models/ticket.rb delete mode 100644 help/app/models/ticket_comment.rb delete mode 100644 help/app/models/ticket_selection.rb delete mode 100644 help/app/views/tickets/_comment.html.haml delete mode 100644 help/app/views/tickets/_edit_form.html.haml delete mode 100644 help/app/views/tickets/_new_comment_form.html.haml delete mode 100644 help/app/views/tickets/_tabs.html.haml delete mode 100644 help/app/views/tickets/_ticket.html.haml delete mode 100644 help/app/views/tickets/index.html.haml delete mode 100644 help/app/views/tickets/new.html.haml delete mode 100644 help/app/views/tickets/show.html.haml delete mode 100644 help/config/initializers/account_lifecycle.rb delete mode 100644 help/config/locales/en.yml delete mode 100644 help/config/routes.rb delete mode 100644 help/leap_web_help.gemspec delete mode 100644 help/lib/leap_web_help.rb delete mode 100644 help/lib/leap_web_help/engine.rb delete mode 100644 help/lib/tasks/leap_web_help_tasks.rake delete mode 100755 help/script/rails delete mode 100644 help/test/factories.rb delete mode 100644 help/test/functional/tickets_controller_test.rb delete mode 100644 help/test/integration/navigation_test.rb delete mode 100644 help/test/leap_web_help_test.rb delete mode 100644 help/test/test_helper.rb delete mode 100644 help/test/unit/account_extension_test.rb delete mode 100644 help/test/unit/ticket_comment_test.rb delete mode 100644 help/test/unit/ticket_test.rb diff --git a/Gemfile b/Gemfile index 50c4b00..b8b4568 100644 --- a/Gemfile +++ b/Gemfile @@ -10,8 +10,8 @@ gem "json" gem "ruby-srp", "~> 0.2.1" gem "rails_warden" -gem 'leap_web_help', :path => 'help' -gem 'leap_web_billing', :path => 'billing' +gem 'leap_web_help', :path => 'engines/support' +gem 'leap_web_billing', :path => 'engines/billing' gem 'http_accept_language' diff --git a/Gemfile.lock b/Gemfile.lock index 537dafd..a2a1785 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,13 +6,13 @@ GIT activemodel (>= 3.0.6) PATH - remote: billing + remote: engines/billing specs: leap_web_billing (0.5.0) braintree PATH - remote: help + remote: engines/support specs: leap_web_help (0.5.0) diff --git a/billing/Gemfile b/billing/Gemfile deleted file mode 100644 index 30e9669..0000000 --- a/billing/Gemfile +++ /dev/null @@ -1,23 +0,0 @@ -source "https://rubygems.org" - -eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) -eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb')) - -# We require leap_web_core from here so we can use the path option. -gem "leap_web_core", :path => '../core' - -# Declare your gem's dependencies in billing.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. -gemspec - -# jquery-rails is used by the dummy application -#gem "jquery-rails" - -# Declare any dependencies that are still in development here instead of in -# your gemspec. These might include edge Rails or gems from your path or -# Git. Remember to move these dependencies to your gemspec before releasing -# your gem to rubygems.org. - -# To use debugger -# gem 'debugger' diff --git a/billing/README.md b/billing/README.md deleted file mode 100644 index 3ef6153..0000000 --- a/billing/README.md +++ /dev/null @@ -1,49 +0,0 @@ -Billing Engine -==================== - -Currently, this engine support billing via Braintree. More backends to come later. - -Configuration ----------------------------------- - -Start with a sandbox account, which you can get here: https://www.braintreepayments.com/get-started - -Once you have registered for the sandbox, logging in will show you three important variables you will need to configure: - -* merchantId -* publicKey -* privatekey - -To configure the billing engine, edit `config/config.yaml` like so: - - production: (or "development", as you prefer) - billing: - braintree: - environment: sandbox - merchant_id: Ohp2aijaaqu6oJ4w - public_key: ahnar0UwLahwe6Ce - private_key: aemie2Geohgah2EaOad9DeeruW4Iegh4 - -If deploying via puppet, the same data in webapp.json would like this: - - "billing": { - "braintree": { - "environment": "sandbox", - "merchant_id": "Ohp2aijaaqu6oJ4w", - "public_key": "ahnar0UwLahwe6Ce", - "private_key": "aemie2Geohgah2EaOad9DeeruW4Iegh4" - } - } - -Now, you should be able to add charges to your own sandbox when you run the webapp. - -The acceptable values for `billing.braintree.environment` are: `development`, `qa`, `sandbox`, or `production`. - -Plans --------------------------------- - -You also will want to add a Plan to your Sandbox. Within the Braintree Sandbox, navigate to 'Recurring Billing' -> 'Plans'. From here, you can add a new Plan. The values of the test plan are not important, but the ID will be displayed, so should pick something descriptive. - -Here are credit cared numbers to try in the Sandbox: - -https://www.braintreepayments.com/docs/ruby/reference/sandbox \ No newline at end of file diff --git a/billing/Rakefile b/billing/Rakefile deleted file mode 100644 index 52929c4..0000000 --- a/billing/Rakefile +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env rake -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end -begin - require 'rdoc/task' -rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' - RDoc::Task = Rake::RDocTask -end - -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebBilling' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = eval(File.read('leap_web_billing.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - - -task :default => :test diff --git a/billing/app/controllers/billing_admin_controller.rb b/billing/app/controllers/billing_admin_controller.rb deleted file mode 100644 index e11d4ee..0000000 --- a/billing/app/controllers/billing_admin_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -class BillingAdminController < BillingBaseController - before_filter :require_admin - - def show - - br_atleast_90_days = Braintree::Subscription.search do |search| - search.days_past_due >= 90 - end - @past_due_atleast_90_days = braintree_resource_collection_to_array(br_atleast_90_days) - - br_all_past_due = Braintree::Subscription.search do |search| - search.status.is Braintree::Subscription::Status::PastDue - #cannot search by balance. - end - @all_past_due = braintree_resource_collection_to_array(br_all_past_due) - - end - - private - - def braintree_resource_collection_to_array(braintree_resource_collection) - array = [] - braintree_resource_collection.each do |object| - array << object - end - array - end - -end diff --git a/billing/app/controllers/billing_base_controller.rb b/billing/app/controllers/billing_base_controller.rb deleted file mode 100644 index 0453677..0000000 --- a/billing/app/controllers/billing_base_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -class BillingBaseController < ApplicationController - before_filter :assign_user - - helper 'billing' - - # required for navigation to work. - def assign_user - if params[:user_id] - @user = User.find(params[:user_id]) - elsif params[:action] == "confirm"# confirms will come back with different ID set, so check for this first - # This is only for cases where an admin cannot apply action for customer, but should be all confirms - @user = current_user - elsif params[:id] - @user = User.find(params[:id]) - else - # TODO - # hacky, what are cases where @user hasn't yet been set? certainly some cases with subscriptions and payments - @user = current_user - end - end - -end diff --git a/billing/app/controllers/credit_card_info_controller.rb b/billing/app/controllers/credit_card_info_controller.rb deleted file mode 100644 index fbaa6f1..0000000 --- a/billing/app/controllers/credit_card_info_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -class CreditCardInfoController < ApplicationController - before_filter :require_login, :set_user - - def edit - @credit_card = Braintree::CreditCard.find(params[:id]) - customer = Customer.find_by_user_id(@user.id) - if customer and customer.braintree_customer_id == @credit_card.customer_id - @tr_data = Braintree::TransparentRedirect. - update_credit_card_data(:redirect_url => confirm_credit_card_info_url, - :payment_method_token => @credit_card.token) - else - access_denied - end - - end - - def confirm - @result = Braintree::TransparentRedirect.confirm(request.query_string) - if @result.success? - render :action => "confirm" - else - @credit_card = Braintree::CreditCard.find(@result.params[:payment_method_token]) - render :action => "edit" - end - end - - - private - - def set_user - # this assumes anybody, even an admin, will not access for another user. - @user = current_user - end - -end diff --git a/billing/app/controllers/customer_controller.rb b/billing/app/controllers/customer_controller.rb deleted file mode 100644 index 6cbcb44..0000000 --- a/billing/app/controllers/customer_controller.rb +++ /dev/null @@ -1,64 +0,0 @@ -class CustomerController < BillingBaseController - before_filter :require_login, :fetch_customer - - def show - if @customer - @customer.with_braintree_data! - @default_cc = @customer.default_credit_card - @active_subscription = @customer.subscriptions - @transactions = @customer.braintree_customer.transactions - end - end - - def new - if @customer.has_payment_info? - redirect_to edit_customer_path(@user), :notice => 'Here is your saved customer data' - else - fetch_new_transparent_redirect_data - end - end - - def edit - fetch_edit_transparent_redirect_data - end - - def confirm - @result = Braintree::TransparentRedirect.confirm(request.query_string) - if @result.success? - @customer.braintree_customer = @result.customer - @customer.save - render :action => "confirm" - elsif @customer.has_payment_info? - fetch_edit_transparent_redirect_data - render :action => "edit" - else - fetch_new_transparent_redirect_data - render :action => "new" - end - end - - protected - - def fetch_new_transparent_redirect_data - access_denied unless @user == current_user # admins cannot do this for others - @tr_data = Braintree::TransparentRedirect. - create_customer_data(:redirect_url => confirm_customer_url) - end - - def fetch_edit_transparent_redirect_data - access_denied unless @user == current_user # admins cannot do this for others - @customer.with_braintree_data! - @default_cc = @customer.default_credit_card - @tr_data = Braintree::TransparentRedirect. - update_customer_data(:redirect_url => confirm_customer_url, - :customer_id => @customer.braintree_customer_id) ##?? - end - - def fetch_customer - @customer = Customer.find_by_user_id(@user.id) - if @user == current_user - @customer ||= Customer.new(user: @user) - end - access_denied unless (@customer and (@customer.user == current_user)) or admin? - end -end diff --git a/billing/app/controllers/payments_controller.rb b/billing/app/controllers/payments_controller.rb deleted file mode 100644 index fce6570..0000000 --- a/billing/app/controllers/payments_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -class PaymentsController < BillingBaseController - before_filter :require_login, :only => [:index] - - def new - fetch_transparent_redirect - end - - def confirm - @result = Braintree::TransparentRedirect.confirm(request.query_string) - if @result.success? - render :action => "confirm" - else - fetch_transparent_redirect - render :action => "new" - end - end - - def index - access_denied unless admin? or (@user == current_user) - customer = Customer.find_by_user_id(@user.id) - braintree_data = Braintree::Customer.find(customer.braintree_customer_id) - # these will be ordered by created_at descending, per http://stackoverflow.com/questions/16425475/ - @transactions = braintree_data.transactions - end - - protected - - - def fetch_transparent_redirect - @tr_data = Braintree::TransparentRedirect.transaction_data redirect_url: confirm_payment_url, - transaction: { type: "sale", options: {submit_for_settlement: true } } - end - -end diff --git a/billing/app/controllers/subscriptions_controller.rb b/billing/app/controllers/subscriptions_controller.rb deleted file mode 100644 index f066b3c..0000000 --- a/billing/app/controllers/subscriptions_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -class SubscriptionsController < BillingBaseController - before_filter :require_login - before_filter :fetch_subscription, :only => [:show, :destroy] - before_filter :confirm_cancel_subscription, :only => [:destroy] - before_filter :confirm_self_or_admin, :only => [:index] - before_filter :confirm_no_pending_active_pastdue_subscription, :only => [:new, :create] - # for now, admins cannot create or destroy subscriptions for others: - before_filter :confirm_self, :only => [:new, :create] - - def new - # don't show link to subscribe if they are already subscribed? - credit_card = @customer.default_credit_card #safe to assume default? - @payment_method_token = credit_card.token - @plans = Braintree::Plan.all - end - - # show has no content, so not needed at this point. - - def create - @result = Braintree::Subscription.create( :payment_method_token => params[:payment_method_token], :plan_id => params[:plan_id] ) - #if you want to test pastdue, can add :price => '2001', :trial_period => true,:trial_duration => 1,:trial_duration_unit => "day" and then wait a day - end - - def destroy - @result = Braintree::Subscription.cancel params[:id] - end - - def index - customer = Customer.find_by_user_id(@user.id) - @subscriptions = customer.subscriptions(nil, false) - end - - private - - def fetch_subscription - @subscription = Braintree::Subscription.find params[:id] - @credit_card = Braintree::CreditCard.find @subscription.payment_method_token - @subscription_customer_id = @credit_card.customer_id - current_user_customer = Customer.find_by_user_id(current_user.id) - access_denied unless admin? or (current_user_customer and current_user_customer.braintree_customer_id == @subscription_customer_id) - - end - - def confirm_cancel_subscription - access_denied unless view_context.allow_cancel_subscription(@subscription) - end - - def confirm_no_pending_active_pastdue_subscription - @customer = Customer.find_by_user_id(@user.id) - if subscription = @customer.subscriptions # will return pending, active or pastdue subscription, if it exists - redirect_to user_subscription_path(@user, subscription.id), :notice => 'You already have a subscription' - end - end - - def confirm_self - @user == current_user - end - - def confirm_self_or_admin - access_denied unless confirm_self or admin? - end - -end diff --git a/billing/app/helpers/billing_helper.rb b/billing/app/helpers/billing_helper.rb deleted file mode 100644 index b9e5e2e..0000000 --- a/billing/app/helpers/billing_helper.rb +++ /dev/null @@ -1,51 +0,0 @@ -module BillingHelper - - def braintree_form_for(object, options = {}, &block) - options.reverse_merge! params: @result && @result.params[object], - errors: @result && @result.errors.for(object), - builder: BraintreeFormHelper::BraintreeFormBuilder, - url: Braintree::TransparentRedirect.url - - form_for object, options, &block - end - - def billing_top_link(user) - # for admins, top link will show special admin information, which has link to show their own customer information - if (admin? and user == current_user) - billing_admin_path - else - show_or_new_customer_link(user) - end - end - - def show_or_new_customer_link(user) - # Link to show if user is admin viewing another user, or user is already a customer. - # Otherwise link to create a new customer. - if (admin? and (user != current_user)) or ((customer = Customer.find_by_user_id(user.id)) and customer.has_payment_info?) - show_customer_path(user) - else - new_customer_path - end - end - - # a bit strange to put here, but we don't have a subscription model - def user_for_subscription(subscription) - - if (transaction = subscription.transactions.first) - # much quicker, but will only work if there is already a transaction associated with subscription (should generally be) - braintree_customer_id = transaction.customer_details.id - else - credit_card = Braintree::CreditCard.find(subscription.payment_method_token) - braintree_customer_id = credit_card.customer_id - end - - customer = Customer.find_by_braintree_customer_id(braintree_customer_id) - user = User.find(customer.user_id) - - end - - def allow_cancel_subscription(subscription) - ['Active', 'Pending'].include? subscription.status or (admin? and subscription.status == 'Past Due') - end - -end diff --git a/billing/app/helpers/braintree_form_helper.rb b/billing/app/helpers/braintree_form_helper.rb deleted file mode 100644 index cb322fa..0000000 --- a/billing/app/helpers/braintree_form_helper.rb +++ /dev/null @@ -1,64 +0,0 @@ -module BraintreeFormHelper - class BraintreeFormBuilder < ActionView::Helpers::FormBuilder - include ActionView::Helpers::AssetTagHelper - include ActionView::Helpers::TagHelper - - def initialize(object_name, object, template, options, proc) - super - @braintree_params = @options[:params] - @braintree_errors = @options[:errors] - @braintree_existing = @options[:existing] - end - - def fields_for(record_name, *args, &block) - options = args.extract_options! - options[:builder] = BraintreeFormBuilder - options[:params] = @braintree_params && @braintree_params[record_name] - options[:errors] = @braintree_errors && @braintree_errors.for(record_name) - new_args = args + [options] - super record_name, *new_args, &block - end - - def text_field(method, options = {}) - has_errors = @braintree_errors && @braintree_errors.on(method).any? - field = super(method, options.merge(:value => determine_value(method))) - result = content_tag("div", field, :class => has_errors ? "fieldWithErrors" : "") - result.safe_concat validation_errors(method) - result - end - - protected - - def determine_value(method) - if @braintree_params - @braintree_params[method] - elsif @braintree_existing - - if @braintree_existing.kind_of?(Braintree::CreditCard) - - case method - when :number - method = :masked_number - when :cvv - return nil - end - end - - @braintree_existing.send(method) - else - nil - end - end - - def validation_errors(method) - if @braintree_errors && @braintree_errors.on(method).any? - @braintree_errors.on(method).map do |error| - content_tag("div", ERB::Util.h(error.message), {:style => "color: red;"}) - end.join - else - "" - end - end - end -end - diff --git a/billing/app/helpers/braintree_helper.rb b/billing/app/helpers/braintree_helper.rb deleted file mode 100644 index 2d18b6c..0000000 --- a/billing/app/helpers/braintree_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module BraintreeHelper - - -end - diff --git a/billing/app/models/customer.rb b/billing/app/models/customer.rb deleted file mode 100644 index 1acc7a5..0000000 --- a/billing/app/models/customer.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Customer < CouchRest::Model::Base - - FIELDS = [:first_name, :last_name, :phone, :website, :company, :fax, :addresses, :credit_cards, :custom_fields] - attr_accessor *FIELDS - - use_database "customers" - belongs_to :user - belongs_to :braintree_customer - - # Braintree::Customer - stored on braintrees servers - we only have the id. - def braintree_customer - @braintree_customer ||= Braintree::Customer.find(braintree_customer_id) - end - - validates :user, presence: true - - design do - view :by_user_id - view :by_braintree_customer_id - end - - def has_payment_info? - !!braintree_customer_id - end - - # from braintree_ruby_examples/rails3_tr_devise and should be tweaked - def with_braintree_data! - return self unless has_payment_info? - - FIELDS.each do |field| - send(:"#{field}=", braintree_customer.send(field)) - end - self - end - - def default_credit_card - return unless has_payment_info? - - credit_cards.find { |cc| cc.default? } - end - - # based on 2nd parameter, either returns the single active subscription (or nil if there isn't one), or an array of all subsciptions - def subscriptions(braintree_data=nil, only_pending_active_pastdue=true) - self.with_braintree_data! - return unless has_payment_info? - - subscriptions = [] - self.default_credit_card.subscriptions.each do |sub| - if only_pending_active_pastdue and ['Pending', 'Active','Past Due'].include? sub.status - return sub - else - subscriptions << sub - end - end - only_pending_active_pastdue ? nil : subscriptions - end - -end diff --git a/billing/app/views/billing_admin/show.html.haml b/billing/app/views/billing_admin/show.html.haml deleted file mode 100644 index 0382cf0..0000000 --- a/billing/app/views/billing_admin/show.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%legend= t(:more_than_90_days_past_due) -= render(:partial => "subscriptions/subscription_details", :collection => @past_due_atleast_90_days, :as => 'subscription', :locals => {:show_user => true}) || t(:none) -%legend= t(:all_past_due) -= render(:partial => "subscriptions/subscription_details", :collection => @all_past_due, :as => 'subscription', :locals => {:show_user => true}) || t(:none) - -%legend= t(:your_settings) -= link_to 'view own billing settings', show_or_new_customer_link(current_user) \ No newline at end of file diff --git a/billing/app/views/credit_card_info/confirm.html.haml b/billing/app/views/credit_card_info/confirm.html.haml deleted file mode 100644 index 9dd8176..0000000 --- a/billing/app/views/credit_card_info/confirm.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%h1 Payment Info Confirmation -%p Your payment information was successfully saved. -%dl - %dt Credit Card - %dd= @result.credit_card.masked_number diff --git a/billing/app/views/credit_card_info/edit.html.haml b/billing/app/views/credit_card_info/edit.html.haml deleted file mode 100644 index bd86a4c..0000000 --- a/billing/app/views/credit_card_info/edit.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%h1 Change Credit Card -- if @result - #total-errors{:style => "color:red;"} - = h(@result.errors.size) - error(s) -= braintree_form_for :credit_card, :existing => @credit_card do |f| - = field_set_tag "Credit Card" do - %dl - %dt= f.label :number, 'Number' - %dd= f.text_field :number - %dt= f.label :expiration_date, 'Expiration Date (MM/YY)' - %dd= f.text_field :expiration_date - %dt= f.label :cvv, 'CVV' - %dd= f.text_field :cvv - = hidden_field_tag :tr_data, @tr_data - = f.submit 'Save Payment Info', :class => :btn - = link_to t(:cancel), edit_customer_path(@user.id), :class => :btn diff --git a/billing/app/views/customer/_customer_data.html.haml b/billing/app/views/customer/_customer_data.html.haml deleted file mode 100644 index e9df040..0000000 --- a/billing/app/views/customer/_customer_data.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%legend= t(:customer_information) -%dl - %dt First Name - %dd= @customer.first_name - %dt Last Name - %dd= @customer.last_name - %dt Phone - %dd= @customer.phone -%legend= t(:credit_card_information) -%dl - %dt Number - %dd= @default_cc.masked_number - %dt Expiration Date - %dd= @default_cc.expiration_date - - if current_user == @user - = link_to t(:edit_saved_data), edit_customer_path(@user.id), :class => :btn diff --git a/billing/app/views/customer/_transaction.html.haml b/billing/app/views/customer/_transaction.html.haml deleted file mode 100644 index e69de29..0000000 diff --git a/billing/app/views/customer/confirm.html.haml b/billing/app/views/customer/confirm.html.haml deleted file mode 100644 index 877a8ac..0000000 --- a/billing/app/views/customer/confirm.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%h1 Payment Info Confirmation -%p Your payment information was successfully saved. -%dl - %dt First Name - %dd= @result.customer.first_name - %dt Last Name - %dd= @result.customer.last_name - %dt Phone - %dd= @result.customer.phone - %dt Credit Card - - @result.customer.credit_cards.each do |cc| - %dd= cc.masked_number -- customer = Customer.find_by_user_id(@user.id) -= link_to 'View Customer Info', show_customer_path(@user.id), :class=> :btn \ No newline at end of file diff --git a/billing/app/views/customer/edit.html.haml b/billing/app/views/customer/edit.html.haml deleted file mode 100644 index e882d53..0000000 --- a/billing/app/views/customer/edit.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -- if @result - #total-errors{:style => "color:red;"} - = h(@result.errors.size) - error(s) -= braintree_form_for :customer, existing: @customer do |f| - = field_set_tag "Customer" do - %dl - %dt= f.label :first_name, 'First Name' - %dd= f.text_field :first_name - %dt= f.label :last_name, 'Last Name' - %dd= f.text_field :last_name - %dt= f.label :phone, 'Phone' - %dd= f.text_field :phone - - if @default_cc - = # todo, as they will need a credit card, so not sure about conditional? - %dt= t(:stored_credit_card) - %dd - = @default_cc.masked_number - = link_to t(:change_credit_card), edit_credit_card_info_path(:id => @default_cc.token), :class => :btn - = hidden_field_tag :tr_data, @tr_data - .form-actions - = f.submit t(:save_customer_info), :class => 'btn btn-primary' - = link_to t(:cancel), show_customer_path(@user), :class=> :btn diff --git a/billing/app/views/customer/new.html.haml b/billing/app/views/customer/new.html.haml deleted file mode 100644 index e1f5ba9..0000000 --- a/billing/app/views/customer/new.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- if @result - #total-errors{:style => "color:red;"} - = h(@result.errors.size) - error(s) -= braintree_form_for :customer do |f| - = field_set_tag "Customer" do - %dl - %dt= f.label :first_name, 'First Name' - %dd= f.text_field :first_name - %dt= f.label :last_name, 'Last Name' - %dd= f.text_field :last_name - %dt= f.label :phone, 'Phone' - %dd= f.text_field :phone - = field_set_tag "Credit Card" do - - f.fields_for :credit_card do |cc| - %dl - %dt= cc.label :number, 'Number' - %dd= cc.text_field :number - %dt= cc.label :expiration_date, 'Expiration Date (MM/YY)' - %dd= cc.text_field :expiration_date - %dt= cc.label :cvv, 'CVV' - %dd= cc.text_field :cvv - = hidden_field_tag :tr_data, @tr_data - = f.submit 'Save Payment Info' diff --git a/billing/app/views/customer/show.html.haml b/billing/app/views/customer/show.html.haml deleted file mode 100644 index ec1779c..0000000 --- a/billing/app/views/customer/show.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- if admin? and !@customer - = t(:no_saved_customer) -- else - = render :partial => 'customer_data' - %legend= t(:last_three_transactions) - - counter = 0 - = # these will be ordered with most recently created first, per http://stackoverflow.com/questions/16425475/ - - @transactions.each do |t| - - break if counter > 2 # not ruby-like, but object is a Braintree::ResourceCollection so limited methods available - = render :partial => "payments/transaction_details", :locals => {:transaction => t} - - counter += 1 - = link_to t(:transaction_history), user_payments_path(@user) - %legend= t(:subscriptions) - - if @active_subscription - = render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription} - - else - %p - = t(:no_relevant_subscription) - - if current_user == @user - %p - .form-actions - = link_to t(:subscribe_to_plan), new_subscription_path, :class => :btn - %p - = link_to t(:all_subscriptions), user_subscriptions_path(@user) - -.form-actions - = link_to t(:make_donation), new_payment_path, :class => 'btn btn-primary' diff --git a/billing/app/views/payments/_non_customer_fields.html.haml b/billing/app/views/payments/_non_customer_fields.html.haml deleted file mode 100644 index 77cfe95..0000000 --- a/billing/app/views/payments/_non_customer_fields.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= field_set_tag "Personal Information" do - = f.fields_for :customer do |c| - %div= c.label :first_name, "First Name" - %div= c.text_field :first_name - %div= c.label :last_name, "Last Name" - %div= c.text_field :last_name - %div= c.label :email, "Email" - %div= c.text_field :email -= field_set_tag "Credit Card" do - = f.fields_for :credit_card do |c| - %div= c.label :number, "Number" - %div= c.text_field :number - %div= c.label :expiration_date, "Expiration Date (MM/YY)" - %div= c.text_field :expiration_date - %div= c.label :cvv, "CVV" - %div= c.text_field :cvv \ No newline at end of file diff --git a/billing/app/views/payments/_transaction_details.html.haml b/billing/app/views/payments/_transaction_details.html.haml deleted file mode 100644 index 85e4f6a..0000000 --- a/billing/app/views/payments/_transaction_details.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%p - = transaction.id - Type: - = transaction.type - Amount: - = number_to_currency(transaction.amount) - Status: - = transaction.status - Date - = transaction.created_at.strftime("%Y-%m-%d") - - if sub_start = transaction.subscription_details.billing_period_start_date - From subscription which started - = sub_start - - else # should not have any of these - Not paid as part of subscription \ No newline at end of file diff --git a/billing/app/views/payments/confirm.html.haml b/billing/app/views/payments/confirm.html.haml deleted file mode 100644 index 45af3c9..0000000 --- a/billing/app/views/payments/confirm.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -%h1 Payment Result -%div Thank you for your donation. -%h2 Transaction Details -%table - %tr - %td Amount - %td - $#{@result.transaction.amount} - %tr - %td Transaction ID: - %td= @result.transaction.id - %tr - %td First Name: - %td= h @result.transaction.customer_details.first_name - %tr - %td Last Name: - %td= h @result.transaction.customer_details.last_name - %tr - %td Email: - %td= h @result.transaction.customer_details.email - %tr - %td Credit Card: - %td= h @result.transaction.credit_card_details.masked_number - %tr - %td Card Type: - %td= h @result.transaction.credit_card_details.card_type \ No newline at end of file diff --git a/billing/app/views/payments/index.html.haml b/billing/app/views/payments/index.html.haml deleted file mode 100644 index 7a89917..0000000 --- a/billing/app/views/payments/index.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%h2=t :transaction_history -- if (@transactions.count == 0) - = t(:no_transaction_history) -- @transactions.each do |t| - = render :partial => "transaction_details", :locals => {:transaction => t} \ No newline at end of file diff --git a/billing/app/views/payments/new.html.haml b/billing/app/views/payments/new.html.haml deleted file mode 100644 index e9a8273..0000000 --- a/billing/app/views/payments/new.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%h1 - = t(:Donation) -- if logged_in? - = t(:donation_not_payment) -- if @result and @result.errors.size > 0 - %div{:style => "color: red;"} - = h @result.errors.size - error(s) -- if @result and @result.transaction and @result.transaction.status != 'success' - %div{:style => "color: red;"} - = t(:processor_declined) -= braintree_form_for :transaction, :html => {:autocomplete => "off"} do |f| - = f.label :amount, t(:amount) - = f.text_field :amount - = render :partial => 'non_customer_fields', :locals => {:f => f} - = hidden_field_tag :tr_data, @tr_data - = f.submit "Submit Donation", :class => 'btn btn-primary' diff --git a/billing/app/views/subscriptions/_subscription_details.html.haml b/billing/app/views/subscriptions/_subscription_details.html.haml deleted file mode 100644 index 6145c95..0000000 --- a/billing/app/views/subscriptions/_subscription_details.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -%p - - if local_assigns[:show_user] - User: - - user_to_show = user_for_subscription(subscription) - = link_to user_to_show.login, user_overview_path(user_to_show) - ID: - = link_to subscription.id, user_subscription_path(@user, subscription.id) - Balance: - - color = (subscription.balance > 0) ? "red" : "" - %font{:color => color} - = number_to_currency(subscription.balance) - Bill on: - = subscription.billing_day_of_month - Start date: - = subscription.first_billing_date - Paid through: - = subscription.paid_through_date - Plan: - = subscription.plan_id - Price: - = number_to_currency(subscription.price) - - color = (subscription.status == 'Active') ? "green" : "red" - Status: - %font{:color => color} - = subscription.status - - # would be good to get plan name but not sure if that is possible? \ No newline at end of file diff --git a/billing/app/views/subscriptions/create.html.haml b/billing/app/views/subscriptions/create.html.haml deleted file mode 100644 index 2b6c5e9..0000000 --- a/billing/app/views/subscriptions/create.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- if @result.success? - %h1 - Subscription Status - = @result.subscription.status - = render :partial => "subscription_details", :locals => {:subscription => @result.subscription} -- else - %h1 - Error: - = @result.message \ No newline at end of file diff --git a/billing/app/views/subscriptions/destroy.html.haml b/billing/app/views/subscriptions/destroy.html.haml deleted file mode 100644 index 44b4333..0000000 --- a/billing/app/views/subscriptions/destroy.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- if @result.success? - Subscription destroyed -- else - Error: - = @result.message -%p - = link_to 'Customer Information', show_customer_path(@user), :class=> :btn \ No newline at end of file diff --git a/billing/app/views/subscriptions/index.html.haml b/billing/app/views/subscriptions/index.html.haml deleted file mode 100644 index 3d4e8fd..0000000 --- a/billing/app/views/subscriptions/index.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%h2=t :all_subscriptions -- pending_active_pastdue = false -- @subscriptions.each do |s| - - if ['Pending', 'Active','Past Due'].include? s.status - - pending_active_pastdue = true - = render :partial => "subscription_details", :locals => {:subscription => s} -- if !pending_active_pastdue and @user == current_user - = link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file diff --git a/billing/app/views/subscriptions/new.html.haml b/billing/app/views/subscriptions/new.html.haml deleted file mode 100644 index 4183458..0000000 --- a/billing/app/views/subscriptions/new.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- if @payment_method_token - %h1 - Subscribe to plan - = #currently just one plan - = @plans[0].name - = number_to_currency(@plans[0].price) - = simple_form_for :subscription, :url => :subscriptions do |f| - = hidden_field_tag :payment_method_token, @payment_method_token - = hidden_field_tag :plan_id, @plans[0].id - .form-actions - = f.submit t(:subscribe), :class => 'btn btn-primary' -- else - = t(:must_create_customer) - %p - = link_to t(:create_new_customer), new_customer_path diff --git a/billing/app/views/subscriptions/show.html.haml b/billing/app/views/subscriptions/show.html.haml deleted file mode 100644 index 2699db9..0000000 --- a/billing/app/views/subscriptions/show.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%h1 - - if @subscription.status == 'Active' - Current - Subscription -= render :partial => "subscription_details", :locals => {:subscription => @subscription} -= link_to t(:cancel_subscription), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if allow_cancel_subscription(@subscription) diff --git a/billing/config/initializers/braintree.rb b/billing/config/initializers/braintree.rb deleted file mode 100644 index 3d87f4c..0000000 --- a/billing/config/initializers/braintree.rb +++ /dev/null @@ -1,23 +0,0 @@ -# -# set logger -# -if APP_CONFIG[:logfile].blank? - require 'syslog/logger' - Braintree::Configuration.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new('webapp')) -else - Braintree::Configuration.logger = Logger.new('log/braintree.log') -end - -# -# You can set these per environment in config/config.yml: -# -# Environment must be one of: :development, :qa, :sandbox, :production -# -if billing = APP_CONFIG[:billing] - if braintree = billing[:braintree] - Braintree::Configuration.environment = braintree[:environment].downcase.to_sym - Braintree::Configuration.merchant_id = braintree[:merchant_id] - Braintree::Configuration.public_key = braintree[:public_key] - Braintree::Configuration.private_key = braintree[:private_key] - end -end diff --git a/billing/config/locales/en.yml b/billing/config/locales/en.yml deleted file mode 100644 index 1300958..0000000 --- a/billing/config/locales/en.yml +++ /dev/null @@ -1,11 +0,0 @@ -en: - create_new_customer: "Create a new Braintree Customer" - must_create_customer: "You must store a customer in braintree before subscribing to a plan" - subscribe: "Subscribe" - save_customer_info: "Save Customer Information" - donation_not_payment: "Note: This is a donation, and will not be applied towards your account." - no_relevant_subscription: "No subscription which is Active, Pending, or Past Due" - plan: "Plan" - description: "Description" - cost: "Cost" - free: "Free" \ No newline at end of file diff --git a/billing/config/routes.rb b/billing/config/routes.rb deleted file mode 100644 index 7263dff..0000000 --- a/billing/config/routes.rb +++ /dev/null @@ -1,25 +0,0 @@ -Rails.application.routes.draw do - - scope "(:locale)", :locale => MATCH_LOCALE do - match 'payments/new' => 'payments#new', :as => :new_payment - match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment - resources :users do - resources :payments, :only => [:index] - resources :subscriptions, :only => [:index, :show, :destroy] - end - - resources :customer, :only => [:new, :edit] - resources :credit_card_info, :only => [:edit] - - match 'customer/confirm/' => 'customer#confirm', :as => :confirm_customer - match 'customer/show/:id' => 'customer#show', :as => :show_customer - match 'credit_card_info/confirm' => 'credit_card_info#confirm', :as => :confirm_credit_card_info - - resources :subscriptions, :only => [:new, :create, :update] # index, show & destroy are within users path - match 'billing_admin' => 'billing_admin#show', :as => :billing_admin - - #match 'transactions/:product_id/new' => 'transactions#new', :as => :new_transaction - #match 'transactions/confirm/:product_id' => 'transactions#confirm', :as => :confirm_transaction - end - -end diff --git a/billing/leap_web_billing.gemspec b/billing/leap_web_billing.gemspec deleted file mode 100644 index cb0335e..0000000 --- a/billing/leap_web_billing.gemspec +++ /dev/null @@ -1,21 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "leap_web_billing" - s.version = LeapWeb::VERSION - s.authors = ["Jessib"] - s.email = ["jessib@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "Billing for LeapWeb" - s.description = "Billing System for a Leap provider" - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] - s.test_files = Dir["test/**/*"] - - # s.add_dependency "braintree-rails", "~> 0.4.5" - s.add_dependency "braintree" - #s.add_dependency "carmen-rails" -end diff --git a/billing/lib/braintree_test_app.rb b/billing/lib/braintree_test_app.rb deleted file mode 100644 index 41c327d..0000000 --- a/billing/lib/braintree_test_app.rb +++ /dev/null @@ -1,36 +0,0 @@ -# RackTest assumes all requests to be local. -# Braintree requests need to go out to a different server though. -# So we use a middleware to catch these and send them out again. - -class BraintreeTestApp - def initialize(app) - @app = app - end - - def call(env) - @env = env - config = Braintree::Configuration.instantiate - if request.path =~ /\/merchants\/#{config.merchant_id}\/transparent_redirect_requests$/ - #proxy post to braintree - uri = URI.parse(config.protocol + "://" + config.server + ":" + - config.port.to_s + request.path) - http = Net::HTTP.new(uri.host, uri.port) - res = http.post(uri.path, request.body.read) - - if res.code == "303" - header_hash = res.header.to_hash - header_hash["location"].first.gsub!("http://localhost:3000/", "http://www.example.com/") - [303, {"location" => header_hash["location"].first}, ""] - else - raise "unexpected response from Braintree: expected a 303" - end - else - @app.call(env) - end - end - - def request - @request = Rack::Request.new(@env) - end -end - diff --git a/billing/lib/leap_web_billing.rb b/billing/lib/leap_web_billing.rb deleted file mode 100644 index 288d846..0000000 --- a/billing/lib/leap_web_billing.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "leap_web_billing/engine" - -module LeapWebBilling -end diff --git a/billing/lib/leap_web_billing/engine.rb b/billing/lib/leap_web_billing/engine.rb deleted file mode 100644 index ab574f2..0000000 --- a/billing/lib/leap_web_billing/engine.rb +++ /dev/null @@ -1,10 +0,0 @@ -# thou shall require all your dependencies in an engine. -#require "braintree-rails" -require "braintree" -#require "carmen-rails" - -module LeapWebBilling - class Engine < ::Rails::Engine - - end -end diff --git a/billing/script/rails b/billing/script/rails deleted file mode 100755 index 8bd9c0a..0000000 --- a/billing/script/rails +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/leap_web_billing/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' diff --git a/billing/test/broken/admin_customer_test.rb b/billing/test/broken/admin_customer_test.rb deleted file mode 100644 index df92a0d..0000000 --- a/billing/test/broken/admin_customer_test.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class AdminCustomerTest < BraintreeIntegrationTest - - setup do - @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') - @user = FactoryGirl.create(:user) - end - - teardown do - @user.destroy if @user - @admin.destroy if @admin - end - - test "check non customer as admin" do - login_as @admin - visit '/' - click_link 'Users' - click_link @user.login - click_link 'Billing Settings' - assert page.has_content? @user.email_address - assert page.has_content? 'No Saved Customer' - end - - test "check customer as admin" do - skip "cannot check customer as admin" - # it would be good to have a test where an admin tries to view the 'Billing Settings' for another user. - # However, partially due to limitations of FakeBraintree, this doesn't seem pursuing at this time. - end -end diff --git a/billing/test/broken/customer_creation_test.rb b/billing/test/broken/customer_creation_test.rb deleted file mode 100644 index 90319a9..0000000 --- a/billing/test/broken/customer_creation_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class CustomerCreationTest < BraintreeIntegrationTest - - setup do - @user = FactoryGirl.create(:user) - login_as @user - end - - teardown do - @user.destroy - end - - # Let's test both steps together with capybara - # - # This test is nice and clean but also a bit fragile: - # RackTest assumes all requests to be local. So we need - # BraintreeTestApp for the braintree transparent redirect to work. - # - # this mystifies me why this works. when i type the click_button line (and the - # customer.braintree_customer line) in the debugger, it gives a timeout, - # but it works fine embedded in the test. - test "create customer with braintree" do - visit '/' - click_link 'Billing Settings' - # i am a bit unclear why this works, as it seems there will be validation errors - assert_difference("Customer.count") do - click_button 'Save Payment Info' # this gives me a timeout - end - assert customer = Customer.find_by_user_id(@user.id) - assert customer.braintree_customer - end - - # We only test the confirmation here. - # The request to Braintree is triggered outside of rails - # In skippped test below, we see this works even if the attributes are - # for a broken customer - test "successfully confirms customer creation" do - response = post_transparent_redirect :create_customer_data, - customer: FactoryGirl.attributes_for(:braintree_customer), - redirect_url: confirm_customer_url - - assert_difference("Customer.count") do - post response['Location'] - end - - assert_equal 200, status - assert customer = Customer.find_by_user_id(@user.id) - assert customer.braintree_customer - end - - - test "failed customer creation" do - skip "cannot get customer creation to fail" - - FakeBraintree.decline_all_cards! - - response = post_transparent_redirect :create_customer_data, - customer: FactoryGirl.attributes_for(:broken_customer), - redirect_url: confirm_customer_url - - assert FakeBraintree.decline_all_cards? - assert_no_difference("Customer.count") do - post response['Location'] #this gives me a timeout when run alone - end - assert_nil Customer.find_by_user_id(@user.id) - - end - - def post_transparent_redirect(type, data) - params = data.dup - params[:tr_data] = Braintree::TransparentRedirect.send(type, params) - post_transparent_redirect_params(params) - end - - def post_transparent_redirect_params(params) - uri = URI.parse(Braintree::TransparentRedirect.url) - Net::HTTP.start(uri.host, uri.port) do |http| - http.post(uri.path, Rack::Utils.build_nested_query(params)) - end - end - -end diff --git a/billing/test/broken/subscription_test.rb b/billing/test/broken/subscription_test.rb deleted file mode 100644 index cd010bd..0000000 --- a/billing/test/broken/subscription_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class SubscriptionTest < BraintreeIntegrationTest - include CustomerTestHelper - include StubRecordHelper - - setup do - @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') - @customer = stub_customer - @braintree_customer = @customer.braintree_customer - response = Braintree::Subscription.create plan_id: '5', - payment_method_token: @braintree_customer.credit_cards.first.token, - price: '10' - @subscription = response.subscription - end - - teardown do - @admin.destroy - end - - test "admin can see all subscriptions for another" do - login_as @admin - @customer.stubs(:subscriptions).returns([@subscription]) - @subscription.stubs(:balance).returns 0 - visit user_subscriptions_path(@customer.user_id, :locale => nil) - assert page.has_content?("Subscriptions") - assert page.has_content?("Status: Active") - end - - # test "user cannot see all subscriptions for other user" do - #end - - #test "admin cannot add subscription for another" do - #end - - #test "authenticated user can cancel own subscription" do - #end - - #test "user cannot add subscription if they have active one" do - #end - - #test "user can view own subscriptions" - #end - - #test "admin can view another user's subscriptions" do - #end - -end diff --git a/billing/test/factories.rb b/billing/test/factories.rb deleted file mode 100644 index 87543b2..0000000 --- a/billing/test/factories.rb +++ /dev/null @@ -1,25 +0,0 @@ -FactoryGirl.define do - - TEST_CC_NUMBER = %w(4111 1111 1111 1111).join - - factory :customer do - user - - factory :customer_with_payment_info do - braintree_customer - end - end - - factory :braintree_customer, class: Braintree::Customer do - first_name 'Big' - last_name 'Spender' - credit_card number: TEST_CC_NUMBER, expiration_date: '04/2016' - initialize_with { Braintree::Customer.create(attributes).customer } - skip_create - - factory :broken_customer do - credit_card number: '123456', expiration_date: '04/2016' - end - end - -end diff --git a/billing/test/functional/customer_controller_test.rb b/billing/test/functional/customer_controller_test.rb deleted file mode 100644 index d943e23..0000000 --- a/billing/test/functional/customer_controller_test.rb +++ /dev/null @@ -1,124 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class CustomerControllerTest < ActionController::TestCase - include CustomerTestHelper - - test "new assigns redirect url" do - login - get :new - - assert_response :success - assert assigns(:tr_data) - tr_data = Braintree::Util.parse_query_string(assigns(:tr_data)) - assert_equal confirm_customer_url, tr_data[:redirect_url] - end - - test "new requires login" do - get :new - - assert_response :redirect - assert_redirected_to login_path - end - - test "edit uses params[:id]" do - customer = stub_customer - login customer.user - get :edit, id: customer.user.id - - assert_response :success - assert assigns(:tr_data) - tr_data = Braintree::Util.parse_query_string(assigns(:tr_data)) - assert_equal customer.braintree_customer_id, tr_data[:customer_id] - assert_equal confirm_customer_url, tr_data[:redirect_url] - end - - test "confirm customer creation" do - login - Braintree::TransparentRedirect.expects(:confirm).returns(success_response) - # to_confirm = prepare_confirmation :create_customer_data, - # customer: FactoryGirl.attributes_for(:braintree_customer), - # redirect_url: confirm_customer_url - - assert_difference("Customer.count") do - post :confirm, braintree: :query - end - - assert_response :success - assert result = assigns(:result) - assert result.success? - assert result.customer.id - end - - test "customer update" do - customer = stub_customer - customer.expects(:save) - login customer.user - Braintree::TransparentRedirect.expects(:confirm). - returns(success_response(customer)) - - assert_no_difference("Customer.count") do - post :confirm, query: :from_braintree - end - - assert_response :success - assert result = assigns(:result) - assert result.success? - assert_equal customer.braintree_customer, result.customer - end - - test "failed customer creation" do - skip "can't get customer creation to fail" - login - FakeBraintree.decline_all_cards! - # what is prepare_confirmation ?? this method isn't found - to_confirm = prepare_confirmation :create_customer_data, - customer: FactoryGirl.attributes_for(:broken_customer), - redirect_url: confirm_customer_url - post :confirm, to_confirm - - FakeBraintree.clear! - assert_response :success - assert result = assigns(:result) - assert !result.success? - end - - test "failed customer creation with stubbing" do - login - Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) - post :confirm, bla: :blub - - assert_response :success - assert_template :new - end - - test "failed customer update with stubbing" do - customer = stub_customer - login customer.user - Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) - post :confirm, bla: :blub - - assert_response :success - assert_template :edit - end - - def failure_response - stub success?: false, - errors: stub(for: nil, size: 0), - params: {} - end - - def success_response(customer = nil) - stub success?: true, - customer: braintree_customer(customer) - end - - def braintree_customer(customer) - if customer - customer.braintree_customer - else - FactoryGirl.build :braintree_customer - end - end - -end diff --git a/billing/test/functional/customers_controller_test.rb b/billing/test/functional/customers_controller_test.rb deleted file mode 100644 index 46c33c9..0000000 --- a/billing/test/functional/customers_controller_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class CustomersControllerTest < ActionController::TestCase - tests CustomerController - - setup do - @user = FactoryGirl.create :user - @other_user = FactoryGirl.create :user - #FakeBraintree.clear! - #FakeBraintree.verify_all_cards! - testid = 'testid' - #this wasn't actually being used - #FakeBraintree::Customer.new({:credit_cards => [{:number=>"5105105105105100", :expiration_date=>"05/2013"}]}, {:id => testid, :merchant_id => Braintree::Configuration.merchant_id}) - # any reason to call the create instance method on the FakeBraintree::Customer ? - @customer = Customer.new(:user_id => @other_user.id) - @customer.braintree_customer_id = testid - @customer.save - - end - - teardown do - @user.destroy - @other_user.destroy - @customer.destroy - end - - test "no access if not logged in" do - get :new - assert_access_denied(true, false) - get :show, :id => @customer.braintree_customer_id - assert_access_denied(true, false) - get :edit, :id => @customer.braintree_customer_id - assert_access_denied(true, false) - end - - - test "should get new if logged in and not customer" do - login @user - get :new - assert_not_nil assigns(:tr_data) - assert_response :success - end - - test "new should direct edit if user is already a customer" do - login @other_user - get :new - assert_response :redirect - assert_equal edit_customer_url(@customer.user), response.header['Location'] - end - - - test "show" do - skip "show customer" - login @other_user - # Below will fail, as when we go to fetch the customer data, Braintree::Customer.find(params[:id]) won't find the customer as it is a FakeBraintree customer. - #get :show, :id => @customer.braintree_customer_id - - end - -end diff --git a/billing/test/functional/payments_controller_test.rb b/billing/test/functional/payments_controller_test.rb deleted file mode 100644 index 90b7582..0000000 --- a/billing/test/functional/payments_controller_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class PaymentsControllerTest < ActionController::TestCase - include CustomerTestHelper - - test "payment when unauthorized" do - get :new - assert_not_nil assigns(:tr_data) - assert_response :success - end - - test "successful confirmation renders confirm" do - Braintree::TransparentRedirect.expects(:confirm).returns(success_response) - get :confirm - - assert_response :success - assert_template :confirm - end - - test "failed confirmation renders new" do - Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) - get :confirm - - assert_response :success - assert_not_nil assigns(:tr_data) - assert_template :new - end - - def failure_response - stub success?: false, - errors: stub(for: nil, size: 0), - params: {}, - transaction: stub(status: nil) - end - - def success_response - stub success?: true, - transaction: stub_transaction - end - - # that's what you get when not following the law of demeter... - def stub_transaction - stub amount: "100.00", - id: "ASDF", - customer_details: FactoryGirl.build(:braintree_customer), - credit_card_details: FactoryGirl.build(:braintree_customer).credit_cards.first - end - -end diff --git a/billing/test/functional/subscriptions_controller_test.rb b/billing/test/functional/subscriptions_controller_test.rb deleted file mode 100644 index a6a1057..0000000 --- a/billing/test/functional/subscriptions_controller_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class SubscriptionsControllerTest < ActionController::TestCase - include CustomerTestHelper - - test "destroy cancels subscription" do - customer = stub_customer - login customer.user - result = Braintree::Subscription.create plan_id: 'my_plan', - payment_method_token: customer.braintree_customer.credit_cards.first.token - subscription = result.subscription - delete :destroy, id: subscription.id, user_id: customer.user.id - assert_equal "Canceled", Braintree::Subscription.find(subscription.id).status - end -end diff --git a/billing/test/support/braintree_integration_test.rb b/billing/test/support/braintree_integration_test.rb deleted file mode 100644 index 976c5a2..0000000 --- a/billing/test/support/braintree_integration_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'capybara/rails' -# require 'fake_braintree' - messes up other integration tests -require 'braintree_test_app' - -class BraintreeIntegrationTest < BrowserIntegrationTest - include Warden::Test::Helpers - - setup do - Warden.test_mode! - Rails.application.config.middleware.use BraintreeTestApp - end - - teardown do - Warden.test_reset! - Rails.application.config.middleware.delete "BraintreeTestApp" - end - -end diff --git a/billing/test/support/customer_test_helper.rb b/billing/test/support/customer_test_helper.rb deleted file mode 100644 index adac00a..0000000 --- a/billing/test/support/customer_test_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module CustomerTestHelper - - def stub_customer(user = nil) - user ||= find_record :user - customer = stub_record :customer_with_payment_info, - user: user, - user_id: user.id - Customer.stubs(:find_by_user_id).with(user.id).returns(customer) - return customer - end -end diff --git a/billing/test/test_helper.rb b/billing/test/test_helper.rb deleted file mode 100644 index 1e26a31..0000000 --- a/billing/test/test_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Configure Rails Environment -ENV["RAILS_ENV"] = "test" - -require File.expand_path("../dummy/config/environment.rb", __FILE__) -require "rails/test_help" - -Rails.backtrace_cleaner.remove_silencers! - -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - -# Load fixtures from the engine -if ActiveSupport::TestCase.method_defined?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) -end diff --git a/billing/test/unit/customer_test.rb b/billing/test/unit/customer_test.rb deleted file mode 100644 index 6156f87..0000000 --- a/billing/test/unit/customer_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'test_helper' - -class CustomerTest < ActiveSupport::TestCase - include StubRecordHelper - - setup do - @user = find_record :user - @customer = FactoryGirl.build(:customer, user: @user) - end - - test "test set of attributes should be valid" do - @customer.valid? - assert_equal Hash.new, @customer.errors.messages - end - - test "customer belongs to user" do - assert_equal User, @customer.user.class - end - - test "user validation" do - @customer.user = nil - assert !@customer.valid? - end - - test "has no payment info" do - assert !@customer.braintree_customer_id - assert !@customer.has_payment_info? - end - - test "with no braintree data" do - assert_equal @customer, @customer.with_braintree_data! - end - - test "without default credit card" do - assert_nil @customer.default_credit_card - end - -end diff --git a/billing/test/unit/customer_with_payment_info_test.rb b/billing/test/unit/customer_with_payment_info_test.rb deleted file mode 100644 index 0589a59..0000000 --- a/billing/test/unit/customer_with_payment_info_test.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'test_helper' -require 'fake_braintree' - -class CustomerWithPaymentInfoTest < ActiveSupport::TestCase - include StubRecordHelper - - setup do - @user = find_record :user - @customer = FactoryGirl.build(:customer_with_payment_info, user: @user) - end - - test "has payment_info" do - assert @customer.braintree_customer_id - assert @customer.has_payment_info? - end - - test "constructs customer with braintree data" do - @customer.with_braintree_data! - assert_equal 'Big', @customer.first_name - assert_equal 'Spender', @customer.last_name - assert_equal 1, @customer.credit_cards.size - assert_equal Hash.new, @customer.custom_fields - end - - test "can access braintree_customer after reload" do - @customer.save - @customer = Customer.find_by_user_id(@customer.user_id) - @customer.with_braintree_data! - assert_equal 'Big', @customer.first_name - assert_equal 'Spender', @customer.last_name - assert_equal 1, @customer.credit_cards.size - assert_equal Hash.new, @customer.custom_fields - @customer.destroy - end - - test "sets default_credit_card" do - @customer.with_braintree_data! - assert_equal @customer.credit_cards.first, @customer.default_credit_card - end -end diff --git a/engines/billing/Gemfile b/engines/billing/Gemfile new file mode 100644 index 0000000..30e9669 --- /dev/null +++ b/engines/billing/Gemfile @@ -0,0 +1,23 @@ +source "https://rubygems.org" + +eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) +eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb')) + +# We require leap_web_core from here so we can use the path option. +gem "leap_web_core", :path => '../core' + +# Declare your gem's dependencies in billing.gemspec. +# Bundler will treat runtime dependencies like base dependencies, and +# development dependencies will be added by default to the :development group. +gemspec + +# jquery-rails is used by the dummy application +#gem "jquery-rails" + +# Declare any dependencies that are still in development here instead of in +# your gemspec. These might include edge Rails or gems from your path or +# Git. Remember to move these dependencies to your gemspec before releasing +# your gem to rubygems.org. + +# To use debugger +# gem 'debugger' diff --git a/engines/billing/README.md b/engines/billing/README.md new file mode 100644 index 0000000..3ef6153 --- /dev/null +++ b/engines/billing/README.md @@ -0,0 +1,49 @@ +Billing Engine +==================== + +Currently, this engine support billing via Braintree. More backends to come later. + +Configuration +---------------------------------- + +Start with a sandbox account, which you can get here: https://www.braintreepayments.com/get-started + +Once you have registered for the sandbox, logging in will show you three important variables you will need to configure: + +* merchantId +* publicKey +* privatekey + +To configure the billing engine, edit `config/config.yaml` like so: + + production: (or "development", as you prefer) + billing: + braintree: + environment: sandbox + merchant_id: Ohp2aijaaqu6oJ4w + public_key: ahnar0UwLahwe6Ce + private_key: aemie2Geohgah2EaOad9DeeruW4Iegh4 + +If deploying via puppet, the same data in webapp.json would like this: + + "billing": { + "braintree": { + "environment": "sandbox", + "merchant_id": "Ohp2aijaaqu6oJ4w", + "public_key": "ahnar0UwLahwe6Ce", + "private_key": "aemie2Geohgah2EaOad9DeeruW4Iegh4" + } + } + +Now, you should be able to add charges to your own sandbox when you run the webapp. + +The acceptable values for `billing.braintree.environment` are: `development`, `qa`, `sandbox`, or `production`. + +Plans +-------------------------------- + +You also will want to add a Plan to your Sandbox. Within the Braintree Sandbox, navigate to 'Recurring Billing' -> 'Plans'. From here, you can add a new Plan. The values of the test plan are not important, but the ID will be displayed, so should pick something descriptive. + +Here are credit cared numbers to try in the Sandbox: + +https://www.braintreepayments.com/docs/ruby/reference/sandbox \ No newline at end of file diff --git a/engines/billing/Rakefile b/engines/billing/Rakefile new file mode 100644 index 0000000..52929c4 --- /dev/null +++ b/engines/billing/Rakefile @@ -0,0 +1,40 @@ +#!/usr/bin/env rake +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end +begin + require 'rdoc/task' +rescue LoadError + require 'rdoc/rdoc' + require 'rake/rdoctask' + RDoc::Task = Rake::RDocTask +end + +RDoc::Task.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'LeapWebBilling' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +spec = eval(File.read('leap_web_billing.gemspec')) +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +Bundler::GemHelper.install_tasks + +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + + +task :default => :test diff --git a/engines/billing/app/controllers/billing_admin_controller.rb b/engines/billing/app/controllers/billing_admin_controller.rb new file mode 100644 index 0000000..e11d4ee --- /dev/null +++ b/engines/billing/app/controllers/billing_admin_controller.rb @@ -0,0 +1,29 @@ +class BillingAdminController < BillingBaseController + before_filter :require_admin + + def show + + br_atleast_90_days = Braintree::Subscription.search do |search| + search.days_past_due >= 90 + end + @past_due_atleast_90_days = braintree_resource_collection_to_array(br_atleast_90_days) + + br_all_past_due = Braintree::Subscription.search do |search| + search.status.is Braintree::Subscription::Status::PastDue + #cannot search by balance. + end + @all_past_due = braintree_resource_collection_to_array(br_all_past_due) + + end + + private + + def braintree_resource_collection_to_array(braintree_resource_collection) + array = [] + braintree_resource_collection.each do |object| + array << object + end + array + end + +end diff --git a/engines/billing/app/controllers/billing_base_controller.rb b/engines/billing/app/controllers/billing_base_controller.rb new file mode 100644 index 0000000..0453677 --- /dev/null +++ b/engines/billing/app/controllers/billing_base_controller.rb @@ -0,0 +1,22 @@ +class BillingBaseController < ApplicationController + before_filter :assign_user + + helper 'billing' + + # required for navigation to work. + def assign_user + if params[:user_id] + @user = User.find(params[:user_id]) + elsif params[:action] == "confirm"# confirms will come back with different ID set, so check for this first + # This is only for cases where an admin cannot apply action for customer, but should be all confirms + @user = current_user + elsif params[:id] + @user = User.find(params[:id]) + else + # TODO + # hacky, what are cases where @user hasn't yet been set? certainly some cases with subscriptions and payments + @user = current_user + end + end + +end diff --git a/engines/billing/app/controllers/credit_card_info_controller.rb b/engines/billing/app/controllers/credit_card_info_controller.rb new file mode 100644 index 0000000..fbaa6f1 --- /dev/null +++ b/engines/billing/app/controllers/credit_card_info_controller.rb @@ -0,0 +1,35 @@ +class CreditCardInfoController < ApplicationController + before_filter :require_login, :set_user + + def edit + @credit_card = Braintree::CreditCard.find(params[:id]) + customer = Customer.find_by_user_id(@user.id) + if customer and customer.braintree_customer_id == @credit_card.customer_id + @tr_data = Braintree::TransparentRedirect. + update_credit_card_data(:redirect_url => confirm_credit_card_info_url, + :payment_method_token => @credit_card.token) + else + access_denied + end + + end + + def confirm + @result = Braintree::TransparentRedirect.confirm(request.query_string) + if @result.success? + render :action => "confirm" + else + @credit_card = Braintree::CreditCard.find(@result.params[:payment_method_token]) + render :action => "edit" + end + end + + + private + + def set_user + # this assumes anybody, even an admin, will not access for another user. + @user = current_user + end + +end diff --git a/engines/billing/app/controllers/customer_controller.rb b/engines/billing/app/controllers/customer_controller.rb new file mode 100644 index 0000000..6cbcb44 --- /dev/null +++ b/engines/billing/app/controllers/customer_controller.rb @@ -0,0 +1,64 @@ +class CustomerController < BillingBaseController + before_filter :require_login, :fetch_customer + + def show + if @customer + @customer.with_braintree_data! + @default_cc = @customer.default_credit_card + @active_subscription = @customer.subscriptions + @transactions = @customer.braintree_customer.transactions + end + end + + def new + if @customer.has_payment_info? + redirect_to edit_customer_path(@user), :notice => 'Here is your saved customer data' + else + fetch_new_transparent_redirect_data + end + end + + def edit + fetch_edit_transparent_redirect_data + end + + def confirm + @result = Braintree::TransparentRedirect.confirm(request.query_string) + if @result.success? + @customer.braintree_customer = @result.customer + @customer.save + render :action => "confirm" + elsif @customer.has_payment_info? + fetch_edit_transparent_redirect_data + render :action => "edit" + else + fetch_new_transparent_redirect_data + render :action => "new" + end + end + + protected + + def fetch_new_transparent_redirect_data + access_denied unless @user == current_user # admins cannot do this for others + @tr_data = Braintree::TransparentRedirect. + create_customer_data(:redirect_url => confirm_customer_url) + end + + def fetch_edit_transparent_redirect_data + access_denied unless @user == current_user # admins cannot do this for others + @customer.with_braintree_data! + @default_cc = @customer.default_credit_card + @tr_data = Braintree::TransparentRedirect. + update_customer_data(:redirect_url => confirm_customer_url, + :customer_id => @customer.braintree_customer_id) ##?? + end + + def fetch_customer + @customer = Customer.find_by_user_id(@user.id) + if @user == current_user + @customer ||= Customer.new(user: @user) + end + access_denied unless (@customer and (@customer.user == current_user)) or admin? + end +end diff --git a/engines/billing/app/controllers/payments_controller.rb b/engines/billing/app/controllers/payments_controller.rb new file mode 100644 index 0000000..fce6570 --- /dev/null +++ b/engines/billing/app/controllers/payments_controller.rb @@ -0,0 +1,34 @@ +class PaymentsController < BillingBaseController + before_filter :require_login, :only => [:index] + + def new + fetch_transparent_redirect + end + + def confirm + @result = Braintree::TransparentRedirect.confirm(request.query_string) + if @result.success? + render :action => "confirm" + else + fetch_transparent_redirect + render :action => "new" + end + end + + def index + access_denied unless admin? or (@user == current_user) + customer = Customer.find_by_user_id(@user.id) + braintree_data = Braintree::Customer.find(customer.braintree_customer_id) + # these will be ordered by created_at descending, per http://stackoverflow.com/questions/16425475/ + @transactions = braintree_data.transactions + end + + protected + + + def fetch_transparent_redirect + @tr_data = Braintree::TransparentRedirect.transaction_data redirect_url: confirm_payment_url, + transaction: { type: "sale", options: {submit_for_settlement: true } } + end + +end diff --git a/engines/billing/app/controllers/subscriptions_controller.rb b/engines/billing/app/controllers/subscriptions_controller.rb new file mode 100644 index 0000000..f066b3c --- /dev/null +++ b/engines/billing/app/controllers/subscriptions_controller.rb @@ -0,0 +1,63 @@ +class SubscriptionsController < BillingBaseController + before_filter :require_login + before_filter :fetch_subscription, :only => [:show, :destroy] + before_filter :confirm_cancel_subscription, :only => [:destroy] + before_filter :confirm_self_or_admin, :only => [:index] + before_filter :confirm_no_pending_active_pastdue_subscription, :only => [:new, :create] + # for now, admins cannot create or destroy subscriptions for others: + before_filter :confirm_self, :only => [:new, :create] + + def new + # don't show link to subscribe if they are already subscribed? + credit_card = @customer.default_credit_card #safe to assume default? + @payment_method_token = credit_card.token + @plans = Braintree::Plan.all + end + + # show has no content, so not needed at this point. + + def create + @result = Braintree::Subscription.create( :payment_method_token => params[:payment_method_token], :plan_id => params[:plan_id] ) + #if you want to test pastdue, can add :price => '2001', :trial_period => true,:trial_duration => 1,:trial_duration_unit => "day" and then wait a day + end + + def destroy + @result = Braintree::Subscription.cancel params[:id] + end + + def index + customer = Customer.find_by_user_id(@user.id) + @subscriptions = customer.subscriptions(nil, false) + end + + private + + def fetch_subscription + @subscription = Braintree::Subscription.find params[:id] + @credit_card = Braintree::CreditCard.find @subscription.payment_method_token + @subscription_customer_id = @credit_card.customer_id + current_user_customer = Customer.find_by_user_id(current_user.id) + access_denied unless admin? or (current_user_customer and current_user_customer.braintree_customer_id == @subscription_customer_id) + + end + + def confirm_cancel_subscription + access_denied unless view_context.allow_cancel_subscription(@subscription) + end + + def confirm_no_pending_active_pastdue_subscription + @customer = Customer.find_by_user_id(@user.id) + if subscription = @customer.subscriptions # will return pending, active or pastdue subscription, if it exists + redirect_to user_subscription_path(@user, subscription.id), :notice => 'You already have a subscription' + end + end + + def confirm_self + @user == current_user + end + + def confirm_self_or_admin + access_denied unless confirm_self or admin? + end + +end diff --git a/engines/billing/app/helpers/billing_helper.rb b/engines/billing/app/helpers/billing_helper.rb new file mode 100644 index 0000000..b9e5e2e --- /dev/null +++ b/engines/billing/app/helpers/billing_helper.rb @@ -0,0 +1,51 @@ +module BillingHelper + + def braintree_form_for(object, options = {}, &block) + options.reverse_merge! params: @result && @result.params[object], + errors: @result && @result.errors.for(object), + builder: BraintreeFormHelper::BraintreeFormBuilder, + url: Braintree::TransparentRedirect.url + + form_for object, options, &block + end + + def billing_top_link(user) + # for admins, top link will show special admin information, which has link to show their own customer information + if (admin? and user == current_user) + billing_admin_path + else + show_or_new_customer_link(user) + end + end + + def show_or_new_customer_link(user) + # Link to show if user is admin viewing another user, or user is already a customer. + # Otherwise link to create a new customer. + if (admin? and (user != current_user)) or ((customer = Customer.find_by_user_id(user.id)) and customer.has_payment_info?) + show_customer_path(user) + else + new_customer_path + end + end + + # a bit strange to put here, but we don't have a subscription model + def user_for_subscription(subscription) + + if (transaction = subscription.transactions.first) + # much quicker, but will only work if there is already a transaction associated with subscription (should generally be) + braintree_customer_id = transaction.customer_details.id + else + credit_card = Braintree::CreditCard.find(subscription.payment_method_token) + braintree_customer_id = credit_card.customer_id + end + + customer = Customer.find_by_braintree_customer_id(braintree_customer_id) + user = User.find(customer.user_id) + + end + + def allow_cancel_subscription(subscription) + ['Active', 'Pending'].include? subscription.status or (admin? and subscription.status == 'Past Due') + end + +end diff --git a/engines/billing/app/helpers/braintree_form_helper.rb b/engines/billing/app/helpers/braintree_form_helper.rb new file mode 100644 index 0000000..cb322fa --- /dev/null +++ b/engines/billing/app/helpers/braintree_form_helper.rb @@ -0,0 +1,64 @@ +module BraintreeFormHelper + class BraintreeFormBuilder < ActionView::Helpers::FormBuilder + include ActionView::Helpers::AssetTagHelper + include ActionView::Helpers::TagHelper + + def initialize(object_name, object, template, options, proc) + super + @braintree_params = @options[:params] + @braintree_errors = @options[:errors] + @braintree_existing = @options[:existing] + end + + def fields_for(record_name, *args, &block) + options = args.extract_options! + options[:builder] = BraintreeFormBuilder + options[:params] = @braintree_params && @braintree_params[record_name] + options[:errors] = @braintree_errors && @braintree_errors.for(record_name) + new_args = args + [options] + super record_name, *new_args, &block + end + + def text_field(method, options = {}) + has_errors = @braintree_errors && @braintree_errors.on(method).any? + field = super(method, options.merge(:value => determine_value(method))) + result = content_tag("div", field, :class => has_errors ? "fieldWithErrors" : "") + result.safe_concat validation_errors(method) + result + end + + protected + + def determine_value(method) + if @braintree_params + @braintree_params[method] + elsif @braintree_existing + + if @braintree_existing.kind_of?(Braintree::CreditCard) + + case method + when :number + method = :masked_number + when :cvv + return nil + end + end + + @braintree_existing.send(method) + else + nil + end + end + + def validation_errors(method) + if @braintree_errors && @braintree_errors.on(method).any? + @braintree_errors.on(method).map do |error| + content_tag("div", ERB::Util.h(error.message), {:style => "color: red;"}) + end.join + else + "" + end + end + end +end + diff --git a/engines/billing/app/helpers/braintree_helper.rb b/engines/billing/app/helpers/braintree_helper.rb new file mode 100644 index 0000000..2d18b6c --- /dev/null +++ b/engines/billing/app/helpers/braintree_helper.rb @@ -0,0 +1,5 @@ +module BraintreeHelper + + +end + diff --git a/engines/billing/app/models/customer.rb b/engines/billing/app/models/customer.rb new file mode 100644 index 0000000..1acc7a5 --- /dev/null +++ b/engines/billing/app/models/customer.rb @@ -0,0 +1,58 @@ +class Customer < CouchRest::Model::Base + + FIELDS = [:first_name, :last_name, :phone, :website, :company, :fax, :addresses, :credit_cards, :custom_fields] + attr_accessor *FIELDS + + use_database "customers" + belongs_to :user + belongs_to :braintree_customer + + # Braintree::Customer - stored on braintrees servers - we only have the id. + def braintree_customer + @braintree_customer ||= Braintree::Customer.find(braintree_customer_id) + end + + validates :user, presence: true + + design do + view :by_user_id + view :by_braintree_customer_id + end + + def has_payment_info? + !!braintree_customer_id + end + + # from braintree_ruby_examples/rails3_tr_devise and should be tweaked + def with_braintree_data! + return self unless has_payment_info? + + FIELDS.each do |field| + send(:"#{field}=", braintree_customer.send(field)) + end + self + end + + def default_credit_card + return unless has_payment_info? + + credit_cards.find { |cc| cc.default? } + end + + # based on 2nd parameter, either returns the single active subscription (or nil if there isn't one), or an array of all subsciptions + def subscriptions(braintree_data=nil, only_pending_active_pastdue=true) + self.with_braintree_data! + return unless has_payment_info? + + subscriptions = [] + self.default_credit_card.subscriptions.each do |sub| + if only_pending_active_pastdue and ['Pending', 'Active','Past Due'].include? sub.status + return sub + else + subscriptions << sub + end + end + only_pending_active_pastdue ? nil : subscriptions + end + +end diff --git a/engines/billing/app/views/billing_admin/show.html.haml b/engines/billing/app/views/billing_admin/show.html.haml new file mode 100644 index 0000000..0382cf0 --- /dev/null +++ b/engines/billing/app/views/billing_admin/show.html.haml @@ -0,0 +1,7 @@ +%legend= t(:more_than_90_days_past_due) += render(:partial => "subscriptions/subscription_details", :collection => @past_due_atleast_90_days, :as => 'subscription', :locals => {:show_user => true}) || t(:none) +%legend= t(:all_past_due) += render(:partial => "subscriptions/subscription_details", :collection => @all_past_due, :as => 'subscription', :locals => {:show_user => true}) || t(:none) + +%legend= t(:your_settings) += link_to 'view own billing settings', show_or_new_customer_link(current_user) \ No newline at end of file diff --git a/engines/billing/app/views/credit_card_info/confirm.html.haml b/engines/billing/app/views/credit_card_info/confirm.html.haml new file mode 100644 index 0000000..9dd8176 --- /dev/null +++ b/engines/billing/app/views/credit_card_info/confirm.html.haml @@ -0,0 +1,5 @@ +%h1 Payment Info Confirmation +%p Your payment information was successfully saved. +%dl + %dt Credit Card + %dd= @result.credit_card.masked_number diff --git a/engines/billing/app/views/credit_card_info/edit.html.haml b/engines/billing/app/views/credit_card_info/edit.html.haml new file mode 100644 index 0000000..bd86a4c --- /dev/null +++ b/engines/billing/app/views/credit_card_info/edit.html.haml @@ -0,0 +1,17 @@ +%h1 Change Credit Card +- if @result + #total-errors{:style => "color:red;"} + = h(@result.errors.size) + error(s) += braintree_form_for :credit_card, :existing => @credit_card do |f| + = field_set_tag "Credit Card" do + %dl + %dt= f.label :number, 'Number' + %dd= f.text_field :number + %dt= f.label :expiration_date, 'Expiration Date (MM/YY)' + %dd= f.text_field :expiration_date + %dt= f.label :cvv, 'CVV' + %dd= f.text_field :cvv + = hidden_field_tag :tr_data, @tr_data + = f.submit 'Save Payment Info', :class => :btn + = link_to t(:cancel), edit_customer_path(@user.id), :class => :btn diff --git a/engines/billing/app/views/customer/_customer_data.html.haml b/engines/billing/app/views/customer/_customer_data.html.haml new file mode 100644 index 0000000..e9df040 --- /dev/null +++ b/engines/billing/app/views/customer/_customer_data.html.haml @@ -0,0 +1,16 @@ +%legend= t(:customer_information) +%dl + %dt First Name + %dd= @customer.first_name + %dt Last Name + %dd= @customer.last_name + %dt Phone + %dd= @customer.phone +%legend= t(:credit_card_information) +%dl + %dt Number + %dd= @default_cc.masked_number + %dt Expiration Date + %dd= @default_cc.expiration_date + - if current_user == @user + = link_to t(:edit_saved_data), edit_customer_path(@user.id), :class => :btn diff --git a/engines/billing/app/views/customer/_transaction.html.haml b/engines/billing/app/views/customer/_transaction.html.haml new file mode 100644 index 0000000..e69de29 diff --git a/engines/billing/app/views/customer/confirm.html.haml b/engines/billing/app/views/customer/confirm.html.haml new file mode 100644 index 0000000..877a8ac --- /dev/null +++ b/engines/billing/app/views/customer/confirm.html.haml @@ -0,0 +1,14 @@ +%h1 Payment Info Confirmation +%p Your payment information was successfully saved. +%dl + %dt First Name + %dd= @result.customer.first_name + %dt Last Name + %dd= @result.customer.last_name + %dt Phone + %dd= @result.customer.phone + %dt Credit Card + - @result.customer.credit_cards.each do |cc| + %dd= cc.masked_number +- customer = Customer.find_by_user_id(@user.id) += link_to 'View Customer Info', show_customer_path(@user.id), :class=> :btn \ No newline at end of file diff --git a/engines/billing/app/views/customer/edit.html.haml b/engines/billing/app/views/customer/edit.html.haml new file mode 100644 index 0000000..e882d53 --- /dev/null +++ b/engines/billing/app/views/customer/edit.html.haml @@ -0,0 +1,23 @@ +- if @result + #total-errors{:style => "color:red;"} + = h(@result.errors.size) + error(s) += braintree_form_for :customer, existing: @customer do |f| + = field_set_tag "Customer" do + %dl + %dt= f.label :first_name, 'First Name' + %dd= f.text_field :first_name + %dt= f.label :last_name, 'Last Name' + %dd= f.text_field :last_name + %dt= f.label :phone, 'Phone' + %dd= f.text_field :phone + - if @default_cc + = # todo, as they will need a credit card, so not sure about conditional? + %dt= t(:stored_credit_card) + %dd + = @default_cc.masked_number + = link_to t(:change_credit_card), edit_credit_card_info_path(:id => @default_cc.token), :class => :btn + = hidden_field_tag :tr_data, @tr_data + .form-actions + = f.submit t(:save_customer_info), :class => 'btn btn-primary' + = link_to t(:cancel), show_customer_path(@user), :class=> :btn diff --git a/engines/billing/app/views/customer/new.html.haml b/engines/billing/app/views/customer/new.html.haml new file mode 100644 index 0000000..e1f5ba9 --- /dev/null +++ b/engines/billing/app/views/customer/new.html.haml @@ -0,0 +1,24 @@ +- if @result + #total-errors{:style => "color:red;"} + = h(@result.errors.size) + error(s) += braintree_form_for :customer do |f| + = field_set_tag "Customer" do + %dl + %dt= f.label :first_name, 'First Name' + %dd= f.text_field :first_name + %dt= f.label :last_name, 'Last Name' + %dd= f.text_field :last_name + %dt= f.label :phone, 'Phone' + %dd= f.text_field :phone + = field_set_tag "Credit Card" do + - f.fields_for :credit_card do |cc| + %dl + %dt= cc.label :number, 'Number' + %dd= cc.text_field :number + %dt= cc.label :expiration_date, 'Expiration Date (MM/YY)' + %dd= cc.text_field :expiration_date + %dt= cc.label :cvv, 'CVV' + %dd= cc.text_field :cvv + = hidden_field_tag :tr_data, @tr_data + = f.submit 'Save Payment Info' diff --git a/engines/billing/app/views/customer/show.html.haml b/engines/billing/app/views/customer/show.html.haml new file mode 100644 index 0000000..ec1779c --- /dev/null +++ b/engines/billing/app/views/customer/show.html.haml @@ -0,0 +1,27 @@ +- if admin? and !@customer + = t(:no_saved_customer) +- else + = render :partial => 'customer_data' + %legend= t(:last_three_transactions) + - counter = 0 + = # these will be ordered with most recently created first, per http://stackoverflow.com/questions/16425475/ + - @transactions.each do |t| + - break if counter > 2 # not ruby-like, but object is a Braintree::ResourceCollection so limited methods available + = render :partial => "payments/transaction_details", :locals => {:transaction => t} + - counter += 1 + = link_to t(:transaction_history), user_payments_path(@user) + %legend= t(:subscriptions) + - if @active_subscription + = render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription} + - else + %p + = t(:no_relevant_subscription) + - if current_user == @user + %p + .form-actions + = link_to t(:subscribe_to_plan), new_subscription_path, :class => :btn + %p + = link_to t(:all_subscriptions), user_subscriptions_path(@user) + +.form-actions + = link_to t(:make_donation), new_payment_path, :class => 'btn btn-primary' diff --git a/engines/billing/app/views/payments/_non_customer_fields.html.haml b/engines/billing/app/views/payments/_non_customer_fields.html.haml new file mode 100644 index 0000000..77cfe95 --- /dev/null +++ b/engines/billing/app/views/payments/_non_customer_fields.html.haml @@ -0,0 +1,16 @@ += field_set_tag "Personal Information" do + = f.fields_for :customer do |c| + %div= c.label :first_name, "First Name" + %div= c.text_field :first_name + %div= c.label :last_name, "Last Name" + %div= c.text_field :last_name + %div= c.label :email, "Email" + %div= c.text_field :email += field_set_tag "Credit Card" do + = f.fields_for :credit_card do |c| + %div= c.label :number, "Number" + %div= c.text_field :number + %div= c.label :expiration_date, "Expiration Date (MM/YY)" + %div= c.text_field :expiration_date + %div= c.label :cvv, "CVV" + %div= c.text_field :cvv \ No newline at end of file diff --git a/engines/billing/app/views/payments/_transaction_details.html.haml b/engines/billing/app/views/payments/_transaction_details.html.haml new file mode 100644 index 0000000..85e4f6a --- /dev/null +++ b/engines/billing/app/views/payments/_transaction_details.html.haml @@ -0,0 +1,15 @@ +%p + = transaction.id + Type: + = transaction.type + Amount: + = number_to_currency(transaction.amount) + Status: + = transaction.status + Date + = transaction.created_at.strftime("%Y-%m-%d") + - if sub_start = transaction.subscription_details.billing_period_start_date + From subscription which started + = sub_start + - else # should not have any of these + Not paid as part of subscription \ No newline at end of file diff --git a/engines/billing/app/views/payments/confirm.html.haml b/engines/billing/app/views/payments/confirm.html.haml new file mode 100644 index 0000000..45af3c9 --- /dev/null +++ b/engines/billing/app/views/payments/confirm.html.haml @@ -0,0 +1,26 @@ +%h1 Payment Result +%div Thank you for your donation. +%h2 Transaction Details +%table + %tr + %td Amount + %td + $#{@result.transaction.amount} + %tr + %td Transaction ID: + %td= @result.transaction.id + %tr + %td First Name: + %td= h @result.transaction.customer_details.first_name + %tr + %td Last Name: + %td= h @result.transaction.customer_details.last_name + %tr + %td Email: + %td= h @result.transaction.customer_details.email + %tr + %td Credit Card: + %td= h @result.transaction.credit_card_details.masked_number + %tr + %td Card Type: + %td= h @result.transaction.credit_card_details.card_type \ No newline at end of file diff --git a/engines/billing/app/views/payments/index.html.haml b/engines/billing/app/views/payments/index.html.haml new file mode 100644 index 0000000..7a89917 --- /dev/null +++ b/engines/billing/app/views/payments/index.html.haml @@ -0,0 +1,5 @@ +%h2=t :transaction_history +- if (@transactions.count == 0) + = t(:no_transaction_history) +- @transactions.each do |t| + = render :partial => "transaction_details", :locals => {:transaction => t} \ No newline at end of file diff --git a/engines/billing/app/views/payments/new.html.haml b/engines/billing/app/views/payments/new.html.haml new file mode 100644 index 0000000..e9a8273 --- /dev/null +++ b/engines/billing/app/views/payments/new.html.haml @@ -0,0 +1,17 @@ +%h1 + = t(:Donation) +- if logged_in? + = t(:donation_not_payment) +- if @result and @result.errors.size > 0 + %div{:style => "color: red;"} + = h @result.errors.size + error(s) +- if @result and @result.transaction and @result.transaction.status != 'success' + %div{:style => "color: red;"} + = t(:processor_declined) += braintree_form_for :transaction, :html => {:autocomplete => "off"} do |f| + = f.label :amount, t(:amount) + = f.text_field :amount + = render :partial => 'non_customer_fields', :locals => {:f => f} + = hidden_field_tag :tr_data, @tr_data + = f.submit "Submit Donation", :class => 'btn btn-primary' diff --git a/engines/billing/app/views/subscriptions/_subscription_details.html.haml b/engines/billing/app/views/subscriptions/_subscription_details.html.haml new file mode 100644 index 0000000..6145c95 --- /dev/null +++ b/engines/billing/app/views/subscriptions/_subscription_details.html.haml @@ -0,0 +1,26 @@ +%p + - if local_assigns[:show_user] + User: + - user_to_show = user_for_subscription(subscription) + = link_to user_to_show.login, user_overview_path(user_to_show) + ID: + = link_to subscription.id, user_subscription_path(@user, subscription.id) + Balance: + - color = (subscription.balance > 0) ? "red" : "" + %font{:color => color} + = number_to_currency(subscription.balance) + Bill on: + = subscription.billing_day_of_month + Start date: + = subscription.first_billing_date + Paid through: + = subscription.paid_through_date + Plan: + = subscription.plan_id + Price: + = number_to_currency(subscription.price) + - color = (subscription.status == 'Active') ? "green" : "red" + Status: + %font{:color => color} + = subscription.status + - # would be good to get plan name but not sure if that is possible? \ No newline at end of file diff --git a/engines/billing/app/views/subscriptions/create.html.haml b/engines/billing/app/views/subscriptions/create.html.haml new file mode 100644 index 0000000..2b6c5e9 --- /dev/null +++ b/engines/billing/app/views/subscriptions/create.html.haml @@ -0,0 +1,9 @@ +- if @result.success? + %h1 + Subscription Status + = @result.subscription.status + = render :partial => "subscription_details", :locals => {:subscription => @result.subscription} +- else + %h1 + Error: + = @result.message \ No newline at end of file diff --git a/engines/billing/app/views/subscriptions/destroy.html.haml b/engines/billing/app/views/subscriptions/destroy.html.haml new file mode 100644 index 0000000..44b4333 --- /dev/null +++ b/engines/billing/app/views/subscriptions/destroy.html.haml @@ -0,0 +1,7 @@ +- if @result.success? + Subscription destroyed +- else + Error: + = @result.message +%p + = link_to 'Customer Information', show_customer_path(@user), :class=> :btn \ No newline at end of file diff --git a/engines/billing/app/views/subscriptions/index.html.haml b/engines/billing/app/views/subscriptions/index.html.haml new file mode 100644 index 0000000..3d4e8fd --- /dev/null +++ b/engines/billing/app/views/subscriptions/index.html.haml @@ -0,0 +1,8 @@ +%h2=t :all_subscriptions +- pending_active_pastdue = false +- @subscriptions.each do |s| + - if ['Pending', 'Active','Past Due'].include? s.status + - pending_active_pastdue = true + = render :partial => "subscription_details", :locals => {:subscription => s} +- if !pending_active_pastdue and @user == current_user + = link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file diff --git a/engines/billing/app/views/subscriptions/new.html.haml b/engines/billing/app/views/subscriptions/new.html.haml new file mode 100644 index 0000000..4183458 --- /dev/null +++ b/engines/billing/app/views/subscriptions/new.html.haml @@ -0,0 +1,15 @@ +- if @payment_method_token + %h1 + Subscribe to plan + = #currently just one plan + = @plans[0].name + = number_to_currency(@plans[0].price) + = simple_form_for :subscription, :url => :subscriptions do |f| + = hidden_field_tag :payment_method_token, @payment_method_token + = hidden_field_tag :plan_id, @plans[0].id + .form-actions + = f.submit t(:subscribe), :class => 'btn btn-primary' +- else + = t(:must_create_customer) + %p + = link_to t(:create_new_customer), new_customer_path diff --git a/engines/billing/app/views/subscriptions/show.html.haml b/engines/billing/app/views/subscriptions/show.html.haml new file mode 100644 index 0000000..2699db9 --- /dev/null +++ b/engines/billing/app/views/subscriptions/show.html.haml @@ -0,0 +1,6 @@ +%h1 + - if @subscription.status == 'Active' + Current + Subscription += render :partial => "subscription_details", :locals => {:subscription => @subscription} += link_to t(:cancel_subscription), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if allow_cancel_subscription(@subscription) diff --git a/engines/billing/config/initializers/braintree.rb b/engines/billing/config/initializers/braintree.rb new file mode 100644 index 0000000..3d87f4c --- /dev/null +++ b/engines/billing/config/initializers/braintree.rb @@ -0,0 +1,23 @@ +# +# set logger +# +if APP_CONFIG[:logfile].blank? + require 'syslog/logger' + Braintree::Configuration.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new('webapp')) +else + Braintree::Configuration.logger = Logger.new('log/braintree.log') +end + +# +# You can set these per environment in config/config.yml: +# +# Environment must be one of: :development, :qa, :sandbox, :production +# +if billing = APP_CONFIG[:billing] + if braintree = billing[:braintree] + Braintree::Configuration.environment = braintree[:environment].downcase.to_sym + Braintree::Configuration.merchant_id = braintree[:merchant_id] + Braintree::Configuration.public_key = braintree[:public_key] + Braintree::Configuration.private_key = braintree[:private_key] + end +end diff --git a/engines/billing/config/locales/en.yml b/engines/billing/config/locales/en.yml new file mode 100644 index 0000000..1300958 --- /dev/null +++ b/engines/billing/config/locales/en.yml @@ -0,0 +1,11 @@ +en: + create_new_customer: "Create a new Braintree Customer" + must_create_customer: "You must store a customer in braintree before subscribing to a plan" + subscribe: "Subscribe" + save_customer_info: "Save Customer Information" + donation_not_payment: "Note: This is a donation, and will not be applied towards your account." + no_relevant_subscription: "No subscription which is Active, Pending, or Past Due" + plan: "Plan" + description: "Description" + cost: "Cost" + free: "Free" \ No newline at end of file diff --git a/engines/billing/config/routes.rb b/engines/billing/config/routes.rb new file mode 100644 index 0000000..7263dff --- /dev/null +++ b/engines/billing/config/routes.rb @@ -0,0 +1,25 @@ +Rails.application.routes.draw do + + scope "(:locale)", :locale => MATCH_LOCALE do + match 'payments/new' => 'payments#new', :as => :new_payment + match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment + resources :users do + resources :payments, :only => [:index] + resources :subscriptions, :only => [:index, :show, :destroy] + end + + resources :customer, :only => [:new, :edit] + resources :credit_card_info, :only => [:edit] + + match 'customer/confirm/' => 'customer#confirm', :as => :confirm_customer + match 'customer/show/:id' => 'customer#show', :as => :show_customer + match 'credit_card_info/confirm' => 'credit_card_info#confirm', :as => :confirm_credit_card_info + + resources :subscriptions, :only => [:new, :create, :update] # index, show & destroy are within users path + match 'billing_admin' => 'billing_admin#show', :as => :billing_admin + + #match 'transactions/:product_id/new' => 'transactions#new', :as => :new_transaction + #match 'transactions/confirm/:product_id' => 'transactions#confirm', :as => :confirm_transaction + end + +end diff --git a/engines/billing/leap_web_billing.gemspec b/engines/billing/leap_web_billing.gemspec new file mode 100644 index 0000000..c6ac3f3 --- /dev/null +++ b/engines/billing/leap_web_billing.gemspec @@ -0,0 +1,21 @@ +$:.push File.expand_path("../lib", __FILE__) + +require File.expand_path('../../../lib/leap_web/version.rb', __FILE__) + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "leap_web_billing" + s.version = LeapWeb::VERSION + s.authors = ["Jessib"] + s.email = ["jessib@leap.se"] + s.homepage = "http://www.leap.se" + s.summary = "Billing for LeapWeb" + s.description = "Billing System for a Leap provider" + + s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.test_files = Dir["test/**/*"] + + # s.add_dependency "braintree-rails", "~> 0.4.5" + s.add_dependency "braintree" + #s.add_dependency "carmen-rails" +end diff --git a/engines/billing/lib/braintree_test_app.rb b/engines/billing/lib/braintree_test_app.rb new file mode 100644 index 0000000..41c327d --- /dev/null +++ b/engines/billing/lib/braintree_test_app.rb @@ -0,0 +1,36 @@ +# RackTest assumes all requests to be local. +# Braintree requests need to go out to a different server though. +# So we use a middleware to catch these and send them out again. + +class BraintreeTestApp + def initialize(app) + @app = app + end + + def call(env) + @env = env + config = Braintree::Configuration.instantiate + if request.path =~ /\/merchants\/#{config.merchant_id}\/transparent_redirect_requests$/ + #proxy post to braintree + uri = URI.parse(config.protocol + "://" + config.server + ":" + + config.port.to_s + request.path) + http = Net::HTTP.new(uri.host, uri.port) + res = http.post(uri.path, request.body.read) + + if res.code == "303" + header_hash = res.header.to_hash + header_hash["location"].first.gsub!("http://localhost:3000/", "http://www.example.com/") + [303, {"location" => header_hash["location"].first}, ""] + else + raise "unexpected response from Braintree: expected a 303" + end + else + @app.call(env) + end + end + + def request + @request = Rack::Request.new(@env) + end +end + diff --git a/engines/billing/lib/leap_web_billing.rb b/engines/billing/lib/leap_web_billing.rb new file mode 100644 index 0000000..288d846 --- /dev/null +++ b/engines/billing/lib/leap_web_billing.rb @@ -0,0 +1,4 @@ +require "leap_web_billing/engine" + +module LeapWebBilling +end diff --git a/engines/billing/lib/leap_web_billing/engine.rb b/engines/billing/lib/leap_web_billing/engine.rb new file mode 100644 index 0000000..ab574f2 --- /dev/null +++ b/engines/billing/lib/leap_web_billing/engine.rb @@ -0,0 +1,10 @@ +# thou shall require all your dependencies in an engine. +#require "braintree-rails" +require "braintree" +#require "carmen-rails" + +module LeapWebBilling + class Engine < ::Rails::Engine + + end +end diff --git a/engines/billing/script/rails b/engines/billing/script/rails new file mode 100755 index 0000000..8bd9c0a --- /dev/null +++ b/engines/billing/script/rails @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/leap_web_billing/engine', __FILE__) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/engines/billing/test/broken/admin_customer_test.rb b/engines/billing/test/broken/admin_customer_test.rb new file mode 100644 index 0000000..df92a0d --- /dev/null +++ b/engines/billing/test/broken/admin_customer_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' +require 'fake_braintree' + +class AdminCustomerTest < BraintreeIntegrationTest + + setup do + @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') + @user = FactoryGirl.create(:user) + end + + teardown do + @user.destroy if @user + @admin.destroy if @admin + end + + test "check non customer as admin" do + login_as @admin + visit '/' + click_link 'Users' + click_link @user.login + click_link 'Billing Settings' + assert page.has_content? @user.email_address + assert page.has_content? 'No Saved Customer' + end + + test "check customer as admin" do + skip "cannot check customer as admin" + # it would be good to have a test where an admin tries to view the 'Billing Settings' for another user. + # However, partially due to limitations of FakeBraintree, this doesn't seem pursuing at this time. + end +end diff --git a/engines/billing/test/broken/customer_creation_test.rb b/engines/billing/test/broken/customer_creation_test.rb new file mode 100644 index 0000000..90319a9 --- /dev/null +++ b/engines/billing/test/broken/customer_creation_test.rb @@ -0,0 +1,84 @@ +require 'test_helper' +require 'fake_braintree' + +class CustomerCreationTest < BraintreeIntegrationTest + + setup do + @user = FactoryGirl.create(:user) + login_as @user + end + + teardown do + @user.destroy + end + + # Let's test both steps together with capybara + # + # This test is nice and clean but also a bit fragile: + # RackTest assumes all requests to be local. So we need + # BraintreeTestApp for the braintree transparent redirect to work. + # + # this mystifies me why this works. when i type the click_button line (and the + # customer.braintree_customer line) in the debugger, it gives a timeout, + # but it works fine embedded in the test. + test "create customer with braintree" do + visit '/' + click_link 'Billing Settings' + # i am a bit unclear why this works, as it seems there will be validation errors + assert_difference("Customer.count") do + click_button 'Save Payment Info' # this gives me a timeout + end + assert customer = Customer.find_by_user_id(@user.id) + assert customer.braintree_customer + end + + # We only test the confirmation here. + # The request to Braintree is triggered outside of rails + # In skippped test below, we see this works even if the attributes are + # for a broken customer + test "successfully confirms customer creation" do + response = post_transparent_redirect :create_customer_data, + customer: FactoryGirl.attributes_for(:braintree_customer), + redirect_url: confirm_customer_url + + assert_difference("Customer.count") do + post response['Location'] + end + + assert_equal 200, status + assert customer = Customer.find_by_user_id(@user.id) + assert customer.braintree_customer + end + + + test "failed customer creation" do + skip "cannot get customer creation to fail" + + FakeBraintree.decline_all_cards! + + response = post_transparent_redirect :create_customer_data, + customer: FactoryGirl.attributes_for(:broken_customer), + redirect_url: confirm_customer_url + + assert FakeBraintree.decline_all_cards? + assert_no_difference("Customer.count") do + post response['Location'] #this gives me a timeout when run alone + end + assert_nil Customer.find_by_user_id(@user.id) + + end + + def post_transparent_redirect(type, data) + params = data.dup + params[:tr_data] = Braintree::TransparentRedirect.send(type, params) + post_transparent_redirect_params(params) + end + + def post_transparent_redirect_params(params) + uri = URI.parse(Braintree::TransparentRedirect.url) + Net::HTTP.start(uri.host, uri.port) do |http| + http.post(uri.path, Rack::Utils.build_nested_query(params)) + end + end + +end diff --git a/engines/billing/test/broken/subscription_test.rb b/engines/billing/test/broken/subscription_test.rb new file mode 100644 index 0000000..cd010bd --- /dev/null +++ b/engines/billing/test/broken/subscription_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' +require 'fake_braintree' + +class SubscriptionTest < BraintreeIntegrationTest + include CustomerTestHelper + include StubRecordHelper + + setup do + @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin') + @customer = stub_customer + @braintree_customer = @customer.braintree_customer + response = Braintree::Subscription.create plan_id: '5', + payment_method_token: @braintree_customer.credit_cards.first.token, + price: '10' + @subscription = response.subscription + end + + teardown do + @admin.destroy + end + + test "admin can see all subscriptions for another" do + login_as @admin + @customer.stubs(:subscriptions).returns([@subscription]) + @subscription.stubs(:balance).returns 0 + visit user_subscriptions_path(@customer.user_id, :locale => nil) + assert page.has_content?("Subscriptions") + assert page.has_content?("Status: Active") + end + + # test "user cannot see all subscriptions for other user" do + #end + + #test "admin cannot add subscription for another" do + #end + + #test "authenticated user can cancel own subscription" do + #end + + #test "user cannot add subscription if they have active one" do + #end + + #test "user can view own subscriptions" + #end + + #test "admin can view another user's subscriptions" do + #end + +end diff --git a/engines/billing/test/factories.rb b/engines/billing/test/factories.rb new file mode 100644 index 0000000..87543b2 --- /dev/null +++ b/engines/billing/test/factories.rb @@ -0,0 +1,25 @@ +FactoryGirl.define do + + TEST_CC_NUMBER = %w(4111 1111 1111 1111).join + + factory :customer do + user + + factory :customer_with_payment_info do + braintree_customer + end + end + + factory :braintree_customer, class: Braintree::Customer do + first_name 'Big' + last_name 'Spender' + credit_card number: TEST_CC_NUMBER, expiration_date: '04/2016' + initialize_with { Braintree::Customer.create(attributes).customer } + skip_create + + factory :broken_customer do + credit_card number: '123456', expiration_date: '04/2016' + end + end + +end diff --git a/engines/billing/test/functional/customer_controller_test.rb b/engines/billing/test/functional/customer_controller_test.rb new file mode 100644 index 0000000..d943e23 --- /dev/null +++ b/engines/billing/test/functional/customer_controller_test.rb @@ -0,0 +1,124 @@ +require 'test_helper' +require 'fake_braintree' + +class CustomerControllerTest < ActionController::TestCase + include CustomerTestHelper + + test "new assigns redirect url" do + login + get :new + + assert_response :success + assert assigns(:tr_data) + tr_data = Braintree::Util.parse_query_string(assigns(:tr_data)) + assert_equal confirm_customer_url, tr_data[:redirect_url] + end + + test "new requires login" do + get :new + + assert_response :redirect + assert_redirected_to login_path + end + + test "edit uses params[:id]" do + customer = stub_customer + login customer.user + get :edit, id: customer.user.id + + assert_response :success + assert assigns(:tr_data) + tr_data = Braintree::Util.parse_query_string(assigns(:tr_data)) + assert_equal customer.braintree_customer_id, tr_data[:customer_id] + assert_equal confirm_customer_url, tr_data[:redirect_url] + end + + test "confirm customer creation" do + login + Braintree::TransparentRedirect.expects(:confirm).returns(success_response) + # to_confirm = prepare_confirmation :create_customer_data, + # customer: FactoryGirl.attributes_for(:braintree_customer), + # redirect_url: confirm_customer_url + + assert_difference("Customer.count") do + post :confirm, braintree: :query + end + + assert_response :success + assert result = assigns(:result) + assert result.success? + assert result.customer.id + end + + test "customer update" do + customer = stub_customer + customer.expects(:save) + login customer.user + Braintree::TransparentRedirect.expects(:confirm). + returns(success_response(customer)) + + assert_no_difference("Customer.count") do + post :confirm, query: :from_braintree + end + + assert_response :success + assert result = assigns(:result) + assert result.success? + assert_equal customer.braintree_customer, result.customer + end + + test "failed customer creation" do + skip "can't get customer creation to fail" + login + FakeBraintree.decline_all_cards! + # what is prepare_confirmation ?? this method isn't found + to_confirm = prepare_confirmation :create_customer_data, + customer: FactoryGirl.attributes_for(:broken_customer), + redirect_url: confirm_customer_url + post :confirm, to_confirm + + FakeBraintree.clear! + assert_response :success + assert result = assigns(:result) + assert !result.success? + end + + test "failed customer creation with stubbing" do + login + Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) + post :confirm, bla: :blub + + assert_response :success + assert_template :new + end + + test "failed customer update with stubbing" do + customer = stub_customer + login customer.user + Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) + post :confirm, bla: :blub + + assert_response :success + assert_template :edit + end + + def failure_response + stub success?: false, + errors: stub(for: nil, size: 0), + params: {} + end + + def success_response(customer = nil) + stub success?: true, + customer: braintree_customer(customer) + end + + def braintree_customer(customer) + if customer + customer.braintree_customer + else + FactoryGirl.build :braintree_customer + end + end + +end diff --git a/engines/billing/test/functional/customers_controller_test.rb b/engines/billing/test/functional/customers_controller_test.rb new file mode 100644 index 0000000..46c33c9 --- /dev/null +++ b/engines/billing/test/functional/customers_controller_test.rb @@ -0,0 +1,61 @@ +require 'test_helper' +require 'fake_braintree' + +class CustomersControllerTest < ActionController::TestCase + tests CustomerController + + setup do + @user = FactoryGirl.create :user + @other_user = FactoryGirl.create :user + #FakeBraintree.clear! + #FakeBraintree.verify_all_cards! + testid = 'testid' + #this wasn't actually being used + #FakeBraintree::Customer.new({:credit_cards => [{:number=>"5105105105105100", :expiration_date=>"05/2013"}]}, {:id => testid, :merchant_id => Braintree::Configuration.merchant_id}) + # any reason to call the create instance method on the FakeBraintree::Customer ? + @customer = Customer.new(:user_id => @other_user.id) + @customer.braintree_customer_id = testid + @customer.save + + end + + teardown do + @user.destroy + @other_user.destroy + @customer.destroy + end + + test "no access if not logged in" do + get :new + assert_access_denied(true, false) + get :show, :id => @customer.braintree_customer_id + assert_access_denied(true, false) + get :edit, :id => @customer.braintree_customer_id + assert_access_denied(true, false) + end + + + test "should get new if logged in and not customer" do + login @user + get :new + assert_not_nil assigns(:tr_data) + assert_response :success + end + + test "new should direct edit if user is already a customer" do + login @other_user + get :new + assert_response :redirect + assert_equal edit_customer_url(@customer.user), response.header['Location'] + end + + + test "show" do + skip "show customer" + login @other_user + # Below will fail, as when we go to fetch the customer data, Braintree::Customer.find(params[:id]) won't find the customer as it is a FakeBraintree customer. + #get :show, :id => @customer.braintree_customer_id + + end + +end diff --git a/engines/billing/test/functional/payments_controller_test.rb b/engines/billing/test/functional/payments_controller_test.rb new file mode 100644 index 0000000..90b7582 --- /dev/null +++ b/engines/billing/test/functional/payments_controller_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' +require 'fake_braintree' + +class PaymentsControllerTest < ActionController::TestCase + include CustomerTestHelper + + test "payment when unauthorized" do + get :new + assert_not_nil assigns(:tr_data) + assert_response :success + end + + test "successful confirmation renders confirm" do + Braintree::TransparentRedirect.expects(:confirm).returns(success_response) + get :confirm + + assert_response :success + assert_template :confirm + end + + test "failed confirmation renders new" do + Braintree::TransparentRedirect.expects(:confirm).returns(failure_response) + get :confirm + + assert_response :success + assert_not_nil assigns(:tr_data) + assert_template :new + end + + def failure_response + stub success?: false, + errors: stub(for: nil, size: 0), + params: {}, + transaction: stub(status: nil) + end + + def success_response + stub success?: true, + transaction: stub_transaction + end + + # that's what you get when not following the law of demeter... + def stub_transaction + stub amount: "100.00", + id: "ASDF", + customer_details: FactoryGirl.build(:braintree_customer), + credit_card_details: FactoryGirl.build(:braintree_customer).credit_cards.first + end + +end diff --git a/engines/billing/test/functional/subscriptions_controller_test.rb b/engines/billing/test/functional/subscriptions_controller_test.rb new file mode 100644 index 0000000..a6a1057 --- /dev/null +++ b/engines/billing/test/functional/subscriptions_controller_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' +require 'fake_braintree' + +class SubscriptionsControllerTest < ActionController::TestCase + include CustomerTestHelper + + test "destroy cancels subscription" do + customer = stub_customer + login customer.user + result = Braintree::Subscription.create plan_id: 'my_plan', + payment_method_token: customer.braintree_customer.credit_cards.first.token + subscription = result.subscription + delete :destroy, id: subscription.id, user_id: customer.user.id + assert_equal "Canceled", Braintree::Subscription.find(subscription.id).status + end +end diff --git a/engines/billing/test/support/braintree_integration_test.rb b/engines/billing/test/support/braintree_integration_test.rb new file mode 100644 index 0000000..976c5a2 --- /dev/null +++ b/engines/billing/test/support/braintree_integration_test.rb @@ -0,0 +1,18 @@ +require 'capybara/rails' +# require 'fake_braintree' - messes up other integration tests +require 'braintree_test_app' + +class BraintreeIntegrationTest < BrowserIntegrationTest + include Warden::Test::Helpers + + setup do + Warden.test_mode! + Rails.application.config.middleware.use BraintreeTestApp + end + + teardown do + Warden.test_reset! + Rails.application.config.middleware.delete "BraintreeTestApp" + end + +end diff --git a/engines/billing/test/support/customer_test_helper.rb b/engines/billing/test/support/customer_test_helper.rb new file mode 100644 index 0000000..adac00a --- /dev/null +++ b/engines/billing/test/support/customer_test_helper.rb @@ -0,0 +1,11 @@ +module CustomerTestHelper + + def stub_customer(user = nil) + user ||= find_record :user + customer = stub_record :customer_with_payment_info, + user: user, + user_id: user.id + Customer.stubs(:find_by_user_id).with(user.id).returns(customer) + return customer + end +end diff --git a/engines/billing/test/test_helper.rb b/engines/billing/test/test_helper.rb new file mode 100644 index 0000000..7ad3869 --- /dev/null +++ b/engines/billing/test/test_helper.rb @@ -0,0 +1,15 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require File.expand_path("../../../../dummy/config/environment.rb", __FILE__) +require "rails/test_help" + +Rails.backtrace_cleaner.remove_silencers! + +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + +# Load fixtures from the engine +if ActiveSupport::TestCase.method_defined?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) +end diff --git a/engines/billing/test/unit/customer_test.rb b/engines/billing/test/unit/customer_test.rb new file mode 100644 index 0000000..6156f87 --- /dev/null +++ b/engines/billing/test/unit/customer_test.rb @@ -0,0 +1,38 @@ +require 'test_helper' + +class CustomerTest < ActiveSupport::TestCase + include StubRecordHelper + + setup do + @user = find_record :user + @customer = FactoryGirl.build(:customer, user: @user) + end + + test "test set of attributes should be valid" do + @customer.valid? + assert_equal Hash.new, @customer.errors.messages + end + + test "customer belongs to user" do + assert_equal User, @customer.user.class + end + + test "user validation" do + @customer.user = nil + assert !@customer.valid? + end + + test "has no payment info" do + assert !@customer.braintree_customer_id + assert !@customer.has_payment_info? + end + + test "with no braintree data" do + assert_equal @customer, @customer.with_braintree_data! + end + + test "without default credit card" do + assert_nil @customer.default_credit_card + end + +end diff --git a/engines/billing/test/unit/customer_with_payment_info_test.rb b/engines/billing/test/unit/customer_with_payment_info_test.rb new file mode 100644 index 0000000..0589a59 --- /dev/null +++ b/engines/billing/test/unit/customer_with_payment_info_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' +require 'fake_braintree' + +class CustomerWithPaymentInfoTest < ActiveSupport::TestCase + include StubRecordHelper + + setup do + @user = find_record :user + @customer = FactoryGirl.build(:customer_with_payment_info, user: @user) + end + + test "has payment_info" do + assert @customer.braintree_customer_id + assert @customer.has_payment_info? + end + + test "constructs customer with braintree data" do + @customer.with_braintree_data! + assert_equal 'Big', @customer.first_name + assert_equal 'Spender', @customer.last_name + assert_equal 1, @customer.credit_cards.size + assert_equal Hash.new, @customer.custom_fields + end + + test "can access braintree_customer after reload" do + @customer.save + @customer = Customer.find_by_user_id(@customer.user_id) + @customer.with_braintree_data! + assert_equal 'Big', @customer.first_name + assert_equal 'Spender', @customer.last_name + assert_equal 1, @customer.credit_cards.size + assert_equal Hash.new, @customer.custom_fields + @customer.destroy + end + + test "sets default_credit_card" do + @customer.with_braintree_data! + assert_equal @customer.credit_cards.first, @customer.default_credit_card + end +end diff --git a/engines/support/Gemfile b/engines/support/Gemfile new file mode 100644 index 0000000..ad7d29b --- /dev/null +++ b/engines/support/Gemfile @@ -0,0 +1,15 @@ +source "https://rubygems.org" + +eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) +eval(File.read(File.dirname(__FILE__) + '/..//ui_dependencies.rb')) + +# We require leap_web_core from here so we can use the path option. +gem "leap_web_core", :path => '../core' + +# Declare your gem's dependencies in leap_web_users.gemspec. +# Bundler will treat runtime dependencies like base dependencies, and +# development dependencies will be added by default to the :development group. +gemspec + +# To use debugger +# gem 'ruby-debug' diff --git a/engines/support/README.md b/engines/support/README.md new file mode 100644 index 0000000..c9573e6 --- /dev/null +++ b/engines/support/README.md @@ -0,0 +1 @@ +Implements a simple, clean, and easy to use help ticketing system. \ No newline at end of file diff --git a/engines/support/Rakefile b/engines/support/Rakefile new file mode 100644 index 0000000..0e73163 --- /dev/null +++ b/engines/support/Rakefile @@ -0,0 +1,44 @@ +#!/usr/bin/env rake + +require 'rake/packagetask' +require 'rubygems/package_task' + +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end +begin + require 'rdoc/task' +rescue LoadError + require 'rdoc/rdoc' + require 'rake/rdoctask' + RDoc::Task = Rake::RDocTask +end + +RDoc::Task.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'LeapWebHelp' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +spec = eval(File.read('leap_web_help.gemspec')) +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +Bundler::GemHelper.install_tasks + +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + + +task :default => :test diff --git a/engines/support/app/assets/javascripts/tickets.js b/engines/support/app/assets/javascripts/tickets.js new file mode 100644 index 0000000..18537aa --- /dev/null +++ b/engines/support/app/assets/javascripts/tickets.js @@ -0,0 +1,4 @@ +//$(document).ready(function () { +// $.fn.editable.defaults.mode = 'inline'; +// $('#subject').editable(); +//}); \ No newline at end of file diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb new file mode 100644 index 0000000..d65ee43 --- /dev/null +++ b/engines/support/app/controllers/tickets_controller.rb @@ -0,0 +1,153 @@ +class TicketsController < ApplicationController + include AutoTicketsPathHelper + + respond_to :html, :json + #has_scope :open, :type => boolean + + before_filter :require_login, :only => [:index] + before_filter :fetch_ticket, :only => [:show, :update, :destroy] # don't now have an edit method + before_filter :fetch_user + before_filter :set_title + + def new + @ticket = Ticket.new + @ticket.comments.build + end + + def create + @ticket = Ticket.new(params[:ticket]) + + @ticket.comments.last.posted_by = (logged_in? ? current_user.id : nil) #protecting posted_by isn't working, so this should protect it. + @ticket.comments.last.private = false unless admin? + @ticket.created_by = current_user.id if logged_in? + @ticket.email = current_user.email_address if logged_in? and current_user.email_address + + if @ticket.save + flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) + end + + # cannot set this until ticket has been saved, as @ticket.id will not be set + if !logged_in? and flash[:notice] + flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) + end + respond_with(@ticket, :location => auto_ticket_path(@ticket)) + end + + def show + @comment = TicketComment.new + if !@ticket + redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => t(:ticket)) + return + end + end + + def update + if params[:commit] == 'close' + @ticket.is_open = false + @ticket.save + redirect_to_tickets + elsif params[:commit] == 'open' + @ticket.is_open = true + @ticket.save + redirect_to auto_ticket_path(@ticket) + else + @ticket.attributes = cleanup_ticket_params(params[:ticket]) + + if params[:commit] == 'reply_and_close' + @ticket.close + end + + if @ticket.comments_changed? + @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) + @ticket.comments.last.private = false unless admin? + end + + if @ticket.changed? and @ticket.save + flash[:notice] = t(:changes_saved) + redirect_to_tickets + else + flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed? + redirect_to auto_ticket_path(@ticket) + end + end + end + + def index + @all_tickets = Ticket.search(search_options(params)) + @tickets = @all_tickets.page(params[:page]).per(APP_CONFIG[:pagination_size]) + end + + def destroy + # should we allow non-admins to delete their own tickets? i don't think necessary. + @ticket.destroy if admin? + redirect_to auto_tickets_path + end + + protected + + def set_title + @title = t(:tickets) + end + + private + + # + # redirects to ticket index, if appropriate. + # otherwise, just redirects to @ticket + # + def redirect_to_tickets + if logged_in? + if params[:commit] == t(:reply_and_close) + redirect_to auto_tickets_path + else + redirect_to auto_ticket_path(@ticket) + end + else + # if we are not logged in, there is no index to view + redirect_to auto_ticket_path(@ticket) + end + end + + # + # unset comments hash if no new comment was typed + # + def cleanup_ticket_params(ticket) + if ticket && ticket[:comments_attributes] + if ticket[:comments_attributes].values.first[:body].blank? + ticket[:comments_attributes] = nil + end + end + return ticket + end + + def ticket_access? + @ticket and (admin? or !@ticket.created_by or (current_user and current_user.id == @ticket.created_by)) + end + + def fetch_ticket + @ticket = Ticket.find(params[:id]) + if !@ticket and admin? + redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => 'ticket') + return + end + access_denied unless ticket_access? + end + + def fetch_user + if params[:user_id] + @user = User.find(params[:user_id]) + end + end + + # + # clean up params for ticket search + # + def search_options(params) + params.merge( + :admin_status => params[:user_id] ? 'mine' : 'all', + :user_id => @user ? @user.id : current_user.id, + :is_admin => admin? + ) + end + +end diff --git a/engines/support/app/designs/ticket/by_includes_post_by.js b/engines/support/app/designs/ticket/by_includes_post_by.js new file mode 100644 index 0000000..2eeac89 --- /dev/null +++ b/engines/support/app/designs/ticket/by_includes_post_by.js @@ -0,0 +1,13 @@ +// TODO: This view is only used in tests--should we keep it? +function(doc) { + var arr = {} + if (doc['type'] == 'Ticket' && doc.comments) { + doc.comments.forEach(function(comment){ + if (comment.posted_by && !arr[comment.posted_by]) { + //don't add duplicates + arr[comment.posted_by] = true; + emit(comment.posted_by, 1); + } + }); + } +} diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js new file mode 100644 index 0000000..72169b0 --- /dev/null +++ b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js @@ -0,0 +1,12 @@ +function(doc) { + var arr = {} + if (doc['type'] == 'Ticket' && doc.comments) { + doc.comments.forEach(function(comment){ + if (comment.posted_by && !arr[comment.posted_by]) { + //don't add duplicates + arr[comment.posted_by] = true; + emit([comment.posted_by, doc.created_at], 1); + } + }); + } +} diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js new file mode 100644 index 0000000..33dfe0b --- /dev/null +++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js @@ -0,0 +1,12 @@ +function(doc) { + var arr = {} + if (doc['type'] == 'Ticket' && doc.comments) { + doc.comments.forEach(function(comment){ + if (comment.posted_by && !arr[comment.posted_by]) { + //don't add duplicates + arr[comment.posted_by] = true; + emit([comment.posted_by, doc.is_open, doc.created_at], 1); + } + }); + } +} diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js new file mode 100644 index 0000000..3bd2a74 --- /dev/null +++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js @@ -0,0 +1,12 @@ +function(doc) { + var arr = {} + if (doc['type'] == 'Ticket' && doc.comments) { + doc.comments.forEach(function(comment){ + if (comment.posted_by && !arr[comment.posted_by]) { + //don't add duplicates + arr[comment.posted_by] = true; + emit([comment.posted_by, doc.is_open, doc.updated_at], 1); + } + }); + } +} diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js new file mode 100644 index 0000000..2b4304f --- /dev/null +++ b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js @@ -0,0 +1,12 @@ +function(doc) { + var arr = {} + if (doc['type'] == 'Ticket' && doc.comments) { + doc.comments.forEach(function(comment){ + if (comment.posted_by && !arr[comment.posted_by]) { + //don't add duplicates + arr[comment.posted_by] = true; + emit([comment.posted_by, doc.updated_at], 1); + } + }); + } +} diff --git a/engines/support/app/helpers/auto_tickets_path_helper.rb b/engines/support/app/helpers/auto_tickets_path_helper.rb new file mode 100644 index 0000000..93f3cb9 --- /dev/null +++ b/engines/support/app/helpers/auto_tickets_path_helper.rb @@ -0,0 +1,53 @@ +# +# These "auto" forms of the normal ticket path route helpers allow us to do two things automatically: +# +# (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. +# +# This is available both to the views and the tickets_controller. +# +module AutoTicketsPathHelper + + protected + + def auto_tickets_path(options={}) + return unless options.class == Hash + options = ticket_view_options.merge options + if @user + user_tickets_path(@user, options) + else + tickets_path(options) + end + end + + def auto_ticket_path(ticket, options={}) + options = ticket_view_options.merge options + if @user + user_ticket_path(@user, ticket, options) + else + ticket_path(ticket, options) + end + end + + def auto_new_ticket_path(options={}) + return unless options.class == Hash + options = ticket_view_options.merge options + if @user + new_user_ticket_path(@user, options) + else + new_ticket_path(options) + end + end + + private + + def ticket_view_options + hsh = {} + hsh[:open_status] = params[:open_status] if params[:open_status] && !params[:open_status].empty? + hsh[:sort_order] = params[:sort_order] if params[:sort_order] && !params[:sort_order].empty? + hsh + end + +end \ No newline at end of file diff --git a/engines/support/app/helpers/tickets_helper.rb b/engines/support/app/helpers/tickets_helper.rb new file mode 100644 index 0000000..7af50d6 --- /dev/null +++ b/engines/support/app/helpers/tickets_helper.rb @@ -0,0 +1,76 @@ +module TicketsHelper + # + # FORM HELPERS + # + + # + # hidden fields that should be added to ever ticket form. + # these are use for proper redirection after successful actions. + # + def hidden_ticket_fields + haml_concat hidden_field_tag('open_status', params[:open_status]) + haml_concat hidden_field_tag('sort_order', params[:sort_order]) + haml_concat hidden_field_tag('user_id', params[:user_id]) + "" + end + + # + # PARAM HELPERS + # + + def search_status + if action?(:index) + params[:open_status] || 'open' + else + nil + end + end + + def search_order + params[:sort_order] || 'updated_at_desc' + end + + # + # LINK HELPERS + # + + def link_to_status(new_status) + if new_status == "open" + label = t(:open_tickets) + elsif new_status == "closed" + label = t(:closed_tickets) + elsif new_status == "all" + label = t(:all_tickets) + end + link_to label, auto_tickets_path(:open_status => new_status, :sort_order => search_order) + end + + def link_to_order(order_field) + if search_order.start_with?(order_field) + # link for currently-filtered field. Link to other direction of this field. + if search_order.end_with? 'asc' + direction = 'desc' + icon_direction = 'up' + else + direction = 'asc' + icon_direction = 'down' + end + arrow = content_tag(:i, '', class: 'icon-arrow-'+ icon_direction) + else + # for not-currently-filtered field, don't display an arrow, and link to descending direction + arrow = '' + direction = 'desc' + end + + if order_field == 'updated' + label = t(:updated) + elsif order_field == 'created' + label = t(:created) + end + + link_to auto_tickets_path(:sort_order => order_field + '_at_' + direction, :open_status => search_status) do + arrow + label + end + end + +end diff --git a/engines/support/app/models/account_extension/tickets.rb b/engines/support/app/models/account_extension/tickets.rb new file mode 100644 index 0000000..f898b56 --- /dev/null +++ b/engines/support/app/models/account_extension/tickets.rb @@ -0,0 +1,13 @@ +module AccountExtension::Tickets + extend ActiveSupport::Concern + + def destroy_with_tickets + Ticket.destroy_all_from(self.user) + destroy_without_tickets + 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 new file mode 100644 index 0000000..cd22758 --- /dev/null +++ b/engines/support/app/models/ticket.rb @@ -0,0 +1,105 @@ +# +# TODO: thought i should reverse keys for descending, but that didn't work. +# look into whether that should be tweaked, and whether it works okay with +# pagination (seems to now...) +# +# TODO: better validation of email +# +# TODO: don't hardcode strings 'unknown user' and 'unauthenticated user' +# +class Ticket < CouchRest::Model::Base + use_database "tickets" + + property :created_by, String, :protected => true # nil for anonymous tickets, should never be changed + property :regarding_user, String # may be nil or valid username + property :subject, String + property :email, String + property :is_open, TrueClass, :default => true + property :comments, [TicketComment] + + timestamps! + + before_validation :set_email, :set_regarding_user, :on => :create + + design do + view :by_updated_at + view :by_created_at + view :by_created_by + + view :by_is_open_and_created_at + view :by_is_open_and_updated_at + + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'ticket')) + end + + validates :subject, :presence => true + validates :email, :allow_blank => true, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ + + def self.search(options = {}) + @selection = TicketSelection.new(options) + @selection.tickets + end + + def self.destroy_all_from(user) + self.by_created_by.key(user.id).each do |ticket| + ticket.destroy + end + end + + def is_creator_validated? + !!created_by + end + + def set_email + self.email = nil if self.email == "" + end + + def set_regarding_user + self.regarding_user = nil if self.regarding_user == "" + end + + def close + self.is_open = false + end + + def reopen + self.is_open = true + end + + def commenters + commenters = [] + self.comments.each do |comment| + if comment.posted_by + if user = User.find(comment.posted_by) + commenters << user.login if user and !commenters.include?(user.login) + else + commenters << 'unknown user' if !commenters.include?('unknown user') + end + else + commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user') + end + end + commenters.join(', ') + end + + # + # update comments. User should be set by controller. + # + def comments_attributes=(attributes) + if attributes + comment = TicketComment.new(attributes.values.first) + comment.posted_at = Time.now + comments << comment + end + end + + def created_by_user + User.find(self.created_by) + end + + def regarding_user_actual_user + User.find_by_login(self.regarding_user) + end + +end diff --git a/engines/support/app/models/ticket_comment.rb b/engines/support/app/models/ticket_comment.rb new file mode 100644 index 0000000..bed5237 --- /dev/null +++ b/engines/support/app/models/ticket_comment.rb @@ -0,0 +1,43 @@ +class TicketComment + include CouchRest::Model::Embeddable + + #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way? + property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this. + # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL + property :posted_at, Time#, :protected => true + #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created + property :body, String + property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin + + # ? timestamps! + validates :body, :presence => true + #before_validation :set_time#, :set_posted_by + + #design do + # view :by_posted_at + # view :by_body + #end + + def is_comment_validated? + !!posted_by + end + + def posted_by_user + User.find(posted_by) if posted_by + end + +=begin + #TODO. + #this is resetting all comments associated with the ticket: + def set_time + self.posted_at = Time.now + end +=end + +=begin + def set_posted_by + self.posted_by = User.current if User.current + end +=end + +end diff --git a/engines/support/app/models/ticket_selection.rb b/engines/support/app/models/ticket_selection.rb new file mode 100644 index 0000000..74d5b78 --- /dev/null +++ b/engines/support/app/models/ticket_selection.rb @@ -0,0 +1,71 @@ +class TicketSelection + + # + # supported options: + # + # user_id: id of the user (uuid string) + # open_status: open | closed | all + # sort_order: updated_at_desc | updated_at_asc | created_at_desc | created_at_asc + # admin_status: mine | all + # is_admin: true | false + # + def initialize(options = {}) + @user_id = options[:user_id].gsub /[^a-z0-9]/, '' + @open_status = allow options[:open_status], 'open', 'closed', 'all' + @sort_order = allow options[:sort_order], 'updated_at_desc', 'updated_at_asc', 'created_at_desc', 'created_at_asc' + @admin_status = allow options[:admin_status], 'mine', 'all' + @is_admin = allow options[:is_admin], false, true + end + + def tickets + Ticket.send(finder_method).startkey(startkey).endkey(endkey).send(order) + end + + protected + + def allow(source, *allowed) + if allowed.include?(source) + source + else + allowed.first + end + end + + def finder_method + method = 'by_' + method += 'includes_post_by_and_' if only_mine? + method += 'is_open_and_' if @open_status != 'all' + method += @sort_order.sub(/_(de|a)sc$/, '') + end + + def startkey + startkeys = [] + startkeys << @user_id if only_mine? + startkeys << (@open_status == 'open') if @open_status != 'all' + startkeys << 0 + startkeys = startkeys.join if startkeys.length == 1 # want string not array if just one thing in array + startkeys + end + + def endkey + endtime = Time.now + 2.days # TODO. this obviously isn't ideal + if self.startkey.is_a?(Array) + endkeys = self.startkey + endkeys.pop + endkeys << endtime + else + endtime + end + end + + def order + # we have defined the ascending method to return the view itself: + (@sort_order.end_with? 'desc') ? 'descending' : 'ascending' + end + + + def only_mine? + !@is_admin || @admin_status == 'mine' + end + +end diff --git a/engines/support/app/views/tickets/_comment.html.haml b/engines/support/app/views/tickets/_comment.html.haml new file mode 100644 index 0000000..778ca13 --- /dev/null +++ b/engines/support/app/views/tickets/_comment.html.haml @@ -0,0 +1,20 @@ +- if admin? or !comment.private # only show comment if user is admin or comment is not private + %tr + %td.user + %div + %strong + - if comment.posted_by_user + = comment.posted_by_user.login + - else + = t(:anonymous) + %div= comment.posted_at.to_s(:short) + - if comment.posted_by_user && comment.posted_by_user.is_admin? + %div + %span.label.label-inverse + = t(:admin) + - if comment.private + %div + %span.label.label-important + = t(:private) + %td.comment + = simple_format(comment.body) \ No newline at end of file diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml new file mode 100644 index 0000000..714f8ff --- /dev/null +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -0,0 +1,48 @@ +:ruby + # created by user link + if @ticket.created_by_user + created_by = link_to @ticket.created_by_user.login, @ticket.created_by_user + else + created_by = t(:anonymous) + end + + # regarding user link + if admin? + if @ticket.regarding_user_actual_user + regarding_user_link = link_to @ticket.regarding_user_actual_user.login, @ticket.regarding_user_actual_user + else + regarding_user_link = "(#{t(:unknown)})" + end + else + regarding_user_link = '' + end + += form_for @ticket do |f| + = hidden_ticket_fields + %p.first + - if @ticket.is_open? + %span.label.label-info= t(:open) + - else + %span.label.label-success= t(:closed) + %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe + %div= t(:subject) + = f.text_field :subject, :class => 'large full-width' + .row-fluid + .span4 + %div= t(:status) + = f.select :is_open, [[t(:open), "true"], [t(:closed), "false"]] + .span4 + %div= t(:email) + = f.text_field :email + .span4 + %div + = t(:regarding_account) + = regarding_user_link + = f.text_field :regarding_user + = f.button t(:save), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'save' + - if @ticket.is_open? + = f.button t(:close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'close' + - else + = f.button t(:open), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'open' + - if admin? + = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml new file mode 100644 index 0000000..8418e01 --- /dev/null +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -0,0 +1,13 @@ +-# +-# for posting a new comment to an existing ticket. +-# += simple_form_for @ticket, :html => {:class => 'slim'} do |f| + = hidden_ticket_fields + = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c| + = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} + - if admin? + = c.input :private, :as => :boolean, :label => false, :inline_label => true + = f.button :button, t(:post_reply), :name => 'commit', :class => 'btn-primary', :type => 'submit', :value => 'post_reply' + - if logged_in? && @ticket.is_open + = f.button :button, t(:reply_and_close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'reply_and_close' + = link_to t(:cancel), auto_tickets_path, :class => :btn diff --git a/engines/support/app/views/tickets/_tabs.html.haml b/engines/support/app/views/tickets/_tabs.html.haml new file mode 100644 index 0000000..b7b5d3a --- /dev/null +++ b/engines/support/app/views/tickets/_tabs.html.haml @@ -0,0 +1,23 @@ +-# +-# SORT ORDER TABS +-# +- unless action?(:new) + %ul.nav.nav-pills.pull-right.slim + %li{:class=> ("active" if search_order.start_with? 'created_at')} + = link_to_order('created') + %li{:class=> ("active" if search_order.start_with? 'updated_at')} + = link_to_order('updated') + +-# +-# STATUS FILTER TABS +-# +%ul.nav.nav-tabs + - if logged_in? + %li{:class => ("active" if search_status == 'open')} + = link_to_status 'open' + %li{:class => ("active" if search_status == 'closed')} + = link_to_status 'closed' + %li{:class => ("active" if search_status == 'all')} + = link_to_status 'all' + %li{:class => ("active" if action?(:new))} + = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path diff --git a/engines/support/app/views/tickets/_ticket.html.haml b/engines/support/app/views/tickets/_ticket.html.haml new file mode 100644 index 0000000..5bc33c8 --- /dev/null +++ b/engines/support/app/views/tickets/_ticket.html.haml @@ -0,0 +1,6 @@ +- url = auto_ticket_path(ticket) +%tr + %td= link_to ticket.subject, url + %td= link_to ticket.created_at.to_s(:short), url + %td= link_to ticket.updated_at.to_s(:short), url + %td= ticket.commenters diff --git a/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml new file mode 100644 index 0000000..c02a326 --- /dev/null +++ b/engines/support/app/views/tickets/index.html.haml @@ -0,0 +1,19 @@ +- @show_navigation = !params[:user_id].nil? + += render 'tickets/tabs' + +%table.table.table-striped.table-bordered + %thead + %tr + %th= t(:subject) + %th= t(:created) + %th= t(:updated) + %th= t(:voices) + %tbody + - if @tickets.any? + = render @tickets.all + - else + %tr + %td{:colspan=>4}= t(:none) + += paginate @tickets diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml new file mode 100644 index 0000000..8f217a5 --- /dev/null +++ b/engines/support/app/views/tickets/new.html.haml @@ -0,0 +1,30 @@ +- @show_navigation = !params[:user_id].nil? + += render 'tickets/tabs' + +- if admin? && @user + - email = @user.email_address + - regarding = @user.login +- elsif logged_in? + - email = current_user.email_address + - regarding = current_user.login + += simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| + = hidden_ticket_fields + = f.input :subject + - if logged_in? + = f.input :email, input_html: {value: email} + = f.input :regarding_user, input_html: {value: regarding} + - else + = f.input :email + = f.input :regarding_user + = f.simple_fields_for :comments, @comment do |c| + = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5} + - if admin? + = c.input :private, :as => :boolean, :label => false, :inline_label => true + .form-actions + = f.button :submit, :class => 'btn-primary', :value => t(:create_thing, :thing => t(:ticket)) + - if logged_in? + = link_to t(:cancel), auto_tickets_path, :class => :btn + - else + = link_to t(:cancel), home_path, :class => 'btn' \ No newline at end of file diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml new file mode 100644 index 0000000..bfdb773 --- /dev/null +++ b/engines/support/app/views/tickets/show.html.haml @@ -0,0 +1,12 @@ +- @show_navigation = !params[:user_id].nil? + +.ticket + = render 'tickets/edit_form' + %table.table.table-striped.table-bordered + %tbody + = render :partial => 'tickets/comment', :collection => @ticket.comments + %tr + %td.user + = logged_in? ? current_user.login : t(:anonymous) + %td.comment + = render 'tickets/new_comment_form' \ No newline at end of file diff --git a/engines/support/config/initializers/account_lifecycle.rb b/engines/support/config/initializers/account_lifecycle.rb new file mode 100644 index 0000000..d9f04c1 --- /dev/null +++ b/engines/support/config/initializers/account_lifecycle.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..342adea --- /dev/null +++ b/engines/support/config/locales/en.yml @@ -0,0 +1,22 @@ +en: + access_ticket_text: > + You can later access this ticket at the URL %{full_url}. You might want to bookmark this page to find it again. + Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to + remove it from the browser history. + support_tickets: "Support Tickets" + all_tickets: "All Tickets" + my_tickets: "My Tickets" + open_tickets: "Open Tickets" + closed_tickets: "Closed Tickets" + new_ticket: "New Ticket" + tickets: "Tickets" + subject: "Subject" + destroy: "Destroy" + open: "Open" + closed: "Closed" + close: "Close" + post_reply: "Post Reply" + reply_and_close: "Reply and Close" + description: "Description" + ticket: "Ticket" + regarding_account: "Regarding Account" \ No newline at end of file diff --git a/engines/support/config/routes.rb b/engines/support/config/routes.rb new file mode 100644 index 0000000..23e0c11 --- /dev/null +++ b/engines/support/config/routes.rb @@ -0,0 +1,8 @@ +Rails.application.routes.draw do + scope "(:locale)", :locale => MATCH_LOCALE do + resources :tickets, :except => :edit + resources :users do + resources :tickets, :except => :edit + end + end +end diff --git a/engines/support/leap_web_help.gemspec b/engines/support/leap_web_help.gemspec new file mode 100644 index 0000000..41959d6 --- /dev/null +++ b/engines/support/leap_web_help.gemspec @@ -0,0 +1,18 @@ +$:.push File.expand_path("../../lib", __FILE__) + +require File.expand_path('../../../lib/leap_web/version.rb', __FILE__) + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "leap_web_help" + s.version = LeapWeb::VERSION + s.authors = ["Jessib"] + s.email = ["jessib@leap.se"] + s.homepage = "http://www.leap.se" + s.summary = "Help Desk for LeapWeb" + s.description = "Managing Tickets for a Leap provider" + + s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.test_files = Dir["test/**/*"] + +end diff --git a/engines/support/lib/leap_web_help.rb b/engines/support/lib/leap_web_help.rb new file mode 100644 index 0000000..f5b04aa --- /dev/null +++ b/engines/support/lib/leap_web_help.rb @@ -0,0 +1,4 @@ +require "leap_web_help/engine" + +module LeapWebHelp +end diff --git a/engines/support/lib/leap_web_help/engine.rb b/engines/support/lib/leap_web_help/engine.rb new file mode 100644 index 0000000..dfa763f --- /dev/null +++ b/engines/support/lib/leap_web_help/engine.rb @@ -0,0 +1,4 @@ +module LeapWebHelp + class Engine < ::Rails::Engine + end +end diff --git a/engines/support/lib/tasks/leap_web_help_tasks.rake b/engines/support/lib/tasks/leap_web_help_tasks.rake new file mode 100644 index 0000000..1f38982 --- /dev/null +++ b/engines/support/lib/tasks/leap_web_help_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :leap_web_help do +# # Task goes here +# end diff --git a/engines/support/script/rails b/engines/support/script/rails new file mode 100755 index 0000000..5676ab9 --- /dev/null +++ b/engines/support/script/rails @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/leap_web_help/engine', __FILE__) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/engines/support/test/factories.rb b/engines/support/test/factories.rb new file mode 100644 index 0000000..be04f15 --- /dev/null +++ b/engines/support/test/factories.rb @@ -0,0 +1,18 @@ +FactoryGirl.define do + + factory :ticket do + subject { Faker::Lorem.sentence } + email { Faker::Internet.email } + + factory :ticket_with_comment do + comments_attributes do + { "0" => { "body" => Faker::Lorem.sentences.join(" ") } } + end + end + + factory :ticket_with_creator do + created_by { FactoryGirl.create(:user).id } + end + end + +end diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb new file mode 100644 index 0000000..416fb73 --- /dev/null +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -0,0 +1,273 @@ +require 'test_helper' + +class TicketsControllerTest < ActionController::TestCase + + teardown do + # destroy all tickets that were created during the test + Ticket.all.each{|t| t.destroy} + end + + test "should get index if logged in" do + login + get :index + assert_response :success + assert_not_nil assigns(:tickets) + end + + test "no index if not logged in" do + get :index + assert_response :redirect + assert_nil assigns(:tickets) + end + + test "should get new" do + get :new + 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 + assert_response :success + end + + test "user tickets are not visible without login" do + user = find_record :user + ticket = find_record :ticket, :created_by => user.id + get :show, :id => ticket.id + assert_response :redirect + assert_redirected_to login_url + end + + test "user tickets are visible to creator" do + user = find_record :user + ticket = find_record :ticket, :created_by => user.id + login user + get :show, :id => ticket.id + assert_response :success + end + + test "other users tickets are not visible" do + other_user = find_record :user + ticket = find_record :ticket, :created_by => other_user.id + login + get :show, :id => ticket.id + assert_response :redirect + assert_redirected_to home_url + end + + test "should create unauthenticated ticket" do + params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} + + assert_difference('Ticket.count') do + post :create, :ticket => params + end + + assert_response :redirect + assert_nil assigns(:ticket).created_by + + assert_equal 1, assigns(:ticket).comments.count + assert_nil assigns(:ticket).comments.first.posted_by + + end + + test "should create authenticated ticket" do + + params = {:subject => "auth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} + + login + + assert_difference('Ticket.count') do + post :create, :ticket => params + end + + assert_response :redirect + + assert_not_nil assigns(:ticket).created_by + assert_equal assigns(:ticket).created_by, @current_user.id + assert_equal assigns(:ticket).email, @current_user.email_address + + assert_equal 1, assigns(:ticket).comments.count + assert_not_nil assigns(:ticket).comments.first.posted_by + assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id + end + + test "add comment to unauthenticated ticket" do + ticket = FactoryGirl.create :ticket, :created_by => nil + + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + + assert_equal ticket, assigns(:ticket) # still same ticket, with different comments + assert_not_equal ticket.comments, assigns(:ticket).comments # ticket == assigns(:ticket), but they have different comments (which we want) + + end + + + test "add comment to own authenticated ticket" do + + login + ticket = FactoryGirl.create :ticket, :created_by => @current_user.id + + #they should be able to comment if it is their ticket: + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + assert_not_equal ticket.comments, assigns(:ticket).comments + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + + end + + + test "cannot comment if it is not your ticket" do + + other_user = find_record :user + login :is_admin? => false, :email => nil + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + # they should *not* be able to comment if it is not their ticket + put :update, :id => ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"not allowed comment"}} } + assert_response :redirect + assert_access_denied + + assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body) + + end + + + test "admin add comment to authenticated ticket" do + + other_user = find_record :user + login :is_admin? => true + + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + + #admin should be able to comment: + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + assert_not_equal ticket.comments, assigns(:ticket).comments + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + end + + test "tickets by admin" do + other_user = find_record :user + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + + login :is_admin? => true + + get :index, {:admin_status => "all", :open_status => "open"} + assert assigns(:all_tickets).count > 0 + + # if we close one ticket, the admin should have 1 less open ticket + assert_difference('assigns[:all_tickets].count', -1) do + assigns(:tickets).first.close + assigns(:tickets).first.save + get :index, {:admin_status => "all", :open_status => "open"} + end + end + + + test "admin_status mine vs all" do + testticket = FactoryGirl.create :ticket + user = find_record :user + login :is_admin? => true, :email => nil + + get :index, {:open_status => "open"} + assert assigns(:all_tickets).include?(testticket) + get :index, {:user_id => user.id, :open_status => "open"} + assert !assigns(:all_tickets).include?(testticket) + end + + test "commenting on a ticket adds to tickets that are mine" do + testticket = FactoryGirl.create :ticket + user = find_record :admin_user + login user + get :index, {:user_id => user.id, :open_status => "open"} + assert_difference('assigns[:all_tickets].count') do + put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} + get :index, {:user_id => user.id, :open_status => "open"} + end + + assert assigns(:all_tickets).include?(assigns(:ticket)) + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + end + + test "admin ticket ordering" do + tickets = FactoryGirl.create_list :ticket, 2 + + login :is_admin? => true, :email => nil + get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_desc'} + + # this will consider all tickets, not just those on first page + first_tick = assigns(:all_tickets).all.first + last_tick = assigns(:all_tickets).all.last + assert first_tick.created_at > last_tick.created_at + + # and now reverse order: + get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_asc'} + + assert_equal first_tick, assigns(:all_tickets).last + assert_equal last_tick, assigns(:all_tickets).first + + assert_not_equal first_tick, assigns(:all_tickets).first + assert_not_equal last_tick, assigns(:all_tickets).last + + end + + test "tickets for regular user" do + login + ticket = FactoryGirl.create :ticket + other_ticket = FactoryGirl.create :ticket + + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + + get :index, {:open_status => "open"} + assert assigns(:all_tickets).count > 0 + assert assigns(:all_tickets).include?(ticket) + assert !assigns(:all_tickets).include?(other_ticket) + + # user should have one more ticket if a new tick gets a comment by this user + assert_difference('assigns[:all_tickets].count') do + put :update, :id => other_ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} + get :index, {:open_status => "open"} + end + assert assigns(:all_tickets).include?(other_ticket) + + # if we close one ticket, the user should have 1 less open ticket + assert_difference('assigns[:all_tickets].count', -1) do + other_ticket.reload + other_ticket.close + other_ticket.save + get :index, {:open_status => "open"} + end + + number_open_tickets = assigns(:all_tickets).count + + # look at closed tickets: + get :index, {:open_status => "closed"} + assert !assigns(:all_tickets).include?(ticket) + assert assigns(:all_tickets).include?(other_ticket) + number_closed_tickets = assigns(:all_tickets).count + + # all tickets should equal closed + open + get :index, {:open_status => "all"} + assert assigns(:all_tickets).include?(ticket) + assert assigns(:all_tickets).include?(other_ticket) + assert_equal assigns(:all_tickets).count, number_closed_tickets + number_open_tickets + + + end + +end + diff --git a/engines/support/test/integration/navigation_test.rb b/engines/support/test/integration/navigation_test.rb new file mode 100644 index 0000000..eec8c0e --- /dev/null +++ b/engines/support/test/integration/navigation_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class NavigationTest < ActionDispatch::IntegrationTest + + # test "the truth" do + # assert true + # end +end + diff --git a/engines/support/test/leap_web_help_test.rb b/engines/support/test/leap_web_help_test.rb new file mode 100644 index 0000000..d74c087 --- /dev/null +++ b/engines/support/test/leap_web_help_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class LeapWebHelpTest < ActiveSupport::TestCase + test "truth" do + assert_kind_of Module, LeapWebHelp + end +end diff --git a/engines/support/test/test_helper.rb b/engines/support/test/test_helper.rb new file mode 100644 index 0000000..fff9173 --- /dev/null +++ b/engines/support/test/test_helper.rb @@ -0,0 +1,15 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require File.expand_path('../../../../test/dummy/config/environment', __FILE__) +require "rails/test_help" + +Rails.backtrace_cleaner.remove_silencers! + +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + +# Load fixtures from the engine +if ActiveSupport::TestCase.method_defined?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) +end diff --git a/engines/support/test/unit/account_extension_test.rb b/engines/support/test/unit/account_extension_test.rb new file mode 100644 index 0000000..aba162c --- /dev/null +++ b/engines/support/test/unit/account_extension_test.rb @@ -0,0 +1,12 @@ +require 'test_helper' + +class AccountExtensionTest < ActiveSupport::TestCase + + test "destroying an account triggers ticket destruction" do + t = FactoryGirl.create :ticket_with_creator + u = t.created_by_user + Account.new(u).destroy + assert_equal nil, Ticket.find(t.id) + end + +end diff --git a/engines/support/test/unit/ticket_comment_test.rb b/engines/support/test/unit/ticket_comment_test.rb new file mode 100644 index 0000000..fe8cc95 --- /dev/null +++ b/engines/support/test/unit/ticket_comment_test.rb @@ -0,0 +1,59 @@ +require 'test_helper' + +class TicketCommentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end + +=begin + setup do + @sample_ticket = Ticket.create :title => 'test ticket' + @sample_ticket.save + end +=end + + test "create" do + + comment2 = TicketComment.new :body => "help my email is broken!" + assert comment2.valid? + #assert_not_nil comment2.posted_at #? + #assert_nil comment2.posted_by #if not logged in #TODO + + #comment.ticket = testticket #Ticket.find_by_title("testing") + #assert_equal testticket.title, comment.ticket.title + + #tc.ticket = Ticket.find_by_title("test title") + #tc.ticket.title + end + +=begin + test "create authenticated comment" do + User.current = 4 + comment2 = TicketComment.new :body => "help my email is broken!" + comment2.valid? #save # should not save comment + assert_not_nil comment2.posted_by + end +=end + + test "add comments" do + testticket = Ticket.create :subject => "testing" + assert_equal testticket.comments.count, 0 + comment = TicketComment.new :body => "my email broke" + #assert comment.valid? #validating or saving necessary for setting posted_at + #assert_not_nil comment.posted_at + + testticket.comments << comment + assert_equal testticket.comments.count, 1 + sleep(1) # so first comment has earlier posted_at time + comment2 = TicketComment.new :body => "my email broke" + testticket.comments << comment2 #this should validate comment2 + testticket.valid? + assert_equal testticket.comments.count, 2 + testticket.reload.destroy + # where should posted_at be set? + #assert_not_nil comment.posted_at + #assert_not_nil testticket.comments.last.posted_at + #assert testticket.comments.first.posted_at < testticket.comments.last.posted_at + end + +end diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb new file mode 100644 index 0000000..f5e6ea7 --- /dev/null +++ b/engines/support/test/unit/ticket_test.rb @@ -0,0 +1,88 @@ +require 'test_helper' + +class TicketTest < ActiveSupport::TestCase + + test "ticket with default attribs is valid" do + t = FactoryGirl.build :ticket + assert t.valid? + end + + test "ticket without email is valid" do + t = FactoryGirl.build :ticket, email: "" + assert t.valid? + end + + test "ticket validates email format" do + t = FactoryGirl.build :ticket, email: "aswerssfd" + assert !t.valid? + end + + test "ticket open states" do + t = FactoryGirl.build :ticket + assert t.is_open + t.close + assert !t.is_open + t.reopen + assert t.is_open + end + + test "creation validated" do + @sample = Ticket.new + assert !@sample.is_creator_validated? + #p current_user + @sample.created_by = 22 #current_user + assert @sample.is_creator_validated? + end + + test "destroy all tickets from a user" do + t = FactoryGirl.create :ticket_with_creator + u = t.created_by_user + Ticket.destroy_all_from(u) + assert_equal nil, Ticket.find(t.id) + end +=begin +# TODO: do once have current_user stuff in order + test "code if & only if not creator-validated" do + User.current_test = nil + t1 = Ticket.create :subject => 'test title' + assert_not_nil t1.code + assert_nil t1.created_by + + User.current_test = 4 + t2 = Ticket.create :subject => 'test title' + assert_nil t2.code + assert_not_nil t2.created_by + end +=end + + + 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 + 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? + + + testticket = Ticket.create :subject => "test retrieving commented tickets" + comment = TicketComment.new :body => "my email broke", :posted_by => "123" + assert_equal 0, testticket.comments.count + assert_equal [], Ticket.by_includes_post_by.key('123').all + + testticket.comments << comment + testticket.save + assert_equal 1, testticket.reload.comments.count + assert_equal [testticket], Ticket.by_includes_post_by.key('123').all + + comment = TicketComment.new :body => "another comment", :posted_by => "123" + testticket.comments << comment + testticket.save + + # this will ensure that the ticket is only included once, even though the user has commented on the ticket twice: + assert_equal [testticket], Ticket.by_includes_post_by.key('123').all + + testticket.destroy + assert_equal [], Ticket.by_includes_post_by.key('123').all; + end + +end diff --git a/help/Gemfile b/help/Gemfile deleted file mode 100644 index ad7d29b..0000000 --- a/help/Gemfile +++ /dev/null @@ -1,15 +0,0 @@ -source "https://rubygems.org" - -eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb')) -eval(File.read(File.dirname(__FILE__) + '/..//ui_dependencies.rb')) - -# We require leap_web_core from here so we can use the path option. -gem "leap_web_core", :path => '../core' - -# Declare your gem's dependencies in leap_web_users.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. -gemspec - -# To use debugger -# gem 'ruby-debug' diff --git a/help/README.md b/help/README.md deleted file mode 100644 index c9573e6..0000000 --- a/help/README.md +++ /dev/null @@ -1 +0,0 @@ -Implements a simple, clean, and easy to use help ticketing system. \ No newline at end of file diff --git a/help/Rakefile b/help/Rakefile deleted file mode 100644 index 0e73163..0000000 --- a/help/Rakefile +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env rake - -require 'rake/packagetask' -require 'rubygems/package_task' - -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end -begin - require 'rdoc/task' -rescue LoadError - require 'rdoc/rdoc' - require 'rake/rdoctask' - RDoc::Task = Rake::RDocTask -end - -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'LeapWebHelp' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = eval(File.read('leap_web_help.gemspec')) -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - - -task :default => :test diff --git a/help/app/assets/javascripts/tickets.js b/help/app/assets/javascripts/tickets.js deleted file mode 100644 index 18537aa..0000000 --- a/help/app/assets/javascripts/tickets.js +++ /dev/null @@ -1,4 +0,0 @@ -//$(document).ready(function () { -// $.fn.editable.defaults.mode = 'inline'; -// $('#subject').editable(); -//}); \ No newline at end of file diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb deleted file mode 100644 index d65ee43..0000000 --- a/help/app/controllers/tickets_controller.rb +++ /dev/null @@ -1,153 +0,0 @@ -class TicketsController < ApplicationController - include AutoTicketsPathHelper - - respond_to :html, :json - #has_scope :open, :type => boolean - - before_filter :require_login, :only => [:index] - before_filter :fetch_ticket, :only => [:show, :update, :destroy] # don't now have an edit method - before_filter :fetch_user - before_filter :set_title - - def new - @ticket = Ticket.new - @ticket.comments.build - end - - def create - @ticket = Ticket.new(params[:ticket]) - - @ticket.comments.last.posted_by = (logged_in? ? current_user.id : nil) #protecting posted_by isn't working, so this should protect it. - @ticket.comments.last.private = false unless admin? - @ticket.created_by = current_user.id if logged_in? - @ticket.email = current_user.email_address if logged_in? and current_user.email_address - - if @ticket.save - flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) - end - - # cannot set this until ticket has been saved, as @ticket.id will not be set - if !logged_in? and flash[:notice] - flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) - end - respond_with(@ticket, :location => auto_ticket_path(@ticket)) - end - - def show - @comment = TicketComment.new - if !@ticket - redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => t(:ticket)) - return - end - end - - def update - if params[:commit] == 'close' - @ticket.is_open = false - @ticket.save - redirect_to_tickets - elsif params[:commit] == 'open' - @ticket.is_open = true - @ticket.save - redirect_to auto_ticket_path(@ticket) - else - @ticket.attributes = cleanup_ticket_params(params[:ticket]) - - if params[:commit] == 'reply_and_close' - @ticket.close - end - - if @ticket.comments_changed? - @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) - @ticket.comments.last.private = false unless admin? - end - - if @ticket.changed? and @ticket.save - flash[:notice] = t(:changes_saved) - redirect_to_tickets - else - flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed? - redirect_to auto_ticket_path(@ticket) - end - end - end - - def index - @all_tickets = Ticket.search(search_options(params)) - @tickets = @all_tickets.page(params[:page]).per(APP_CONFIG[:pagination_size]) - end - - def destroy - # should we allow non-admins to delete their own tickets? i don't think necessary. - @ticket.destroy if admin? - redirect_to auto_tickets_path - end - - protected - - def set_title - @title = t(:tickets) - end - - private - - # - # redirects to ticket index, if appropriate. - # otherwise, just redirects to @ticket - # - def redirect_to_tickets - if logged_in? - if params[:commit] == t(:reply_and_close) - redirect_to auto_tickets_path - else - redirect_to auto_ticket_path(@ticket) - end - else - # if we are not logged in, there is no index to view - redirect_to auto_ticket_path(@ticket) - end - end - - # - # unset comments hash if no new comment was typed - # - def cleanup_ticket_params(ticket) - if ticket && ticket[:comments_attributes] - if ticket[:comments_attributes].values.first[:body].blank? - ticket[:comments_attributes] = nil - end - end - return ticket - end - - def ticket_access? - @ticket and (admin? or !@ticket.created_by or (current_user and current_user.id == @ticket.created_by)) - end - - def fetch_ticket - @ticket = Ticket.find(params[:id]) - if !@ticket and admin? - redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => 'ticket') - return - end - access_denied unless ticket_access? - end - - def fetch_user - if params[:user_id] - @user = User.find(params[:user_id]) - end - end - - # - # clean up params for ticket search - # - def search_options(params) - params.merge( - :admin_status => params[:user_id] ? 'mine' : 'all', - :user_id => @user ? @user.id : current_user.id, - :is_admin => admin? - ) - end - -end diff --git a/help/app/designs/ticket/by_includes_post_by.js b/help/app/designs/ticket/by_includes_post_by.js deleted file mode 100644 index 2eeac89..0000000 --- a/help/app/designs/ticket/by_includes_post_by.js +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: This view is only used in tests--should we keep it? -function(doc) { - var arr = {} - if (doc['type'] == 'Ticket' && doc.comments) { - doc.comments.forEach(function(comment){ - if (comment.posted_by && !arr[comment.posted_by]) { - //don't add duplicates - arr[comment.posted_by] = true; - emit(comment.posted_by, 1); - } - }); - } -} diff --git a/help/app/designs/ticket/by_includes_post_by_and_created_at.js b/help/app/designs/ticket/by_includes_post_by_and_created_at.js deleted file mode 100644 index 72169b0..0000000 --- a/help/app/designs/ticket/by_includes_post_by_and_created_at.js +++ /dev/null @@ -1,12 +0,0 @@ -function(doc) { - var arr = {} - if (doc['type'] == 'Ticket' && doc.comments) { - doc.comments.forEach(function(comment){ - if (comment.posted_by && !arr[comment.posted_by]) { - //don't add duplicates - arr[comment.posted_by] = true; - emit([comment.posted_by, doc.created_at], 1); - } - }); - } -} diff --git a/help/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js b/help/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js deleted file mode 100644 index 33dfe0b..0000000 --- a/help/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js +++ /dev/null @@ -1,12 +0,0 @@ -function(doc) { - var arr = {} - if (doc['type'] == 'Ticket' && doc.comments) { - doc.comments.forEach(function(comment){ - if (comment.posted_by && !arr[comment.posted_by]) { - //don't add duplicates - arr[comment.posted_by] = true; - emit([comment.posted_by, doc.is_open, doc.created_at], 1); - } - }); - } -} diff --git a/help/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js b/help/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js deleted file mode 100644 index 3bd2a74..0000000 --- a/help/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js +++ /dev/null @@ -1,12 +0,0 @@ -function(doc) { - var arr = {} - if (doc['type'] == 'Ticket' && doc.comments) { - doc.comments.forEach(function(comment){ - if (comment.posted_by && !arr[comment.posted_by]) { - //don't add duplicates - arr[comment.posted_by] = true; - emit([comment.posted_by, doc.is_open, doc.updated_at], 1); - } - }); - } -} diff --git a/help/app/designs/ticket/by_includes_post_by_and_updated_at.js b/help/app/designs/ticket/by_includes_post_by_and_updated_at.js deleted file mode 100644 index 2b4304f..0000000 --- a/help/app/designs/ticket/by_includes_post_by_and_updated_at.js +++ /dev/null @@ -1,12 +0,0 @@ -function(doc) { - var arr = {} - if (doc['type'] == 'Ticket' && doc.comments) { - doc.comments.forEach(function(comment){ - if (comment.posted_by && !arr[comment.posted_by]) { - //don't add duplicates - arr[comment.posted_by] = true; - emit([comment.posted_by, doc.updated_at], 1); - } - }); - } -} diff --git a/help/app/helpers/auto_tickets_path_helper.rb b/help/app/helpers/auto_tickets_path_helper.rb deleted file mode 100644 index 93f3cb9..0000000 --- a/help/app/helpers/auto_tickets_path_helper.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# These "auto" forms of the normal ticket path route helpers allow us to do two things automatically: -# -# (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. -# -# This is available both to the views and the tickets_controller. -# -module AutoTicketsPathHelper - - protected - - def auto_tickets_path(options={}) - return unless options.class == Hash - options = ticket_view_options.merge options - if @user - user_tickets_path(@user, options) - else - tickets_path(options) - end - end - - def auto_ticket_path(ticket, options={}) - options = ticket_view_options.merge options - if @user - user_ticket_path(@user, ticket, options) - else - ticket_path(ticket, options) - end - end - - def auto_new_ticket_path(options={}) - return unless options.class == Hash - options = ticket_view_options.merge options - if @user - new_user_ticket_path(@user, options) - else - new_ticket_path(options) - end - end - - private - - def ticket_view_options - hsh = {} - hsh[:open_status] = params[:open_status] if params[:open_status] && !params[:open_status].empty? - hsh[:sort_order] = params[:sort_order] if params[:sort_order] && !params[:sort_order].empty? - hsh - end - -end \ No newline at end of file diff --git a/help/app/helpers/tickets_helper.rb b/help/app/helpers/tickets_helper.rb deleted file mode 100644 index 7af50d6..0000000 --- a/help/app/helpers/tickets_helper.rb +++ /dev/null @@ -1,76 +0,0 @@ -module TicketsHelper - # - # FORM HELPERS - # - - # - # hidden fields that should be added to ever ticket form. - # these are use for proper redirection after successful actions. - # - def hidden_ticket_fields - haml_concat hidden_field_tag('open_status', params[:open_status]) - haml_concat hidden_field_tag('sort_order', params[:sort_order]) - haml_concat hidden_field_tag('user_id', params[:user_id]) - "" - end - - # - # PARAM HELPERS - # - - def search_status - if action?(:index) - params[:open_status] || 'open' - else - nil - end - end - - def search_order - params[:sort_order] || 'updated_at_desc' - end - - # - # LINK HELPERS - # - - def link_to_status(new_status) - if new_status == "open" - label = t(:open_tickets) - elsif new_status == "closed" - label = t(:closed_tickets) - elsif new_status == "all" - label = t(:all_tickets) - end - link_to label, auto_tickets_path(:open_status => new_status, :sort_order => search_order) - end - - def link_to_order(order_field) - if search_order.start_with?(order_field) - # link for currently-filtered field. Link to other direction of this field. - if search_order.end_with? 'asc' - direction = 'desc' - icon_direction = 'up' - else - direction = 'asc' - icon_direction = 'down' - end - arrow = content_tag(:i, '', class: 'icon-arrow-'+ icon_direction) - else - # for not-currently-filtered field, don't display an arrow, and link to descending direction - arrow = '' - direction = 'desc' - end - - if order_field == 'updated' - label = t(:updated) - elsif order_field == 'created' - label = t(:created) - end - - link_to auto_tickets_path(:sort_order => order_field + '_at_' + direction, :open_status => search_status) do - arrow + label - end - end - -end diff --git a/help/app/models/account_extension/tickets.rb b/help/app/models/account_extension/tickets.rb deleted file mode 100644 index f898b56..0000000 --- a/help/app/models/account_extension/tickets.rb +++ /dev/null @@ -1,13 +0,0 @@ -module AccountExtension::Tickets - extend ActiveSupport::Concern - - def destroy_with_tickets - Ticket.destroy_all_from(self.user) - destroy_without_tickets - end - - included do - alias_method_chain :destroy, :tickets - end - -end diff --git a/help/app/models/ticket.rb b/help/app/models/ticket.rb deleted file mode 100644 index cd22758..0000000 --- a/help/app/models/ticket.rb +++ /dev/null @@ -1,105 +0,0 @@ -# -# TODO: thought i should reverse keys for descending, but that didn't work. -# look into whether that should be tweaked, and whether it works okay with -# pagination (seems to now...) -# -# TODO: better validation of email -# -# TODO: don't hardcode strings 'unknown user' and 'unauthenticated user' -# -class Ticket < CouchRest::Model::Base - use_database "tickets" - - property :created_by, String, :protected => true # nil for anonymous tickets, should never be changed - property :regarding_user, String # may be nil or valid username - property :subject, String - property :email, String - property :is_open, TrueClass, :default => true - property :comments, [TicketComment] - - timestamps! - - before_validation :set_email, :set_regarding_user, :on => :create - - design do - view :by_updated_at - view :by_created_at - view :by_created_by - - view :by_is_open_and_created_at - view :by_is_open_and_updated_at - - own_path = Pathname.new(File.dirname(__FILE__)) - load_views(own_path.join('..', 'designs', 'ticket')) - end - - validates :subject, :presence => true - validates :email, :allow_blank => true, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ - - def self.search(options = {}) - @selection = TicketSelection.new(options) - @selection.tickets - end - - def self.destroy_all_from(user) - self.by_created_by.key(user.id).each do |ticket| - ticket.destroy - end - end - - def is_creator_validated? - !!created_by - end - - def set_email - self.email = nil if self.email == "" - end - - def set_regarding_user - self.regarding_user = nil if self.regarding_user == "" - end - - def close - self.is_open = false - end - - def reopen - self.is_open = true - end - - def commenters - commenters = [] - self.comments.each do |comment| - if comment.posted_by - if user = User.find(comment.posted_by) - commenters << user.login if user and !commenters.include?(user.login) - else - commenters << 'unknown user' if !commenters.include?('unknown user') - end - else - commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user') - end - end - commenters.join(', ') - end - - # - # update comments. User should be set by controller. - # - def comments_attributes=(attributes) - if attributes - comment = TicketComment.new(attributes.values.first) - comment.posted_at = Time.now - comments << comment - end - end - - def created_by_user - User.find(self.created_by) - end - - def regarding_user_actual_user - User.find_by_login(self.regarding_user) - end - -end diff --git a/help/app/models/ticket_comment.rb b/help/app/models/ticket_comment.rb deleted file mode 100644 index bed5237..0000000 --- a/help/app/models/ticket_comment.rb +++ /dev/null @@ -1,43 +0,0 @@ -class TicketComment - include CouchRest::Model::Embeddable - - #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way? - property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this. - # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL - property :posted_at, Time#, :protected => true - #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created - property :body, String - property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin - - # ? timestamps! - validates :body, :presence => true - #before_validation :set_time#, :set_posted_by - - #design do - # view :by_posted_at - # view :by_body - #end - - def is_comment_validated? - !!posted_by - end - - def posted_by_user - User.find(posted_by) if posted_by - end - -=begin - #TODO. - #this is resetting all comments associated with the ticket: - def set_time - self.posted_at = Time.now - end -=end - -=begin - def set_posted_by - self.posted_by = User.current if User.current - end -=end - -end diff --git a/help/app/models/ticket_selection.rb b/help/app/models/ticket_selection.rb deleted file mode 100644 index 74d5b78..0000000 --- a/help/app/models/ticket_selection.rb +++ /dev/null @@ -1,71 +0,0 @@ -class TicketSelection - - # - # supported options: - # - # user_id: id of the user (uuid string) - # open_status: open | closed | all - # sort_order: updated_at_desc | updated_at_asc | created_at_desc | created_at_asc - # admin_status: mine | all - # is_admin: true | false - # - def initialize(options = {}) - @user_id = options[:user_id].gsub /[^a-z0-9]/, '' - @open_status = allow options[:open_status], 'open', 'closed', 'all' - @sort_order = allow options[:sort_order], 'updated_at_desc', 'updated_at_asc', 'created_at_desc', 'created_at_asc' - @admin_status = allow options[:admin_status], 'mine', 'all' - @is_admin = allow options[:is_admin], false, true - end - - def tickets - Ticket.send(finder_method).startkey(startkey).endkey(endkey).send(order) - end - - protected - - def allow(source, *allowed) - if allowed.include?(source) - source - else - allowed.first - end - end - - def finder_method - method = 'by_' - method += 'includes_post_by_and_' if only_mine? - method += 'is_open_and_' if @open_status != 'all' - method += @sort_order.sub(/_(de|a)sc$/, '') - end - - def startkey - startkeys = [] - startkeys << @user_id if only_mine? - startkeys << (@open_status == 'open') if @open_status != 'all' - startkeys << 0 - startkeys = startkeys.join if startkeys.length == 1 # want string not array if just one thing in array - startkeys - end - - def endkey - endtime = Time.now + 2.days # TODO. this obviously isn't ideal - if self.startkey.is_a?(Array) - endkeys = self.startkey - endkeys.pop - endkeys << endtime - else - endtime - end - end - - def order - # we have defined the ascending method to return the view itself: - (@sort_order.end_with? 'desc') ? 'descending' : 'ascending' - end - - - def only_mine? - !@is_admin || @admin_status == 'mine' - end - -end diff --git a/help/app/views/tickets/_comment.html.haml b/help/app/views/tickets/_comment.html.haml deleted file mode 100644 index 778ca13..0000000 --- a/help/app/views/tickets/_comment.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- if admin? or !comment.private # only show comment if user is admin or comment is not private - %tr - %td.user - %div - %strong - - if comment.posted_by_user - = comment.posted_by_user.login - - else - = t(:anonymous) - %div= comment.posted_at.to_s(:short) - - if comment.posted_by_user && comment.posted_by_user.is_admin? - %div - %span.label.label-inverse - = t(:admin) - - if comment.private - %div - %span.label.label-important - = t(:private) - %td.comment - = simple_format(comment.body) \ No newline at end of file diff --git a/help/app/views/tickets/_edit_form.html.haml b/help/app/views/tickets/_edit_form.html.haml deleted file mode 100644 index 714f8ff..0000000 --- a/help/app/views/tickets/_edit_form.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -:ruby - # created by user link - if @ticket.created_by_user - created_by = link_to @ticket.created_by_user.login, @ticket.created_by_user - else - created_by = t(:anonymous) - end - - # regarding user link - if admin? - if @ticket.regarding_user_actual_user - regarding_user_link = link_to @ticket.regarding_user_actual_user.login, @ticket.regarding_user_actual_user - else - regarding_user_link = "(#{t(:unknown)})" - end - else - regarding_user_link = '' - end - -= form_for @ticket do |f| - = hidden_ticket_fields - %p.first - - if @ticket.is_open? - %span.label.label-info= t(:open) - - else - %span.label.label-success= t(:closed) - %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe - %div= t(:subject) - = f.text_field :subject, :class => 'large full-width' - .row-fluid - .span4 - %div= t(:status) - = f.select :is_open, [[t(:open), "true"], [t(:closed), "false"]] - .span4 - %div= t(:email) - = f.text_field :email - .span4 - %div - = t(:regarding_account) - = regarding_user_link - = f.text_field :regarding_user - = f.button t(:save), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'save' - - if @ticket.is_open? - = f.button t(:close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'close' - - else - = f.button t(:open), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'open' - - if admin? - = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' diff --git a/help/app/views/tickets/_new_comment_form.html.haml b/help/app/views/tickets/_new_comment_form.html.haml deleted file mode 100644 index 8418e01..0000000 --- a/help/app/views/tickets/_new_comment_form.html.haml +++ /dev/null @@ -1,13 +0,0 @@ --# --# for posting a new comment to an existing ticket. --# -= simple_form_for @ticket, :html => {:class => 'slim'} do |f| - = hidden_ticket_fields - = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c| - = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} - - if admin? - = c.input :private, :as => :boolean, :label => false, :inline_label => true - = f.button :button, t(:post_reply), :name => 'commit', :class => 'btn-primary', :type => 'submit', :value => 'post_reply' - - if logged_in? && @ticket.is_open - = f.button :button, t(:reply_and_close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'reply_and_close' - = link_to t(:cancel), auto_tickets_path, :class => :btn diff --git a/help/app/views/tickets/_tabs.html.haml b/help/app/views/tickets/_tabs.html.haml deleted file mode 100644 index b7b5d3a..0000000 --- a/help/app/views/tickets/_tabs.html.haml +++ /dev/null @@ -1,23 +0,0 @@ --# --# SORT ORDER TABS --# -- unless action?(:new) - %ul.nav.nav-pills.pull-right.slim - %li{:class=> ("active" if search_order.start_with? 'created_at')} - = link_to_order('created') - %li{:class=> ("active" if search_order.start_with? 'updated_at')} - = link_to_order('updated') - --# --# STATUS FILTER TABS --# -%ul.nav.nav-tabs - - if logged_in? - %li{:class => ("active" if search_status == 'open')} - = link_to_status 'open' - %li{:class => ("active" if search_status == 'closed')} - = link_to_status 'closed' - %li{:class => ("active" if search_status == 'all')} - = link_to_status 'all' - %li{:class => ("active" if action?(:new))} - = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path diff --git a/help/app/views/tickets/_ticket.html.haml b/help/app/views/tickets/_ticket.html.haml deleted file mode 100644 index 5bc33c8..0000000 --- a/help/app/views/tickets/_ticket.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- url = auto_ticket_path(ticket) -%tr - %td= link_to ticket.subject, url - %td= link_to ticket.created_at.to_s(:short), url - %td= link_to ticket.updated_at.to_s(:short), url - %td= ticket.commenters diff --git a/help/app/views/tickets/index.html.haml b/help/app/views/tickets/index.html.haml deleted file mode 100644 index c02a326..0000000 --- a/help/app/views/tickets/index.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -- @show_navigation = !params[:user_id].nil? - -= render 'tickets/tabs' - -%table.table.table-striped.table-bordered - %thead - %tr - %th= t(:subject) - %th= t(:created) - %th= t(:updated) - %th= t(:voices) - %tbody - - if @tickets.any? - = render @tickets.all - - else - %tr - %td{:colspan=>4}= t(:none) - -= paginate @tickets diff --git a/help/app/views/tickets/new.html.haml b/help/app/views/tickets/new.html.haml deleted file mode 100644 index 8f217a5..0000000 --- a/help/app/views/tickets/new.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -- @show_navigation = !params[:user_id].nil? - -= render 'tickets/tabs' - -- if admin? && @user - - email = @user.email_address - - regarding = @user.login -- elsif logged_in? - - email = current_user.email_address - - regarding = current_user.login - -= simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| - = hidden_ticket_fields - = f.input :subject - - if logged_in? - = f.input :email, input_html: {value: email} - = f.input :regarding_user, input_html: {value: regarding} - - else - = f.input :email - = f.input :regarding_user - = f.simple_fields_for :comments, @comment do |c| - = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5} - - if admin? - = c.input :private, :as => :boolean, :label => false, :inline_label => true - .form-actions - = f.button :submit, :class => 'btn-primary', :value => t(:create_thing, :thing => t(:ticket)) - - if logged_in? - = link_to t(:cancel), auto_tickets_path, :class => :btn - - else - = link_to t(:cancel), home_path, :class => 'btn' \ No newline at end of file diff --git a/help/app/views/tickets/show.html.haml b/help/app/views/tickets/show.html.haml deleted file mode 100644 index bfdb773..0000000 --- a/help/app/views/tickets/show.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- @show_navigation = !params[:user_id].nil? - -.ticket - = render 'tickets/edit_form' - %table.table.table-striped.table-bordered - %tbody - = render :partial => 'tickets/comment', :collection => @ticket.comments - %tr - %td.user - = logged_in? ? current_user.login : t(:anonymous) - %td.comment - = render 'tickets/new_comment_form' \ No newline at end of file diff --git a/help/config/initializers/account_lifecycle.rb b/help/config/initializers/account_lifecycle.rb deleted file mode 100644 index d9f04c1..0000000 --- a/help/config/initializers/account_lifecycle.rb +++ /dev/null @@ -1,3 +0,0 @@ -ActiveSupport.on_load(:account) do - include AccountExtension::Tickets -end diff --git a/help/config/locales/en.yml b/help/config/locales/en.yml deleted file mode 100644 index 342adea..0000000 --- a/help/config/locales/en.yml +++ /dev/null @@ -1,22 +0,0 @@ -en: - access_ticket_text: > - You can later access this ticket at the URL %{full_url}. You might want to bookmark this page to find it again. - Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to - remove it from the browser history. - support_tickets: "Support Tickets" - all_tickets: "All Tickets" - my_tickets: "My Tickets" - open_tickets: "Open Tickets" - closed_tickets: "Closed Tickets" - new_ticket: "New Ticket" - tickets: "Tickets" - subject: "Subject" - destroy: "Destroy" - open: "Open" - closed: "Closed" - close: "Close" - post_reply: "Post Reply" - reply_and_close: "Reply and Close" - description: "Description" - ticket: "Ticket" - regarding_account: "Regarding Account" \ No newline at end of file diff --git a/help/config/routes.rb b/help/config/routes.rb deleted file mode 100644 index 23e0c11..0000000 --- a/help/config/routes.rb +++ /dev/null @@ -1,8 +0,0 @@ -Rails.application.routes.draw do - scope "(:locale)", :locale => MATCH_LOCALE do - resources :tickets, :except => :edit - resources :users do - resources :tickets, :except => :edit - end - end -end diff --git a/help/leap_web_help.gemspec b/help/leap_web_help.gemspec deleted file mode 100644 index ac6d78d..0000000 --- a/help/leap_web_help.gemspec +++ /dev/null @@ -1,18 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -require File.expand_path('../../lib/leap_web/version.rb', __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "leap_web_help" - s.version = LeapWeb::VERSION - s.authors = ["Jessib"] - s.email = ["jessib@leap.se"] - s.homepage = "http://www.leap.se" - s.summary = "Help Desk for LeapWeb" - s.description = "Managing Tickets for a Leap provider" - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] - s.test_files = Dir["test/**/*"] - -end diff --git a/help/lib/leap_web_help.rb b/help/lib/leap_web_help.rb deleted file mode 100644 index f5b04aa..0000000 --- a/help/lib/leap_web_help.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "leap_web_help/engine" - -module LeapWebHelp -end diff --git a/help/lib/leap_web_help/engine.rb b/help/lib/leap_web_help/engine.rb deleted file mode 100644 index dfa763f..0000000 --- a/help/lib/leap_web_help/engine.rb +++ /dev/null @@ -1,4 +0,0 @@ -module LeapWebHelp - class Engine < ::Rails::Engine - end -end diff --git a/help/lib/tasks/leap_web_help_tasks.rake b/help/lib/tasks/leap_web_help_tasks.rake deleted file mode 100644 index 1f38982..0000000 --- a/help/lib/tasks/leap_web_help_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :leap_web_help do -# # Task goes here -# end diff --git a/help/script/rails b/help/script/rails deleted file mode 100755 index 5676ab9..0000000 --- a/help/script/rails +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/leap_web_help/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' diff --git a/help/test/factories.rb b/help/test/factories.rb deleted file mode 100644 index be04f15..0000000 --- a/help/test/factories.rb +++ /dev/null @@ -1,18 +0,0 @@ -FactoryGirl.define do - - factory :ticket do - subject { Faker::Lorem.sentence } - email { Faker::Internet.email } - - factory :ticket_with_comment do - comments_attributes do - { "0" => { "body" => Faker::Lorem.sentences.join(" ") } } - end - end - - factory :ticket_with_creator do - created_by { FactoryGirl.create(:user).id } - end - end - -end diff --git a/help/test/functional/tickets_controller_test.rb b/help/test/functional/tickets_controller_test.rb deleted file mode 100644 index 416fb73..0000000 --- a/help/test/functional/tickets_controller_test.rb +++ /dev/null @@ -1,273 +0,0 @@ -require 'test_helper' - -class TicketsControllerTest < ActionController::TestCase - - teardown do - # destroy all tickets that were created during the test - Ticket.all.each{|t| t.destroy} - end - - test "should get index if logged in" do - login - get :index - assert_response :success - assert_not_nil assigns(:tickets) - end - - test "no index if not logged in" do - get :index - assert_response :redirect - assert_nil assigns(:tickets) - end - - test "should get new" do - get :new - 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 - assert_response :success - end - - test "user tickets are not visible without login" do - user = find_record :user - ticket = find_record :ticket, :created_by => user.id - get :show, :id => ticket.id - assert_response :redirect - assert_redirected_to login_url - end - - test "user tickets are visible to creator" do - user = find_record :user - ticket = find_record :ticket, :created_by => user.id - login user - get :show, :id => ticket.id - assert_response :success - end - - test "other users tickets are not visible" do - other_user = find_record :user - ticket = find_record :ticket, :created_by => other_user.id - login - get :show, :id => ticket.id - assert_response :redirect - assert_redirected_to home_url - end - - test "should create unauthenticated ticket" do - params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} - - assert_difference('Ticket.count') do - post :create, :ticket => params - end - - assert_response :redirect - assert_nil assigns(:ticket).created_by - - assert_equal 1, assigns(:ticket).comments.count - assert_nil assigns(:ticket).comments.first.posted_by - - end - - test "should create authenticated ticket" do - - params = {:subject => "auth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} - - login - - assert_difference('Ticket.count') do - post :create, :ticket => params - end - - assert_response :redirect - - assert_not_nil assigns(:ticket).created_by - assert_equal assigns(:ticket).created_by, @current_user.id - assert_equal assigns(:ticket).email, @current_user.email_address - - assert_equal 1, assigns(:ticket).comments.count - assert_not_nil assigns(:ticket).comments.first.posted_by - assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id - end - - test "add comment to unauthenticated ticket" do - ticket = FactoryGirl.create :ticket, :created_by => nil - - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - - assert_equal ticket, assigns(:ticket) # still same ticket, with different comments - assert_not_equal ticket.comments, assigns(:ticket).comments # ticket == assigns(:ticket), but they have different comments (which we want) - - end - - - test "add comment to own authenticated ticket" do - - login - ticket = FactoryGirl.create :ticket, :created_by => @current_user.id - - #they should be able to comment if it is their ticket: - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - assert_not_equal ticket.comments, assigns(:ticket).comments - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - - end - - - test "cannot comment if it is not your ticket" do - - other_user = find_record :user - login :is_admin? => false, :email => nil - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - # they should *not* be able to comment if it is not their ticket - put :update, :id => ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"not allowed comment"}} } - assert_response :redirect - assert_access_denied - - assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body) - - end - - - test "admin add comment to authenticated ticket" do - - other_user = find_record :user - login :is_admin? => true - - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - - #admin should be able to comment: - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - assert_not_equal ticket.comments, assigns(:ticket).comments - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - end - - test "tickets by admin" do - other_user = find_record :user - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - - login :is_admin? => true - - get :index, {:admin_status => "all", :open_status => "open"} - assert assigns(:all_tickets).count > 0 - - # if we close one ticket, the admin should have 1 less open ticket - assert_difference('assigns[:all_tickets].count', -1) do - assigns(:tickets).first.close - assigns(:tickets).first.save - get :index, {:admin_status => "all", :open_status => "open"} - end - end - - - test "admin_status mine vs all" do - testticket = FactoryGirl.create :ticket - user = find_record :user - login :is_admin? => true, :email => nil - - get :index, {:open_status => "open"} - assert assigns(:all_tickets).include?(testticket) - get :index, {:user_id => user.id, :open_status => "open"} - assert !assigns(:all_tickets).include?(testticket) - end - - test "commenting on a ticket adds to tickets that are mine" do - testticket = FactoryGirl.create :ticket - user = find_record :admin_user - login user - get :index, {:user_id => user.id, :open_status => "open"} - assert_difference('assigns[:all_tickets].count') do - put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} - get :index, {:user_id => user.id, :open_status => "open"} - end - - assert assigns(:all_tickets).include?(assigns(:ticket)) - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - end - - test "admin ticket ordering" do - tickets = FactoryGirl.create_list :ticket, 2 - - login :is_admin? => true, :email => nil - get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_desc'} - - # this will consider all tickets, not just those on first page - first_tick = assigns(:all_tickets).all.first - last_tick = assigns(:all_tickets).all.last - assert first_tick.created_at > last_tick.created_at - - # and now reverse order: - get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_asc'} - - assert_equal first_tick, assigns(:all_tickets).last - assert_equal last_tick, assigns(:all_tickets).first - - assert_not_equal first_tick, assigns(:all_tickets).first - assert_not_equal last_tick, assigns(:all_tickets).last - - end - - test "tickets for regular user" do - login - ticket = FactoryGirl.create :ticket - other_ticket = FactoryGirl.create :ticket - - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - - get :index, {:open_status => "open"} - assert assigns(:all_tickets).count > 0 - assert assigns(:all_tickets).include?(ticket) - assert !assigns(:all_tickets).include?(other_ticket) - - # user should have one more ticket if a new tick gets a comment by this user - assert_difference('assigns[:all_tickets].count') do - put :update, :id => other_ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} - get :index, {:open_status => "open"} - end - assert assigns(:all_tickets).include?(other_ticket) - - # if we close one ticket, the user should have 1 less open ticket - assert_difference('assigns[:all_tickets].count', -1) do - other_ticket.reload - other_ticket.close - other_ticket.save - get :index, {:open_status => "open"} - end - - number_open_tickets = assigns(:all_tickets).count - - # look at closed tickets: - get :index, {:open_status => "closed"} - assert !assigns(:all_tickets).include?(ticket) - assert assigns(:all_tickets).include?(other_ticket) - number_closed_tickets = assigns(:all_tickets).count - - # all tickets should equal closed + open - get :index, {:open_status => "all"} - assert assigns(:all_tickets).include?(ticket) - assert assigns(:all_tickets).include?(other_ticket) - assert_equal assigns(:all_tickets).count, number_closed_tickets + number_open_tickets - - - end - -end - diff --git a/help/test/integration/navigation_test.rb b/help/test/integration/navigation_test.rb deleted file mode 100644 index eec8c0e..0000000 --- a/help/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/help/test/leap_web_help_test.rb b/help/test/leap_web_help_test.rb deleted file mode 100644 index d74c087..0000000 --- a/help/test/leap_web_help_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class LeapWebHelpTest < ActiveSupport::TestCase - test "truth" do - assert_kind_of Module, LeapWebHelp - end -end diff --git a/help/test/test_helper.rb b/help/test/test_helper.rb deleted file mode 100644 index 3381f44..0000000 --- a/help/test/test_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Configure Rails Environment -ENV["RAILS_ENV"] = "test" - -require File.expand_path('../../../test/dummy/config/environment', __FILE__) -require "rails/test_help" - -Rails.backtrace_cleaner.remove_silencers! - -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - -# Load fixtures from the engine -if ActiveSupport::TestCase.method_defined?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) -end diff --git a/help/test/unit/account_extension_test.rb b/help/test/unit/account_extension_test.rb deleted file mode 100644 index aba162c..0000000 --- a/help/test/unit/account_extension_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'test_helper' - -class AccountExtensionTest < ActiveSupport::TestCase - - test "destroying an account triggers ticket destruction" do - t = FactoryGirl.create :ticket_with_creator - u = t.created_by_user - Account.new(u).destroy - assert_equal nil, Ticket.find(t.id) - end - -end diff --git a/help/test/unit/ticket_comment_test.rb b/help/test/unit/ticket_comment_test.rb deleted file mode 100644 index fe8cc95..0000000 --- a/help/test/unit/ticket_comment_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'test_helper' - -class TicketCommentTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end - -=begin - setup do - @sample_ticket = Ticket.create :title => 'test ticket' - @sample_ticket.save - end -=end - - test "create" do - - comment2 = TicketComment.new :body => "help my email is broken!" - assert comment2.valid? - #assert_not_nil comment2.posted_at #? - #assert_nil comment2.posted_by #if not logged in #TODO - - #comment.ticket = testticket #Ticket.find_by_title("testing") - #assert_equal testticket.title, comment.ticket.title - - #tc.ticket = Ticket.find_by_title("test title") - #tc.ticket.title - end - -=begin - test "create authenticated comment" do - User.current = 4 - comment2 = TicketComment.new :body => "help my email is broken!" - comment2.valid? #save # should not save comment - assert_not_nil comment2.posted_by - end -=end - - test "add comments" do - testticket = Ticket.create :subject => "testing" - assert_equal testticket.comments.count, 0 - comment = TicketComment.new :body => "my email broke" - #assert comment.valid? #validating or saving necessary for setting posted_at - #assert_not_nil comment.posted_at - - testticket.comments << comment - assert_equal testticket.comments.count, 1 - sleep(1) # so first comment has earlier posted_at time - comment2 = TicketComment.new :body => "my email broke" - testticket.comments << comment2 #this should validate comment2 - testticket.valid? - assert_equal testticket.comments.count, 2 - testticket.reload.destroy - # where should posted_at be set? - #assert_not_nil comment.posted_at - #assert_not_nil testticket.comments.last.posted_at - #assert testticket.comments.first.posted_at < testticket.comments.last.posted_at - end - -end diff --git a/help/test/unit/ticket_test.rb b/help/test/unit/ticket_test.rb deleted file mode 100644 index f5e6ea7..0000000 --- a/help/test/unit/ticket_test.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'test_helper' - -class TicketTest < ActiveSupport::TestCase - - test "ticket with default attribs is valid" do - t = FactoryGirl.build :ticket - assert t.valid? - end - - test "ticket without email is valid" do - t = FactoryGirl.build :ticket, email: "" - assert t.valid? - end - - test "ticket validates email format" do - t = FactoryGirl.build :ticket, email: "aswerssfd" - assert !t.valid? - end - - test "ticket open states" do - t = FactoryGirl.build :ticket - assert t.is_open - t.close - assert !t.is_open - t.reopen - assert t.is_open - end - - test "creation validated" do - @sample = Ticket.new - assert !@sample.is_creator_validated? - #p current_user - @sample.created_by = 22 #current_user - assert @sample.is_creator_validated? - end - - test "destroy all tickets from a user" do - t = FactoryGirl.create :ticket_with_creator - u = t.created_by_user - Ticket.destroy_all_from(u) - assert_equal nil, Ticket.find(t.id) - end -=begin -# TODO: do once have current_user stuff in order - test "code if & only if not creator-validated" do - User.current_test = nil - t1 = Ticket.create :subject => 'test title' - assert_not_nil t1.code - assert_nil t1.created_by - - User.current_test = 4 - t2 = Ticket.create :subject => 'test title' - assert_nil t2.code - assert_not_nil t2.created_by - end -=end - - - 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 - 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? - - - testticket = Ticket.create :subject => "test retrieving commented tickets" - comment = TicketComment.new :body => "my email broke", :posted_by => "123" - assert_equal 0, testticket.comments.count - assert_equal [], Ticket.by_includes_post_by.key('123').all - - testticket.comments << comment - testticket.save - assert_equal 1, testticket.reload.comments.count - assert_equal [testticket], Ticket.by_includes_post_by.key('123').all - - comment = TicketComment.new :body => "another comment", :posted_by => "123" - testticket.comments << comment - testticket.save - - # this will ensure that the ticket is only included once, even though the user has commented on the ticket twice: - assert_equal [testticket], Ticket.by_includes_post_by.key('123').all - - testticket.destroy - assert_equal [], Ticket.by_includes_post_by.key('123').all; - end - -end diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb index a9a195e..95f5d92 100644 --- a/lib/extensions/couchrest.rb +++ b/lib/extensions/couchrest.rb @@ -45,7 +45,7 @@ module CouchRest def self.load_all_models_with_engines self.load_all_models_without_engines return unless defined?(Rails) - Dir[Rails.root + '*/app/models/**/*.rb'].each do |path| + Dir[Rails.root + 'engines/*/app/models/**/*.rb'].each do |path| require path end end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 3c87b45..d96b625 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -1,18 +1,11 @@ namespace :test do - Rails::SubTestTask.new(:units => "test:prepare") do |t| - t.libs << "test" - t.pattern = '*/test/unit/**/*_test.rb' - end - - Rails::SubTestTask.new(:functionals => "test:prepare") do |t| - t.libs << "test" - t.pattern = '*/test/functional/**/*_test.rb' - end - - Rails::SubTestTask.new(:integration => "test:prepare") do |t| - t.libs << "test" - t.pattern = '*/test/integration/**/*_test.rb' + [: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" + end end end diff --git a/test/factories.rb b/test/factories.rb index 980e2aa..ac9333c 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -1,6 +1,8 @@ -Dir.glob(Rails.root.join('*','test','factories.rb')) do |factory_file| +ENGINE_FACTORY_FILES = Rails.root.join('engines','*','test','factories.rb') +Dir.glob(ENGINE_FACTORY_FILES) do |factory_file| require factory_file end + FactoryGirl.define do factory :user do diff --git a/test/test_helper.rb b/test/test_helper.rb index b844b90..d001ac7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,7 +8,7 @@ require 'mocha/setup' Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } # Load support files from all engines -Dir["#{File.dirname(__FILE__)}/../*/test/support/**/*.rb"].each { |f| require f } +Dir["#{File.dirname(__FILE__)}/../engines/*/test/support/**/*.rb"].each { |f| require f } class ActiveSupport::TestCase # Add more helper methods to be used by all tests here... -- cgit v1.2.3 From 38558224e10f3fddba29aaa25397495a0fded263 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 11 Apr 2014 10:06:02 +0200 Subject: minor: our engines do not have a db directory --- engines/billing/leap_web_billing.gemspec | 2 +- engines/support/leap_web_help.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/engines/billing/leap_web_billing.gemspec b/engines/billing/leap_web_billing.gemspec index c6ac3f3..ff11c98 100644 --- a/engines/billing/leap_web_billing.gemspec +++ b/engines/billing/leap_web_billing.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = "Billing for LeapWeb" s.description = "Billing System for a Leap provider" - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir["{app,config,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] # s.add_dependency "braintree-rails", "~> 0.4.5" diff --git a/engines/support/leap_web_help.gemspec b/engines/support/leap_web_help.gemspec index 41959d6..7b668d5 100644 --- a/engines/support/leap_web_help.gemspec +++ b/engines/support/leap_web_help.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = "Help Desk for LeapWeb" s.description = "Managing Tickets for a Leap provider" - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir["{app,config,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] end -- cgit v1.2.3 From 63010ddb7cf4ef124376d8a6f35b6de35c99d175 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 11 Apr 2014 10:35:41 +0200 Subject: move include AssertResponses into test itself it may not have been required before the RackTest support class. --- test/integration/api/srp_test.rb | 1 + test/support/rack_test.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb index 946450e..26adc8c 100644 --- a/test/integration/api/srp_test.rb +++ b/test/integration/api/srp_test.rb @@ -1,4 +1,5 @@ class SrpTest < RackTest + include AssertResponses teardown do if @user diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb index 35d191b..806339a 100644 --- a/test/support/rack_test.rb +++ b/test/support/rack_test.rb @@ -1,7 +1,8 @@ +require_relative 'assert_responses' + class RackTest < ActiveSupport::TestCase include Rack::Test::Methods include Warden::Test::Helpers - include AssertResponses CONFIG_RU = (Rails.root + 'config.ru').to_s OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first -- cgit v1.2.3 From 3d3688647fab7049e5b531c45b85c1e46a1d528f Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 10:11:18 +0200 Subject: doc: update list of engines --- DEVELOP.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index 43af04d..0bc2031 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -11,12 +11,10 @@ Some tips on modifying the views: 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. -Currently Leap Web includes of 4 Engines: +Currently Leap Web includes 2 Engines: -* [users](https://github.com/leapcode/leap_web/blob/master/users) - user registration and authorization - this will probably be included in the main webapp any time soon -* [certs](https://github.com/leapcode/leap_web/blob/master/certs) - Cert distribution for the EIP client - might be included in toplevel too. -* [help](https://github.com/leapcode/leap_web/blob/master/help) - Help ticket management -* [billing](https://github.com/leapcode/leap_web/blob/master/billing) - Billing System +* [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 ## -- cgit v1.2.3