From e8032fd9027435c57572fd0d8bab411841859cbc Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 23 Dec 2013 16:44:18 -0800 Subject: Initial start to messages API. --- users/app/controllers/v1/messages_controller.rb | 25 +++++++++++++++++++++++++ users/app/models/message.rb | 10 ++++++++++ users/app/models/user_message.rb | 22 ++++++++++++++++++++++ users/config/routes.rb | 2 ++ 4 files changed, 59 insertions(+) create mode 100644 users/app/controllers/v1/messages_controller.rb create mode 100644 users/app/models/message.rb create mode 100644 users/app/models/user_message.rb diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb new file mode 100644 index 0000000..e67e2a3 --- /dev/null +++ b/users/app/controllers/v1/messages_controller.rb @@ -0,0 +1,25 @@ +module V1 + class MessagesController < ApplicationController + + # TODO need to add authentication + respond_to :json + + def user_messages(unseen = true) + user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [params[:user_id], false]).all : UserMessage.by_user_id(:key => params[:user_id]).all + + messages = [] + user_messages.each do |um| + messages << Message.find(um.message.id) + end + + render json: messages + end + + + # only for PUT + def mark_read + # params[:user_id] params[:message_id] + end + + end +end diff --git a/users/app/models/message.rb b/users/app/models/message.rb new file mode 100644 index 0000000..38fa71e --- /dev/null +++ b/users/app/models/message.rb @@ -0,0 +1,10 @@ +class Message < CouchRest::Model::Base + + use_database :messages + + property :text, String + + design do + end + +end diff --git a/users/app/models/user_message.rb b/users/app/models/user_message.rb new file mode 100644 index 0000000..0b2ce17 --- /dev/null +++ b/users/app/models/user_message.rb @@ -0,0 +1,22 @@ +class UserMessage < CouchRest::Model::Base + + use_database :user_messages + belongs_to :user + belongs_to :message + + validates :user_id, presence: true + validates :message_id, presence: true + + + property :seen, TrueClass, :default => false + + design do + view :by_user_id + view :by_message_id + view :by_user_id_and_seen + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'user_message')) + + end + +end diff --git a/users/config/routes.rb b/users/config/routes.rb index 736b283..9a7c531 100644 --- a/users/config/routes.rb +++ b/users/config/routes.rb @@ -6,6 +6,8 @@ Rails.application.routes.draw do resources :sessions, :only => [:new, :create, :update] delete "logout" => "sessions#destroy", :as => "logout" resources :users, :only => [:create, :update, :destroy, :index] + get "user_messages/:user_id" => "messages#user_messages" + put "mark_read/:user_id/:message_id" => "messages#mark_read" end scope "(:locale)", :locale => MATCH_LOCALE do -- cgit v1.2.3 From a9ff52501e9c04edacd250dd94ee3f3ad28cd73d Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 24 Dec 2013 11:13:28 -0800 Subject: API method to mark a user's message as read (will refactor) --- users/app/controllers/v1/messages_controller.rb | 13 +++++++++++-- users/app/models/user_message.rb | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index e67e2a3..a4e9aec 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -4,6 +4,7 @@ module V1 # TODO need to add authentication respond_to :json + # for now, will not pass unseen, so unseen will always be true def user_messages(unseen = true) user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [params[:user_id], false]).all : UserMessage.by_user_id(:key => params[:user_id]).all @@ -16,9 +17,17 @@ module V1 end - # only for PUT + # routes ensure this is only for PUT def mark_read - # params[:user_id] params[:message_id] + user_message = UserMessage.find_by_user_id_and_message_id([params[:user_id], params[:message_id]]) + user_message.seen = true + + # TODO what to return? + if user_message.save + render json: true + else + render json: false + end end end diff --git a/users/app/models/user_message.rb b/users/app/models/user_message.rb index 0b2ce17..37aae0b 100644 --- a/users/app/models/user_message.rb +++ b/users/app/models/user_message.rb @@ -7,6 +7,8 @@ class UserMessage < CouchRest::Model::Base validates :user_id, presence: true validates :message_id, presence: true + # should not have multiple rows connecting one user to particular message: + validates_uniqueness_of :user_id, :scope => [:message_id] property :seen, TrueClass, :default => false @@ -14,6 +16,7 @@ class UserMessage < CouchRest::Model::Base view :by_user_id view :by_message_id view :by_user_id_and_seen + view :by_user_id_and_message_id own_path = Pathname.new(File.dirname(__FILE__)) load_views(own_path.join('..', 'designs', 'user_message')) -- cgit v1.2.3 From e4390e2ee5b2df20038f12865db462cf1e208ee6 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 24 Dec 2013 12:23:04 -0800 Subject: Add API tests and some refactoring of messages so we can get a user's messages within the webapp. --- users/app/controllers/v1/messages_controller.rb | 10 +---- users/app/models/user.rb | 12 ++++++ .../test/functional/v1/messages_controller_test.rb | 44 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 users/test/functional/v1/messages_controller_test.rb diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index a4e9aec..fa98042 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -6,14 +6,8 @@ module V1 # for now, will not pass unseen, so unseen will always be true def user_messages(unseen = true) - user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [params[:user_id], false]).all : UserMessage.by_user_id(:key => params[:user_id]).all - - messages = [] - user_messages.each do |um| - messages << Message.find(um.message.id) - end - - render json: messages + user = User.find(params[:user_id]) + render json: (user ? user.messages : [] ) end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 720f5a9..fe3a127 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -72,6 +72,18 @@ class User < CouchRest::Model::Base Ticket.for_user(self).limit(count).all #defaults to having most recent updated first end + def messages(unseen = true) + + user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [self.id, false]).all : UserMessage.by_user_id(:key => self.id).all + + messages = [] + user_messages.each do |um| + messages << Message.find(um.message.id) + end + messages + + end + # DEPRECATED # # Please set the key on the identity directly diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb new file mode 100644 index 0000000..de2182b --- /dev/null +++ b/users/test/functional/v1/messages_controller_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + + +class V1::MessagesControllerTest < ActionController::TestCase + + #TODO ensure authentication for all tests here + + setup do + @user = FactoryGirl.build(:user) + @user.save + @message = Message.new(:text => 'a test message') + @message.save + @user_message = UserMessage.new(:message_id => @message.id, :user_id => @user.id) + @user_message.save + end + + teardown do + @user_message.destroy + @user.destroy + @message.destroy + end + + test "get messages for user" do + get :user_messages, :user_id => @user.id + assert response.body.include? @message.text + assert response.body.include? @message.id + end + + test "mark message read for user" do + assert !@user_message.seen + put :mark_read, :user_id => @user.id, :message_id => @message.id + @user_message.reload + assert @user_message.seen + end + + test "do not get seen messages" do + @user_message.seen = true + @user_message.save + get :user_messages, :user_id => @user.id + assert !(response.body.include? @message.text) + assert !(response.body.include? @message.id) + end + +end -- cgit v1.2.3 From 7f12c795207ac818bffac42aa581bf1165f9e424 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 24 Dec 2013 13:27:22 -0800 Subject: Catching some corner cases & new tests. --- users/app/controllers/v1/messages_controller.rb | 5 ++--- users/test/functional/v1/messages_controller_test.rb | 12 +++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index fa98042..d49b161 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -10,14 +10,13 @@ module V1 render json: (user ? user.messages : [] ) end - # routes ensure this is only for PUT def mark_read user_message = UserMessage.find_by_user_id_and_message_id([params[:user_id], params[:message_id]]) - user_message.seen = true + user_message.seen = true if user_message # TODO what to return? - if user_message.save + if user_message and user_message.save render json: true else render json: false diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb index de2182b..7bffa8f 100644 --- a/users/test/functional/v1/messages_controller_test.rb +++ b/users/test/functional/v1/messages_controller_test.rb @@ -1,6 +1,5 @@ require 'test_helper' - class V1::MessagesControllerTest < ActionController::TestCase #TODO ensure authentication for all tests here @@ -31,6 +30,7 @@ class V1::MessagesControllerTest < ActionController::TestCase put :mark_read, :user_id => @user.id, :message_id => @message.id @user_message.reload assert @user_message.seen + assert_json_response true end test "do not get seen messages" do @@ -41,4 +41,14 @@ class V1::MessagesControllerTest < ActionController::TestCase assert !(response.body.include? @message.id) end + test "empty messages for non-existing user" do + get :user_messages, :user_id => 'some random string' + assert_json_response [] + end + + test "mark read responds even with bad inputs" do + put :mark_read, :user_id => 'nonsense', :message_id => 'more nonsense' + assert_json_response false + end + end -- cgit v1.2.3 From fac34e1fb21d9310227c4cfc28c3ef03806ba465 Mon Sep 17 00:00:00 2001 From: jessib Date: Thu, 26 Dec 2013 12:37:26 -0800 Subject: Very very rough start to having messages for payment automatically created. --- Gemfile.lock | 5 +++++ users/app/models/user.rb | 20 ++++++++++++++++++++ users/config/locales/en.yml | 1 + users/config/schedule.rb | 24 ++++++++++++++++++++++++ users/leap_web_users.gemspec | 1 + users/lib/leap_web_users/engine.rb | 1 + 6 files changed, 52 insertions(+) create mode 100644 users/config/schedule.rb diff --git a/Gemfile.lock b/Gemfile.lock index cc549ee..7d5f798 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,7 @@ PATH leap_web_core (= 0.5.0.rc) rails_warden ruby-srp (~> 0.2.1) + whenever GEM remote: https://rubygems.org/ @@ -88,6 +89,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + chronic (0.9.1) client_side_validations (3.2.6) client_side_validations-simple_form (2.1.0) client_side_validations (~> 3.2.5) @@ -253,6 +255,9 @@ GEM warden (1.2.3) rack (>= 1.0) websocket-driver (0.3.1) + whenever (0.8.2) + activesupport (>= 2.3.4) + chronic (>= 0.6.3) xpath (2.0.0) nokogiri (~> 1.3) diff --git a/users/app/models/user.rb b/users/app/models/user.rb index fe3a127..28adc54 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -122,6 +122,26 @@ class User < CouchRest::Model::Base ServiceLevel.new({id: code}) end + def one_month_warning_to_pay + # get all users who are not customers with active subscription and have existed for exactly a month (take account of months having difft amount of days. Maybe jsut those who signed up 30 days ago?) + #users_to_warn = User.find_by_created_at(Time.now-1.month).all #NO, this will require time to be right + #users_1_month_old = User.by_created_at.startkey(Time.now-1.month-1.day).endkey(Time.now-1.month).al + users_30_days_old = User.by_created_at.startkey(Time.now-31.days).endkey(Time.now-30.days).all + # TODO, now, limit users to those who do not have a braintree customer, or have a braintree customer without an active subscription. This might have to happen when we are looping through anyway. + #users_to_warn = + + # TODO: only create message if any messages are going to be created. + # create a message for today's date + message = Message.new(:text => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + message.save + + # for each such user, create a user message for that user and the message + users_to_warn.each do |user_to_warn| + user_message = UserMessage.new(:message_id => message.id, :user_id => user_to_warn.id) + user_message.save + end + end + protected ## diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml index 934fcee..0db63eb 100644 --- a/users/config/locales/en.yml +++ b/users/config/locales/en.yml @@ -38,6 +38,7 @@ en: 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" # # overview diff --git a/users/config/schedule.rb b/users/config/schedule.rb new file mode 100644 index 0000000..1b2b171 --- /dev/null +++ b/users/config/schedule.rb @@ -0,0 +1,24 @@ +# Use this file to easily define all of your cron jobs. +# +# It's helpful, but not entirely necessary to understand cron before proceeding. +# http://en.wikipedia.org/wiki/Cron + +# Example: +# +# set :output, "/path/to/my/cron_log.log" +# +# every 2.hours do +# command "/usr/bin/some_great_command" +# runner "MyModel.some_method" +# rake "some:great:rake:task" +# end +# +# every 4.days do +# runner "AnotherModel.prune_old_records" +# end + +# Learn more: http://github.com/javan/whenever + +every 1.day, :at => '1am' do + runner "User.one_month_warning_to_pay" +end diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index 7d1f220..06965a7 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -19,4 +19,5 @@ Gem::Specification.new do |s| s.add_dependency "ruby-srp", "~> 0.2.1" s.add_dependency "rails_warden" + s.add_dependency "whenever" end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb index f8ed71c..61131ef 100644 --- a/users/lib/leap_web_users/engine.rb +++ b/users/lib/leap_web_users/engine.rb @@ -8,6 +8,7 @@ require "warden/session_serializer" require "warden/strategies/secure_remote_password" require "webfinger" +require "whenever" module LeapWebUsers class Engine < ::Rails::Engine -- cgit v1.2.3 From 7d1a25c2477b9607475f6b4c56f94d392c46950a Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 30 Dec 2013 11:41:51 -0800 Subject: Not actually how we want to do this, but at least finish outlined part, that will later be replaced. --- users/app/models/user.rb | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 28adc54..bf48185 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -123,22 +123,30 @@ class User < CouchRest::Model::Base end def one_month_warning_to_pay - # get all users who are not customers with active subscription and have existed for exactly a month (take account of months having difft amount of days. Maybe jsut those who signed up 30 days ago?) + # get all users who are not customers with active subscription and have existed for exactly a month (take account of months having difft amount of days. Maybe just those who signed up 30 days ago?) #users_to_warn = User.find_by_created_at(Time.now-1.month).all #NO, this will require time to be right #users_1_month_old = User.by_created_at.startkey(Time.now-1.month-1.day).endkey(Time.now-1.month).al users_30_days_old = User.by_created_at.startkey(Time.now-31.days).endkey(Time.now-30.days).all - # TODO, now, limit users to those who do not have a braintree customer, or have a braintree customer without an active subscription. This might have to happen when we are looping through anyway. - #users_to_warn = - - # TODO: only create message if any messages are going to be created. - # create a message for today's date - message = Message.new(:text => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) - message.save - - # for each such user, create a user message for that user and the message - users_to_warn.each do |user_to_warn| - user_message = UserMessage.new(:message_id => message.id, :user_id => user_to_warn.id) - user_message.save + # TODO, above really is quite problematic, in that if the cron job fails to run on 1 day, say, the warning will not get created. + + users_30_days_old.each do |user| + + # create a user message for each user that does not has a braintree customer, or + # has a braintree customer w/out an active subscription. + unless ((customer = Customer.find_by_user(user.id)) && customer.subscriptions) + + 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 => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + @message.save + end + + user_message = UserMessage.new(:message_id => @message.id, :user_id => user.id) + # is following preferred?? + # user_message = UserMessage.new(:message => @message, :user => user) + user_message.save + end end end -- cgit v1.2.3 From 16b28882aa7659fb89e1661ed8af0c0db72642c8 Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 30 Dec 2013 15:22:44 -0800 Subject: Change structure to be more no-sql-y, rather than relational. --- users/app/controllers/v1/messages_controller.rb | 12 ++++++++ users/app/models/user.rb | 13 +++++++++ users/app/models/user_message.rb | 3 +- .../test/functional/v1/messages_controller_test.rb | 33 ++++++++++++++-------- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index d49b161..18f9f46 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -12,6 +12,7 @@ module V1 # routes ensure this is only for PUT def mark_read +=begin user_message = UserMessage.find_by_user_id_and_message_id([params[:user_id], params[:message_id]]) user_message.seen = true if user_message @@ -22,6 +23,17 @@ module V1 render json: false end end +=end + if (user = User.find(params[:user_id])) && Message.find(params[:message_id]) + user.message_ids_seen << params[:message_id] if !user.message_ids_seen.include?(params[:message_id]) #is it quicker to instead run uniq after adding? + user.message_ids_to_see.delete(params[:message_id]) + user.save + render json: true + return + else + render json: false + end + end end end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index fe3a127..fc63ae9 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -13,6 +13,9 @@ class User < CouchRest::Model::Base property :desired_service_level_code, Integer, :accessible => true property :effective_service_level_code, Integer, :accessible => true + property :message_ids_to_see, [String] + property :message_ids_seen, [String] + before_save :update_effective_service_level validates :login, :password_salt, :password_verifier, @@ -74,6 +77,7 @@ class User < CouchRest::Model::Base def messages(unseen = true) +=begin user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [self.id, false]).all : UserMessage.by_user_id(:key => self.id).all messages = [] @@ -81,6 +85,15 @@ class User < CouchRest::Model::Base messages << Message.find(um.message.id) end messages +=end + + message_ids = unseen ? self.message_ids_to_see : self.message_ids_to_see + self.message_ids_seen # TODO check unique? + + messages = [] + message_ids.each do |message_id| + messages << Message.find(message_id) + end + messages end diff --git a/users/app/models/user_message.rb b/users/app/models/user_message.rb index 37aae0b..51ca46c 100644 --- a/users/app/models/user_message.rb +++ b/users/app/models/user_message.rb @@ -1,4 +1,5 @@ -class UserMessage < CouchRest::Model::Base +# TODO WOULD NOT USE anymore, in proposed alternate, so delete file. +class XXXXUserMessage < CouchRest::Model::Base use_database :user_messages belongs_to :user diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb index 7bffa8f..d7ada82 100644 --- a/users/test/functional/v1/messages_controller_test.rb +++ b/users/test/functional/v1/messages_controller_test.rb @@ -1,20 +1,23 @@ require 'test_helper' class V1::MessagesControllerTest < ActionController::TestCase - + #TODO ensure authentication for all tests here setup do - @user = FactoryGirl.build(:user) - @user.save @message = Message.new(:text => 'a test message') @message.save - @user_message = UserMessage.new(:message_id => @message.id, :user_id => @user.id) - @user_message.save + @user = FactoryGirl.build(:user) + @user.message_ids_to_see << @message.id + @user.save + + # @user_message = UserMessage.new(:message_id => @message.id, :user_id => @user.id) + # @user_message.save + end teardown do - @user_message.destroy + # @user_message.destroy @user.destroy @message.destroy end @@ -26,16 +29,24 @@ class V1::MessagesControllerTest < ActionController::TestCase end test "mark message read for user" do - assert !@user_message.seen + #assert !@user_message.seen + assert @user.message_ids_to_see.include?(@message.id) + assert !@user.message_ids_seen.include?(@message.id) + put :mark_read, :user_id => @user.id, :message_id => @message.id - @user_message.reload - assert @user_message.seen + #@user_message.reload + #assert @user_message.seen + @user.reload + assert !@user.message_ids_to_see.include?(@message.id) + assert @user.message_ids_seen.include?(@message.id) assert_json_response true end test "do not get seen messages" do - @user_message.seen = true - @user_message.save + # @user_message.seen = true + # @user_message.save + put :mark_read, :user_id => @user.id, :message_id => @message.id + @user.reload get :user_messages, :user_id => @user.id assert !(response.body.include? @message.text) assert !(response.body.include? @message.id) -- cgit v1.2.3 From 4dae5eaa1bdb211d9d3ff29d5a9e9a86424e9748 Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 30 Dec 2013 16:22:38 -0800 Subject: Fixes to initial go at job to send one month warnings. --- ...by_created_at_and_one_month_warning_not_sent.js | 5 +++++ users/app/models/user.rb | 23 +++++++++++++++++++++- users/config/schedule.rb | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 users/app/designs/user/by_created_at_and_one_month_warning_not_sent.js 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 new file mode 100644 index 0000000..53a95de --- /dev/null +++ b/users/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/users/app/models/user.rb b/users/app/models/user.rb index d9b03ec..30b9ee7 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -15,6 +15,7 @@ class User < CouchRest::Model::Base property :message_ids_to_see, [String] property :message_ids_seen, [String] + property :one_month_warning_sent, TrueClass before_save :update_effective_service_level @@ -135,7 +136,10 @@ class User < CouchRest::Model::Base ServiceLevel.new({id: code}) end - def one_month_warning_to_pay + + def self.send_one_month_warnings # class not instance method + +=begin # get all users who are not customers with active subscription and have existed for exactly a month (take account of months having difft amount of days. Maybe just those who signed up 30 days ago?) #users_to_warn = User.find_by_created_at(Time.now-1.month).all #NO, this will require time to be right #users_1_month_old = User.by_created_at.startkey(Time.now-1.month-1.day).endkey(Time.now-1.month).al @@ -161,6 +165,23 @@ class User < CouchRest::Model::Base user_message.save end end +=end + + #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. this will likely be custom js view/design + users_to_warn = User.by_created_at_and_one_month_warning_not_sent.endkey(Time.now-1.month) + users_to_warn.each do |user| + 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 => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + @message.save + end + + user.message_ids_to_see << @message.id + user.one_month_warning_sent = true + user.save #?? + end + end protected diff --git a/users/config/schedule.rb b/users/config/schedule.rb index 1b2b171..9ddcf2f 100644 --- a/users/config/schedule.rb +++ b/users/config/schedule.rb @@ -20,5 +20,5 @@ # Learn more: http://github.com/javan/whenever every 1.day, :at => '1am' do - runner "User.one_month_warning_to_pay" + runner "User.send_one_month_warnings end -- cgit v1.2.3 From f9c96f8844205afe3c310b5b914752068728d38f Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 31 Dec 2013 11:48:18 -0800 Subject: Cleanup of code for messages API and cron job for 1 month payment warning. Authentication still remaining piece. --- users/app/controllers/v1/messages_controller.rb | 15 ++----- users/app/models/user.rb | 47 +++------------------- users/config/schedule.rb | 2 +- .../test/functional/v1/messages_controller_test.rb | 10 ----- 4 files changed, 9 insertions(+), 65 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index 18f9f46..42a88f7 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -12,20 +12,11 @@ module V1 # routes ensure this is only for PUT def mark_read -=begin - user_message = UserMessage.find_by_user_id_and_message_id([params[:user_id], params[:message_id]]) - user_message.seen = true if user_message - # TODO what to return? - if user_message and user_message.save - render json: true - else - render json: false - end - end -=end + # make sure user and message exist if (user = User.find(params[:user_id])) && Message.find(params[:message_id]) - user.message_ids_seen << params[:message_id] if !user.message_ids_seen.include?(params[:message_id]) #is it quicker to instead run uniq after adding? + + user.message_ids_seen << params[:message_id] if !user.message_ids_seen.include?(params[:message_id]) #TODO: is it quicker to instead call uniq! after adding? user.message_ids_to_see.delete(params[:message_id]) user.save render json: true diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 30b9ee7..6e445be 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -78,16 +78,6 @@ class User < CouchRest::Model::Base def messages(unseen = true) -=begin - user_messages = unseen ? UserMessage.by_user_id_and_seen(:key => [self.id, false]).all : UserMessage.by_user_id(:key => self.id).all - - messages = [] - user_messages.each do |um| - messages << Message.find(um.message.id) - end - messages -=end - message_ids = unseen ? self.message_ids_to_see : self.message_ids_to_see + self.message_ids_seen # TODO check unique? messages = [] @@ -137,49 +127,22 @@ class User < CouchRest::Model::Base end - def self.send_one_month_warnings # class not instance method - -=begin - # get all users who are not customers with active subscription and have existed for exactly a month (take account of months having difft amount of days. Maybe just those who signed up 30 days ago?) - #users_to_warn = User.find_by_created_at(Time.now-1.month).all #NO, this will require time to be right - #users_1_month_old = User.by_created_at.startkey(Time.now-1.month-1.day).endkey(Time.now-1.month).al - users_30_days_old = User.by_created_at.startkey(Time.now-31.days).endkey(Time.now-30.days).all - # TODO, above really is quite problematic, in that if the cron job fails to run on 1 day, say, the warning will not get created. - - users_30_days_old.each do |user| - - # create a user message for each user that does not has a braintree customer, or - # has a braintree customer w/out an active subscription. - unless ((customer = Customer.find_by_user(user.id)) && customer.subscriptions) - - 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 => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) - @message.save - end - - user_message = UserMessage.new(:message_id => @message.id, :user_id => user.id) - # is following preferred?? - # user_message = UserMessage.new(:message => @message, :user => user) - user_message.save - end - end -=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. this will likely be custom js view/design + # 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| 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 => t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + @message = Message.new(:text => I18n.t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) @message.save end user.message_ids_to_see << @message.id user.one_month_warning_sent = true - user.save #?? + user.save end end diff --git a/users/config/schedule.rb b/users/config/schedule.rb index 9ddcf2f..4ecbe7c 100644 --- a/users/config/schedule.rb +++ b/users/config/schedule.rb @@ -20,5 +20,5 @@ # Learn more: http://github.com/javan/whenever every 1.day, :at => '1am' do - runner "User.send_one_month_warnings + runner "User.send_one_month_warnings" end diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb index d7ada82..7666ba3 100644 --- a/users/test/functional/v1/messages_controller_test.rb +++ b/users/test/functional/v1/messages_controller_test.rb @@ -10,14 +10,9 @@ class V1::MessagesControllerTest < ActionController::TestCase @user = FactoryGirl.build(:user) @user.message_ids_to_see << @message.id @user.save - - # @user_message = UserMessage.new(:message_id => @message.id, :user_id => @user.id) - # @user_message.save - end teardown do - # @user_message.destroy @user.destroy @message.destroy end @@ -29,13 +24,10 @@ class V1::MessagesControllerTest < ActionController::TestCase end test "mark message read for user" do - #assert !@user_message.seen assert @user.message_ids_to_see.include?(@message.id) assert !@user.message_ids_seen.include?(@message.id) put :mark_read, :user_id => @user.id, :message_id => @message.id - #@user_message.reload - #assert @user_message.seen @user.reload assert !@user.message_ids_to_see.include?(@message.id) assert @user.message_ids_seen.include?(@message.id) @@ -43,8 +35,6 @@ class V1::MessagesControllerTest < ActionController::TestCase end test "do not get seen messages" do - # @user_message.seen = true - # @user_message.save put :mark_read, :user_id => @user.id, :message_id => @message.id @user.reload get :user_messages, :user_id => @user.id -- cgit v1.2.3 From fe3e374daa274a38723da52d929805b80f7ef383 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 31 Dec 2013 11:50:34 -0800 Subject: Removing join-model we are no longer using. --- users/app/models/user_message.rb | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 users/app/models/user_message.rb diff --git a/users/app/models/user_message.rb b/users/app/models/user_message.rb deleted file mode 100644 index 51ca46c..0000000 --- a/users/app/models/user_message.rb +++ /dev/null @@ -1,26 +0,0 @@ -# TODO WOULD NOT USE anymore, in proposed alternate, so delete file. -class XXXXUserMessage < CouchRest::Model::Base - - use_database :user_messages - belongs_to :user - belongs_to :message - - validates :user_id, presence: true - validates :message_id, presence: true - - # should not have multiple rows connecting one user to particular message: - validates_uniqueness_of :user_id, :scope => [:message_id] - - property :seen, TrueClass, :default => false - - design do - view :by_user_id - view :by_message_id - view :by_user_id_and_seen - view :by_user_id_and_message_id - own_path = Pathname.new(File.dirname(__FILE__)) - load_views(own_path.join('..', 'designs', 'user_message')) - - end - -end -- cgit v1.2.3 From 47d9b62913789358aefe769de6b7e33da8547891 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 31 Dec 2013 12:16:43 -0800 Subject: Add authentication to API, but not sure it is best way. --- users/app/controllers/v1/messages_controller.rb | 2 +- users/test/functional/v1/messages_controller_test.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index 42a88f7..b58dfe9 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -1,7 +1,7 @@ module V1 class MessagesController < ApplicationController - # TODO need to add authentication + before_filter :authorize_admin # not sure this is best way respond_to :json # for now, will not pass unseen, so unseen will always be true diff --git a/users/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb index 7666ba3..0bc09be 100644 --- a/users/test/functional/v1/messages_controller_test.rb +++ b/users/test/functional/v1/messages_controller_test.rb @@ -2,14 +2,13 @@ require 'test_helper' class V1::MessagesControllerTest < ActionController::TestCase - #TODO ensure authentication for all tests here - setup do @message = Message.new(:text => 'a test message') @message.save @user = FactoryGirl.build(:user) @user.message_ids_to_see << @message.id @user.save + login :is_admin? => true end teardown do @@ -52,4 +51,10 @@ class V1::MessagesControllerTest < ActionController::TestCase assert_json_response false end + test "fails if not admin" do + login :is_admin? => false + get :user_messages, :user_id => @user.id + assert_access_denied + end + end -- cgit v1.2.3 From c7e66852324714a166dd35dc3d5873a0053dcb9b Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 7 Jan 2014 12:57:01 -0800 Subject: Some refactoring, to simplify user model, optimize, and allow messages to be sorted by date (although are not now.) Also, rather than use whenever gem, will have cron job created to call task. --- users/app/controllers/v1/messages_controller.rb | 29 +++++++------- users/app/designs/message/by_user_ids_to_show.js | 7 ++++ .../message/by_user_ids_to_show_and_created_at.js | 8 ++++ users/app/models/message.rb | 4 ++ users/app/models/user.rb | 14 +++---- users/config/routes.rb | 3 +- users/leap_web_users.gemspec | 1 - users/lib/leap_web_users/engine.rb | 1 - users/lib/tasks/leap_web_users_tasks.rake | 6 +++ .../test/functional/v1/messages_controller_test.rb | 45 ++++++++++------------ 10 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 users/app/designs/message/by_user_ids_to_show.js create mode 100644 users/app/designs/message/by_user_ids_to_show_and_created_at.js diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index b58dfe9..371b83e 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -1,30 +1,27 @@ module V1 class MessagesController < ApplicationController - before_filter :authorize_admin # not sure this is best way + skip_before_filter :verify_authenticity_token + before_filter :authorize + respond_to :json - # for now, will not pass unseen, so unseen will always be true - def user_messages(unseen = true) - user = User.find(params[:user_id]) - render json: (user ? user.messages : [] ) + def index + render json: (current_user ? current_user.messages : [] ) end - # routes ensure this is only for PUT - def mark_read - - # make sure user and message exist - if (user = User.find(params[:user_id])) && Message.find(params[:message_id]) - - user.message_ids_seen << params[:message_id] if !user.message_ids_seen.include?(params[:message_id]) #TODO: is it quicker to instead call uniq! after adding? - user.message_ids_to_see.delete(params[:message_id]) - user.save + def update + message = Message.find(params[:id]) + if (message and current_user) + message.user_ids_to_show.delete(current_user.id) + # is it necessary to keep track of what users have already seen it?: + message.user_ids_have_shown << current_user.id if !message.user_ids_have_shown.include?(current_user.id) #TODO: is it quicker to instead call uniq! after adding? + message.save render json: true - return else render json: false end - end + 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 new file mode 100644 index 0000000..95ccd0d --- /dev/null +++ b/users/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 (userIdsToShow) { + emit(userIdsToShow, 1); + }); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7bd7c2c --- /dev/null +++ b/users/app/designs/message/by_user_ids_to_show_and_created_at.js @@ -0,0 +1,8 @@ +// not using at moment +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 (userIdsToShow) { + emit([userIdsToShow, doc.created_at], 1); + }); + } +} \ No newline at end of file diff --git a/users/app/models/message.rb b/users/app/models/message.rb index 38fa71e..16a19f4 100644 --- a/users/app/models/message.rb +++ b/users/app/models/message.rb @@ -3,8 +3,12 @@ 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? design do + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'message')) end end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 6e445be..71e57f3 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -13,8 +13,6 @@ class User < CouchRest::Model::Base property :desired_service_level_code, Integer, :accessible => true property :effective_service_level_code, Integer, :accessible => true - property :message_ids_to_see, [String] - property :message_ids_seen, [String] property :one_month_warning_sent, TrueClass before_save :update_effective_service_level @@ -77,12 +75,11 @@ class User < CouchRest::Model::Base end def messages(unseen = true) - - message_ids = unseen ? self.message_ids_to_see : self.message_ids_to_see + self.message_ids_seen # TODO check unique? - + #TODO for now this only shows unseen messages. Will we ever want seen ones? Is it necessary to store? + #Message.by_user_ids_to_show.key(self.id).all # we don't want to emit all the userids associated with a message, so looping through to only emit text and id. messages = [] - message_ids.each do |message_id| - messages << Message.find(message_id) + Message.by_user_ids_to_show.key(self.id).each do |message| + messages << [message.id, message.text] end messages @@ -140,7 +137,8 @@ class User < CouchRest::Model::Base @message.save end - user.message_ids_to_see << @message.id + @message.user_ids_to_show << user.id + @message.save user.one_month_warning_sent = true user.save end diff --git a/users/config/routes.rb b/users/config/routes.rb index 9a7c531..2819fa9 100644 --- a/users/config/routes.rb +++ b/users/config/routes.rb @@ -6,8 +6,7 @@ Rails.application.routes.draw do resources :sessions, :only => [:new, :create, :update] delete "logout" => "sessions#destroy", :as => "logout" resources :users, :only => [:create, :update, :destroy, :index] - get "user_messages/:user_id" => "messages#user_messages" - put "mark_read/:user_id/:message_id" => "messages#mark_read" + resources :messages, :only => [:index, :update] end scope "(:locale)", :locale => MATCH_LOCALE do diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index 06965a7..7d1f220 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -19,5 +19,4 @@ Gem::Specification.new do |s| s.add_dependency "ruby-srp", "~> 0.2.1" s.add_dependency "rails_warden" - s.add_dependency "whenever" end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb index 61131ef..f8ed71c 100644 --- a/users/lib/leap_web_users/engine.rb +++ b/users/lib/leap_web_users/engine.rb @@ -8,7 +8,6 @@ require "warden/session_serializer" require "warden/strategies/secure_remote_password" require "webfinger" -require "whenever" module LeapWebUsers class Engine < ::Rails::Engine diff --git a/users/lib/tasks/leap_web_users_tasks.rake b/users/lib/tasks/leap_web_users_tasks.rake index 3d0649c..62bcbe9 100644 --- a/users/lib/tasks/leap_web_users_tasks.rake +++ b/users/lib/tasks/leap_web_users_tasks.rake @@ -2,3 +2,9 @@ # 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/test/functional/v1/messages_controller_test.rb b/users/test/functional/v1/messages_controller_test.rb index 0bc09be..24a5b1f 100644 --- a/users/test/functional/v1/messages_controller_test.rb +++ b/users/test/functional/v1/messages_controller_test.rb @@ -3,57 +3,54 @@ require 'test_helper' class V1::MessagesControllerTest < ActionController::TestCase setup do - @message = Message.new(:text => 'a test message') - @message.save @user = FactoryGirl.build(:user) - @user.message_ids_to_see << @message.id @user.save - login :is_admin? => true + @message = Message.new(:text => 'a test message') + @message.user_ids_to_show << @user.id + @message.save end teardown do - @user.destroy @message.destroy + @user.destroy end test "get messages for user" do - get :user_messages, :user_id => @user.id + login @user + get :index assert response.body.include? @message.text assert response.body.include? @message.id end test "mark message read for user" do - assert @user.message_ids_to_see.include?(@message.id) - assert !@user.message_ids_seen.include?(@message.id) - - put :mark_read, :user_id => @user.id, :message_id => @message.id - @user.reload - assert !@user.message_ids_to_see.include?(@message.id) - assert @user.message_ids_seen.include?(@message.id) + 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 - put :mark_read, :user_id => @user.id, :message_id => @message.id - @user.reload - get :user_messages, :user_id => @user.id + 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 "empty messages for non-existing user" do - get :user_messages, :user_id => 'some random string' - assert_json_response [] - end test "mark read responds even with bad inputs" do - put :mark_read, :user_id => 'nonsense', :message_id => 'more nonsense' + login @user + put :update, :id => 'more nonsense' assert_json_response false end - test "fails if not admin" do - login :is_admin? => false - get :user_messages, :user_id => @user.id + test "fails if not authenticated" do + get :index, :format => :json assert_access_denied end -- cgit v1.2.3 From 90243155b425a540cb357f1c84b93effe798c9a1 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 7 Jan 2014 13:00:50 -0800 Subject: Remove schedule file now that we aren't using whenever gem. --- users/config/schedule.rb | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 users/config/schedule.rb diff --git a/users/config/schedule.rb b/users/config/schedule.rb deleted file mode 100644 index 4ecbe7c..0000000 --- a/users/config/schedule.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Use this file to easily define all of your cron jobs. -# -# It's helpful, but not entirely necessary to understand cron before proceeding. -# http://en.wikipedia.org/wiki/Cron - -# Example: -# -# set :output, "/path/to/my/cron_log.log" -# -# every 2.hours do -# command "/usr/bin/some_great_command" -# runner "MyModel.some_method" -# rake "some:great:rake:task" -# end -# -# every 4.days do -# runner "AnotherModel.prune_old_records" -# end - -# Learn more: http://github.com/javan/whenever - -every 1.day, :at => '1am' do - runner "User.send_one_month_warnings" -end -- cgit v1.2.3 From 6c478c5a1634b5da9d269c938f67d2ac4d8f03df Mon Sep 17 00:00:00 2001 From: jessib Date: Thu, 9 Jan 2014 12:18:37 -0800 Subject: Some more cleanup, but still want to make sure by_user_ids_to_show_and_created_at view is right before issuing pull request. --- Gemfile.lock | 5 ----- users/app/controllers/v1/messages_controller.rb | 5 +++-- users/app/designs/message/by_user_ids_to_show_and_created_at.js | 2 +- users/app/models/message.rb | 2 ++ users/app/models/user.rb | 9 +++------ 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b42ac86..09d3123 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,7 +42,6 @@ PATH leap_web_core (= 0.5.0.rc) rails_warden ruby-srp (~> 0.2.1) - whenever GEM remote: https://rubygems.org/ @@ -89,7 +88,6 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - chronic (0.9.1) client_side_validations (3.2.6) client_side_validations-simple_form (2.1.0) client_side_validations (~> 3.2.5) @@ -257,9 +255,6 @@ GEM warden (1.2.3) rack (>= 1.0) websocket-driver (0.3.1) - whenever (0.8.2) - activesupport (>= 2.3.4) - chronic (>= 0.6.3) xpath (2.0.0) nokogiri (~> 1.3) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index 371b83e..1b994ca 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -14,8 +14,9 @@ module V1 message = Message.find(params[:id]) if (message and current_user) message.user_ids_to_show.delete(current_user.id) - # is it necessary to keep track of what users have already seen it?: - message.user_ids_have_shown << current_user.id if !message.user_ids_have_shown.include?(current_user.id) #TODO: is it quicker to instead call uniq! after adding? + # is it necessary to keep track of what users have already seen it? + message.user_ids_have_shown << current_user.id if !message.user_ids_have_shown.include?(current_user.id) + # TODO: is it quicker to call uniq! after adding rather than check if it is already included? message.save render json: true else 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 index 7bd7c2c..bb6412b 100644 --- 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 @@ -2,7 +2,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 (userIdsToShow) { - emit([userIdsToShow, doc.created_at], 1); + emit([userIdsToShow, doc.created_at], 1); }); } } \ No newline at end of file diff --git a/users/app/models/message.rb b/users/app/models/message.rb index 16a19f4..d9ccee8 100644 --- a/users/app/models/message.rb +++ b/users/app/models/message.rb @@ -6,6 +6,8 @@ class Message < CouchRest::Model::Base 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')) diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 71e57f3..44237ff 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -76,12 +76,9 @@ class User < CouchRest::Model::Base def messages(unseen = true) #TODO for now this only shows unseen messages. Will we ever want seen ones? Is it necessary to store? - #Message.by_user_ids_to_show.key(self.id).all # we don't want to emit all the userids associated with a message, so looping through to only emit text and id. - messages = [] - Message.by_user_ids_to_show.key(self.id).each do |message| - messages << [message.id, message.text] - end - messages + + # 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 -- cgit v1.2.3 From a19d01313ac534bdbbd8381593f887f1f77c94d5 Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 13 Jan 2014 11:28:14 -0800 Subject: Comment for how to call view that we aren't now using. --- users/app/designs/message/by_user_ids_to_show_and_created_at.js | 1 + 1 file changed, 1 insertion(+) 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 index bb6412b..18969b8 100644 --- 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 @@ -1,4 +1,5 @@ // 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 (userIdsToShow) { -- cgit v1.2.3 From 3bfbf0ad20bb5b8e4689fda287cd47738571d10d Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 21 Jan 2014 11:26:19 -0800 Subject: Small optimization to saving message when sending one month warnings. --- users/app/models/user.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 44237ff..c297ac8 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -126,19 +126,23 @@ class User < CouchRest::Model::Base # 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"))) - @message.save end @message.user_ids_to_show << user.id - @message.save user.one_month_warning_sent = true user.save end + @message.save if @message end -- cgit v1.2.3 From bd867c51c4d9e3d4c6b4c55d326eb9b13b89288b Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 14:23:38 +0100 Subject: minor: move some logic from message controller into model --- users/app/controllers/v1/messages_controller.rb | 5 +---- users/app/models/message.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index 1b994ca..55292ff 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -13,10 +13,7 @@ module V1 def update message = Message.find(params[:id]) if (message and current_user) - message.user_ids_to_show.delete(current_user.id) - # is it necessary to keep track of what users have already seen it? - message.user_ids_have_shown << current_user.id if !message.user_ids_have_shown.include?(current_user.id) - # TODO: is it quicker to call uniq! after adding rather than check if it is already included? + message.mark_as_read_by(current_user) message.save render json: true else diff --git a/users/app/models/message.rb b/users/app/models/message.rb index d9ccee8..7c05e06 100644 --- a/users/app/models/message.rb +++ b/users/app/models/message.rb @@ -13,4 +13,18 @@ class Message < CouchRest::Model::Base 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) + # TODO: is it quicker to call uniq! after adding rather than check if it is already included? + end + + def read_by?(user) + user_ids_have_shown.include?(user.id) + end + + def unread_by?(user) + user_ids_to_shown.include?(user.id) + end end -- cgit v1.2.3 From 2f00e2ec3c076e4910008904bc01e09df097944e Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 14:24:08 +0100 Subject: minor: rename var that holds a single user id to userId --- users/app/designs/message/by_user_ids_to_show.js | 6 +++--- users/app/designs/message/by_user_ids_to_show_and_created_at.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/users/app/designs/message/by_user_ids_to_show.js b/users/app/designs/message/by_user_ids_to_show.js index 95ccd0d..e33566b 100644 --- a/users/app/designs/message/by_user_ids_to_show.js +++ b/users/app/designs/message/by_user_ids_to_show.js @@ -1,7 +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 (userIdsToShow) { - emit(userIdsToShow, 1); + doc.user_ids_to_show.forEach(function (userId) { + emit(userId, 1); }); } -} \ No newline at end of file +} 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 index 18969b8..54e4604 100644 --- 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 @@ -2,8 +2,8 @@ // 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 (userIdsToShow) { - emit([userIdsToShow, doc.created_at], 1); + doc.user_ids_to_show.forEach(function (userId) { + emit([userId, doc.created_at], 1); }); } -} \ No newline at end of file +} -- cgit v1.2.3 From e1243d02953b4012d6bb216efc9b0606809ab4bb Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 6 Feb 2014 09:47:37 +0100 Subject: minor: refactor token auth a bit --- .../controller_extension/token_authentication.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/users/app/controllers/controller_extension/token_authentication.rb b/users/app/controllers/controller_extension/token_authentication.rb index 530294a..cd5c074 100644 --- a/users/app/controllers/controller_extension/token_authentication.rb +++ b/users/app/controllers/controller_extension/token_authentication.rb @@ -1,11 +1,14 @@ module ControllerExtension::TokenAuthentication extend ActiveSupport::Concern - def token_authenticate - authenticate_with_http_token do |token_id, options| - @token = Token.find(token_id) + def token + @token ||= authenticate_with_http_token do |token_id, options| + Token.find(token_id) end - @token.authenticate if @token + end + + def token_authenticate + token.authenticate if token end def logout @@ -14,10 +17,7 @@ module ControllerExtension::TokenAuthentication end def clear_token - authenticate_with_http_token do |token_id, options| - @token = Token.find(token_id) - @token.destroy if @token - end + token.destroy if token end end -- cgit v1.2.3 From 3f9dc65636afb57fed441978dca4bf7d3209bd2d Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 7 Feb 2014 14:38:56 +0100 Subject: rename authorize to require_login authorize_admin -> require_admin also add require_token which will ensure token has been used for auth. --- billing/app/controllers/billing_admin_controller.rb | 2 +- billing/app/controllers/credit_card_info_controller.rb | 2 +- billing/app/controllers/customer_controller.rb | 2 +- billing/app/controllers/payments_controller.rb | 2 +- billing/app/controllers/subscriptions_controller.rb | 2 +- certs/app/controllers/certs_controller.rb | 7 +++---- help/app/controllers/tickets_controller.rb | 2 +- users/app/controllers/controller_extension/authentication.rb | 4 ++-- .../controllers/controller_extension/token_authentication.rb | 4 ++++ users/app/controllers/users_controller.rb | 4 ++-- users/app/controllers/v1/users_controller.rb | 4 ++-- users/test/functional/application_controller_test.rb | 12 ++++++------ users/test/functional/v1/sessions_controller_test.rb | 2 +- users/test/unit/unauthenticated_user_test.rb | 7 +++++++ users/test/unit/unauthorized_user_test.rb | 7 ------- 15 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 users/test/unit/unauthenticated_user_test.rb delete mode 100644 users/test/unit/unauthorized_user_test.rb diff --git a/billing/app/controllers/billing_admin_controller.rb b/billing/app/controllers/billing_admin_controller.rb index cd6149f..e11d4ee 100644 --- a/billing/app/controllers/billing_admin_controller.rb +++ b/billing/app/controllers/billing_admin_controller.rb @@ -1,5 +1,5 @@ class BillingAdminController < BillingBaseController - before_filter :authorize_admin + before_filter :require_admin def show diff --git a/billing/app/controllers/credit_card_info_controller.rb b/billing/app/controllers/credit_card_info_controller.rb index 717fa18..fbaa6f1 100644 --- a/billing/app/controllers/credit_card_info_controller.rb +++ b/billing/app/controllers/credit_card_info_controller.rb @@ -1,5 +1,5 @@ class CreditCardInfoController < ApplicationController - before_filter :authorize, :set_user + before_filter :require_login, :set_user def edit @credit_card = Braintree::CreditCard.find(params[:id]) diff --git a/billing/app/controllers/customer_controller.rb b/billing/app/controllers/customer_controller.rb index 901cb34..6cbcb44 100644 --- a/billing/app/controllers/customer_controller.rb +++ b/billing/app/controllers/customer_controller.rb @@ -1,5 +1,5 @@ class CustomerController < BillingBaseController - before_filter :authorize, :fetch_customer + before_filter :require_login, :fetch_customer def show if @customer diff --git a/billing/app/controllers/payments_controller.rb b/billing/app/controllers/payments_controller.rb index 0b5abe7..fce6570 100644 --- a/billing/app/controllers/payments_controller.rb +++ b/billing/app/controllers/payments_controller.rb @@ -1,5 +1,5 @@ class PaymentsController < BillingBaseController - before_filter :authorize, :only => [:index] + before_filter :require_login, :only => [:index] def new fetch_transparent_redirect diff --git a/billing/app/controllers/subscriptions_controller.rb b/billing/app/controllers/subscriptions_controller.rb index 01aaab4..f066b3c 100644 --- a/billing/app/controllers/subscriptions_controller.rb +++ b/billing/app/controllers/subscriptions_controller.rb @@ -1,5 +1,5 @@ class SubscriptionsController < BillingBaseController - before_filter :authorize + 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] diff --git a/certs/app/controllers/certs_controller.rb b/certs/app/controllers/certs_controller.rb index 62ef3fd..82cbc44 100644 --- a/certs/app/controllers/certs_controller.rb +++ b/certs/app/controllers/certs_controller.rb @@ -1,6 +1,6 @@ class CertsController < ApplicationController - before_filter :login_if_required + before_filter :require_login, :unless => :anonymous_certs_allowed? # GET /cert def show @@ -10,10 +10,9 @@ class CertsController < ApplicationController protected - def login_if_required - authorize unless APP_CONFIG[:allow_anonymous_certs] + 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. # diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb index c193ff4..d65ee43 100644 --- a/help/app/controllers/tickets_controller.rb +++ b/help/app/controllers/tickets_controller.rb @@ -4,7 +4,7 @@ class TicketsController < ApplicationController respond_to :html, :json #has_scope :open, :type => boolean - before_filter :authorize, :only => [:index] + 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 diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb index d831fbe..e83d6b2 100644 --- a/users/app/controllers/controller_extension/authentication.rb +++ b/users/app/controllers/controller_extension/authentication.rb @@ -15,7 +15,7 @@ module ControllerExtension::Authentication !!current_user end - def authorize + def require_login access_denied unless logged_in? end @@ -38,7 +38,7 @@ module ControllerExtension::Authentication current_user && current_user.is_admin? end - def authorize_admin + def require_admin access_denied unless admin? end diff --git a/users/app/controllers/controller_extension/token_authentication.rb b/users/app/controllers/controller_extension/token_authentication.rb index cd5c074..ee24f73 100644 --- a/users/app/controllers/controller_extension/token_authentication.rb +++ b/users/app/controllers/controller_extension/token_authentication.rb @@ -11,6 +11,10 @@ module ControllerExtension::TokenAuthentication token.authenticate if token end + def require_token + access_denied unless token + end + def logout super clear_token diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb index a5461cd..6b32d49 100644 --- a/users/app/controllers/users_controller.rb +++ b/users/app/controllers/users_controller.rb @@ -4,9 +4,9 @@ class UsersController < UsersBaseController - before_filter :authorize, :only => [:show, :edit, :update, :destroy] + before_filter :require_login, :except => [:new] + before_filter :require_admin, :only => [:index, :deactivate, :enable] before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable] - before_filter :authorize_admin, :only => [:index, :deactivate, :enable] respond_to :html diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb index 0903888..a16c6e9 100644 --- a/users/app/controllers/v1/users_controller.rb +++ b/users/app/controllers/v1/users_controller.rb @@ -3,8 +3,8 @@ module V1 skip_before_filter :verify_authenticity_token before_filter :fetch_user, :only => [:update] - before_filter :authorize, :only => [:update] - before_filter :authorize_admin, :only => [:index] + before_filter :require_login, :only => [:update, :index] + before_filter :require_admin, :only => [:index] respond_to :json diff --git a/users/test/functional/application_controller_test.rb b/users/test/functional/application_controller_test.rb index 94b77bd..c4c922b 100644 --- a/users/test/functional/application_controller_test.rb +++ b/users/test/functional/application_controller_test.rb @@ -7,21 +7,21 @@ class ApplicationControllerTest < ActionController::TestCase @controller.response = @response end - def test_authorize_redirect - @controller.send(:authorize) + def test_require_login_redirect + @controller.send(:require_login) assert_access_denied(true, false) end - def test_authorized + def test_require_login login - @controller.send(:authorize) + @controller.send(:require_login) assert_access_denied(false) end - def test_authorize_admin + def test_require_admin login @current_user.expects(:is_admin?).returns(false) - @controller.send(:authorize_admin) + @controller.send(:require_admin) assert_access_denied end diff --git a/users/test/functional/v1/sessions_controller_test.rb b/users/test/functional/v1/sessions_controller_test.rb index 4200e8f..df0d681 100644 --- a/users/test/functional/v1/sessions_controller_test.rb +++ b/users/test/functional/v1/sessions_controller_test.rb @@ -36,7 +36,7 @@ class V1::SessionsControllerTest < ActionController::TestCase post :create, :login => @user.login, 'A' => @client_hex end - test "should authorize" do + test "should authenticate" do request.env['warden'].expects(:authenticate!) @controller.stubs(:current_user).returns(@user) handshake = stub(:to_hash => {h: "ash"}) diff --git a/users/test/unit/unauthenticated_user_test.rb b/users/test/unit/unauthenticated_user_test.rb new file mode 100644 index 0000000..e5fafb8 --- /dev/null +++ b/users/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/users/test/unit/unauthorized_user_test.rb b/users/test/unit/unauthorized_user_test.rb deleted file mode 100644 index 5b96ae1..0000000 --- a/users/test/unit/unauthorized_user_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class UnauthorizedUserTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end -- cgit v1.2.3 From 88f8128d568daaaa122d55ac7e546a81ae60964a Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 7 Feb 2014 15:46:43 +0100 Subject: minor: more robust destruction of records in tests --- billing/test/integration/admin_customer_test.rb | 4 ++-- help/test/functional/tickets_controller_test.rb | 20 +++++--------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/billing/test/integration/admin_customer_test.rb b/billing/test/integration/admin_customer_test.rb index 58a7557..1b9953f 100644 --- a/billing/test/integration/admin_customer_test.rb +++ b/billing/test/integration/admin_customer_test.rb @@ -14,8 +14,8 @@ class AdminCustomerTest < ActionDispatch::IntegrationTest teardown do Warden.test_reset! - @user.destroy - @admin.destroy + @user.destroy if @user + @admin.destroy if @admin end test "check non customer as admin" do diff --git a/help/test/functional/tickets_controller_test.rb b/help/test/functional/tickets_controller_test.rb index 2530ba1..416fb73 100644 --- a/help/test/functional/tickets_controller_test.rb +++ b/help/test/functional/tickets_controller_test.rb @@ -2,6 +2,11 @@ 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 @@ -64,7 +69,6 @@ class TicketsControllerTest < ActionController::TestCase assert_equal 1, assigns(:ticket).comments.count assert_nil assigns(:ticket).comments.first.posted_by - assigns(:ticket).destroy # destroys without checking permission. is that okay? end @@ -87,7 +91,6 @@ class TicketsControllerTest < ActionController::TestCase 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 - assigns(:ticket).destroy end test "add comment to unauthenticated ticket" do @@ -101,7 +104,6 @@ class TicketsControllerTest < ActionController::TestCase 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) - assigns(:ticket).destroy end @@ -118,7 +120,6 @@ class TicketsControllerTest < ActionController::TestCase 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 - assigns(:ticket).destroy end @@ -153,12 +154,9 @@ class TicketsControllerTest < ActionController::TestCase 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 - - assigns(:ticket).destroy end test "tickets by admin" do - begin other_user = find_record :user ticket = FactoryGirl.create :ticket, :created_by => other_user.id @@ -173,9 +171,6 @@ class TicketsControllerTest < ActionController::TestCase assigns(:tickets).first.save get :index, {:admin_status => "all", :open_status => "open"} end - ensure - ticket.reload.destroy if ticket - end end @@ -188,7 +183,6 @@ class TicketsControllerTest < ActionController::TestCase assert assigns(:all_tickets).include?(testticket) get :index, {:user_id => user.id, :open_status => "open"} assert !assigns(:all_tickets).include?(testticket) - testticket.destroy end test "commenting on a ticket adds to tickets that are mine" do @@ -204,8 +198,6 @@ class TicketsControllerTest < ActionController::TestCase 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 - - assigns(:ticket).destroy end test "admin ticket ordering" do @@ -228,7 +220,6 @@ class TicketsControllerTest < ActionController::TestCase assert_not_equal first_tick, assigns(:all_tickets).first assert_not_equal last_tick, assigns(:all_tickets).last - tickets.each {|ticket| ticket.destroy} end test "tickets for regular user" do @@ -275,7 +266,6 @@ class TicketsControllerTest < ActionController::TestCase assert assigns(:all_tickets).include?(other_ticket) assert_equal assigns(:all_tickets).count, number_closed_tickets + number_open_tickets - assigns(:all_tickets).each {|t| t.destroy} end -- cgit v1.2.3 From 67f17e65b9e9e8ad2991b9c4002dba5203baa77f Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 8 Feb 2014 11:13:10 +0100 Subject: refactor tests to ease the testing of token only auth --- core/test/support/browser_integration_test.rb | 81 +++++++++++++++++ core/test/support/rack_test.rb | 13 +++ test/test_helper.rb | 73 ++------------- users/test/integration/api/account_flow_test.rb | 114 +++++------------------- users/test/integration/api/login_test.rb | 3 +- users/test/integration/api/pgp_key_test.rb | 35 ++++++++ users/test/integration/api/rack_test.rb | 9 -- users/test/integration/api/srp_test.rb | 83 +++++++++++++++++ users/test/support/integration_test_helper.rb | 12 --- 9 files changed, 240 insertions(+), 183 deletions(-) create mode 100644 core/test/support/browser_integration_test.rb create mode 100644 core/test/support/rack_test.rb create mode 100644 users/test/integration/api/pgp_key_test.rb delete mode 100644 users/test/integration/api/rack_test.rb create mode 100644 users/test/integration/api/srp_test.rb delete mode 100644 users/test/support/integration_test_helper.rb diff --git a/core/test/support/browser_integration_test.rb b/core/test/support/browser_integration_test.rb new file mode 100644 index 0000000..2885c3a --- /dev/null +++ b/core/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/core/test/support/rack_test.rb b/core/test/support/rack_test.rb new file mode 100644 index 0000000..0476cf7 --- /dev/null +++ b/core/test/support/rack_test.rb @@ -0,0 +1,13 @@ +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 + +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3fb2716..f63591f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,74 +10,6 @@ Dir["#{File.dirname(__FILE__)}/../*/test/support/**/*.rb"].each { |f| require f class ActiveSupport::TestCase # Add more helper methods to be used by all tests here... - def file_path(name) - File.join(Rails.root, 'test', 'files', name) - end - -end - -require 'capybara/poltergeist' - -CONFIG_RU = (Rails.root + 'config.ru').to_s -OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first - -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 - -class BrowserIntegrationTest < ActionDispatch::IntegrationTest - # Make the Capybara DSL available - include Capybara::DSL - include IntegrationTestHelper - - 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 - - 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 - protected def logfile_path @@ -87,4 +19,9 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest def screenshot_path Rails.root + 'tmp' + "#{self.class.name.underscore}.#{__name__}.png" end + + def file_path(name) + File.join(Rails.root, 'test', 'files', name) + end + end diff --git a/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb index edd0859..b56d07b 100644 --- a/users/test/integration/api/account_flow_test.rb +++ b/users/test/integration/api/account_flow_test.rb @@ -1,50 +1,10 @@ require 'test_helper' -require_relative 'rack_test' +require_relative 'srp_test' -class AccountFlowTest < RackTest +class AccountFlowTest < SrpTest setup do - @login = "integration_test_user" - Identity.find_by_address(@login + '@' + APP_CONFIG[:domain]).tap{|i| i.destroy if i} - User.find_by_login(@login).tap{|u| u.destroy if u} - @password = "srp, verify me!" - @srp = SRP::Client.new @login, :password => @password - @user_params = { - :login => @login, - :password_verifier => @srp.verifier.to_s(16), - :password_salt => @srp.salt.to_s(16) - } - post 'http://api.lvh.me:3000/1/users.json', :user => @user_params - @user = User.find_by_login(@login) - end - - teardown do - if @user.reload - @user.identity.destroy - @user.destroy - 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) + register_user end test "signup response" do @@ -53,25 +13,22 @@ class AccountFlowTest < RackTest end test "signup and login with srp via api" do - server_auth = @srp.authenticate(self) + authenticate assert last_response.successful? assert_nil server_auth["errors"] assert server_auth["M2"] end test "signup and wrong password login attempt" do - srp = SRP::Client.new @login, :password => "wrong password" - server_auth = srp.authenticate(self) + 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 "signup and wrong username login attempt" do - srp = SRP::Client.new "wrong_login", :password => @password - server_auth = nil assert_raises RECORD_NOT_FOUND do - server_auth = srp.authenticate(self) + authenticate login: "wrong login" end assert_json_error "base" => "Not a valid username/password combination" assert !last_response.successful? @@ -79,58 +36,31 @@ class AccountFlowTest < RackTest end test "update password via api" do - @srp.authenticate(self) - @password = "No! Verify me instead." - @srp = SRP::Client.new @login, :password => @password - @user_params = { - # :login => @login, - :password_verifier => @srp.verifier.to_s(16), - :password_salt => @srp.salt.to_s(16) - } - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', - :user => @user_params, - :format => :json - server_auth = @srp.authenticate(self) + 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 + assert last_response.successful? + assert_equal new_login, @user.reload.login + end + test "prevent changing login without changing password_verifier" do - server_auth = @srp.authenticate(self) + authenticate original_login = @user.login new_login = 'zaph' - User.find_by_login(new_login).try(:destroy) - Identity.by_address.key(new_login + '@' + APP_CONFIG[:domain]).each do |identity| - identity.destroy - end - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:login => new_login}, :format => :json + 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.login - end - - test "upload pgp key" do - server_auth = @srp.authenticate(self) - key = FactoryGirl.build :pgp_key - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => key}, :format => :json - assert_equal key, Identity.for(@user).keys[:pgp] + assert_equal original_login, @user.reload.login end - - # eventually probably want to remove most of this into a non-integration - # functional test - test "prevent uploading invalid key" do - server_auth = @srp.authenticate(self) - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => :blah}, :format => :json - assert_nil Identity.for(@user).keys[:pgp] - end - - test "prevent emptying public key" do - server_auth = @srp.authenticate(self) - key = FactoryGirl.build :pgp_key - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => key}, :format => :json - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => ""}, :format => :json - assert_equal key, Identity.for(@user).keys[:pgp] - end - end diff --git a/users/test/integration/api/login_test.rb b/users/test/integration/api/login_test.rb index fb761e5..a760d38 100644 --- a/users/test/integration/api/login_test.rb +++ b/users/test/integration/api/login_test.rb @@ -1,7 +1,6 @@ require 'test_helper' -require_relative 'rack_test' -class AccountFlowTest < RackTest +class LoginTest < RackTest setup do @login = "integration_test_user" diff --git a/users/test/integration/api/pgp_key_test.rb b/users/test/integration/api/pgp_key_test.rb new file mode 100644 index 0000000..4c7fb4c --- /dev/null +++ b/users/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/users/test/integration/api/rack_test.rb b/users/test/integration/api/rack_test.rb deleted file mode 100644 index 9a69f52..0000000 --- a/users/test/integration/api/rack_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -class RackTest < ActiveSupport::TestCase - include Rack::Test::Methods - include Warden::Test::Helpers - include LeapWebCore::AssertResponses - - def app - OUTER_APP - end -end diff --git a/users/test/integration/api/srp_test.rb b/users/test/integration/api/srp_test.rb new file mode 100644 index 0000000..b291269 --- /dev/null +++ b/users/test/integration/api/srp_test.rb @@ -0,0 +1,83 @@ +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: user_params(login: login, password: password), + format: :json + @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 => user_params(params), + :format => :json + end + + def authenticate(params = nil) + @server_auth = srp(params).authenticate(self) + 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 there is no srp magic needed just return the params + return params unless params.keys.include?(:password) + 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/support/integration_test_helper.rb b/users/test/support/integration_test_helper.rb deleted file mode 100644 index 51e47c6..0000000 --- a/users/test/support/integration_test_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module IntegrationTestHelper - 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 -end -- cgit v1.2.3 From 758b9a3c30a73fd985943fb7a887f0373be3a833 Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 8 Feb 2014 12:29:08 +0100 Subject: split up and expand account integration test --- core/lib/extensions/testing.rb | 2 + core/test/support/rack_test.rb | 24 +++++++++ users/test/integration/api/account_flow_test.rb | 66 ----------------------- users/test/integration/api/login_test.rb | 38 +++++++++++-- users/test/integration/api/signup_test.rb | 20 +++++++ users/test/integration/api/srp_test.rb | 5 ++ users/test/integration/api/update_account_test.rb | 44 +++++++++++++++ 7 files changed, 128 insertions(+), 71 deletions(-) delete mode 100644 users/test/integration/api/account_flow_test.rb create mode 100644 users/test/integration/api/signup_test.rb create mode 100644 users/test/integration/api/update_account_test.rb diff --git a/core/lib/extensions/testing.rb b/core/lib/extensions/testing.rb index aad7fc1..d9b6da8 100644 --- a/core/lib/extensions/testing.rb +++ b/core/lib/extensions/testing.rb @@ -22,6 +22,8 @@ module LeapWebCore end def assert_json_response(object) + assert_equal 'application/json', + get_response.content_type.split(';').first if object.is_a? Hash object.stringify_keys! if object.respond_to? :stringify_keys! assert_equal object, json_response diff --git a/core/test/support/rack_test.rb b/core/test/support/rack_test.rb index 0476cf7..2d8e5c4 100644 --- a/core/test/support/rack_test.rb +++ b/core/test/support/rack_test.rb @@ -10,4 +10,28 @@ class RackTest < ActiveSupport::TestCase OUTER_APP end + def assert_access_denied + assert_json_response('error' => I18n.t(:not_authorized)) + assert_response :unprocessable_entity + end + + # inspired by rails 4 + # -> actionpack/lib/action_dispatch/testing/assertions/response.rb + def assert_response(type, message = nil) + # 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/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb deleted file mode 100644 index b56d07b..0000000 --- a/users/test/integration/api/account_flow_test.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'test_helper' -require_relative 'srp_test' - -class AccountFlowTest < SrpTest - - setup do - register_user - end - - test "signup response" do - assert_json_response :login => @login, :ok => true - assert last_response.successful? - end - - test "signup and login with srp via api" do - authenticate - assert last_response.successful? - assert_nil server_auth["errors"] - assert server_auth["M2"] - end - - test "signup and 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 "signup and 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 "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 - 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/api/login_test.rb b/users/test/integration/api/login_test.rb index a760d38..82219d0 100644 --- a/users/test/integration/api/login_test.rb +++ b/users/test/integration/api/login_test.rb @@ -1,15 +1,43 @@ require 'test_helper' +require_relative 'srp_test' -class LoginTest < RackTest +class LoginTest < SrpTest setup do - @login = "integration_test_user" + register_user end - test "require json requests" do - put "http://api.lvh.me:3000/1/sessions/" + @login, - :client_auth => "This is not a valid login anyway" + 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 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 end diff --git a/users/test/integration/api/signup_test.rb b/users/test/integration/api/signup_test.rb new file mode 100644 index 0000000..236c547 --- /dev/null +++ b/users/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/users/test/integration/api/srp_test.rb b/users/test/integration/api/srp_test.rb index b291269..bb24f5f 100644 --- a/users/test/integration/api/srp_test.rb +++ b/users/test/integration/api/srp_test.rb @@ -52,6 +52,11 @@ class SrpTest < RackTest @server_auth = srp(params).authenticate(self) end + def logout + delete "http://api.lvh.me:3000/1/logout.json", + format: :json + end + def cleanup_user(login = nil) login ||= @user.login Identity.by_address.key(login + '@' + APP_CONFIG[:domain]).each do |identity| diff --git a/users/test/integration/api/update_account_test.rb b/users/test/integration/api/update_account_test.rb new file mode 100644 index 0000000..16c2357 --- /dev/null +++ b/users/test/integration/api/update_account_test.rb @@ -0,0 +1,44 @@ +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 "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 -- cgit v1.2.3 From cbd757cf151cd61bfdd5637d09f43e4831fec3bb Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 8 Feb 2014 16:15:46 +0100 Subject: require token when updating user via API --- users/app/controllers/v1/users_controller.rb | 2 +- users/test/integration/api/login_test.rb | 1 + users/test/integration/api/srp_test.rb | 29 +++++++++++++++++------ users/test/integration/api/update_account_test.rb | 7 ++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb index a16c6e9..8897d01 100644 --- a/users/app/controllers/v1/users_controller.rb +++ b/users/app/controllers/v1/users_controller.rb @@ -3,8 +3,8 @@ module V1 skip_before_filter :verify_authenticity_token before_filter :fetch_user, :only => [:update] - before_filter :require_login, :only => [:update, :index] before_filter :require_admin, :only => [:index] + before_filter :require_token, :only => [:update] respond_to :json diff --git a/users/test/integration/api/login_test.rb b/users/test/integration/api/login_test.rb index 82219d0..d56dfd1 100644 --- a/users/test/integration/api/login_test.rb +++ b/users/test/integration/api/login_test.rb @@ -14,6 +14,7 @@ class LoginTest < SrpTest 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"] diff --git a/users/test/integration/api/srp_test.rb b/users/test/integration/api/srp_test.rb index bb24f5f..fcda187 100644 --- a/users/test/integration/api/srp_test.rb +++ b/users/test/integration/api/srp_test.rb @@ -35,8 +35,7 @@ class SrpTest < RackTest def register_user(login = "integration_test_user", password = 'srp, verify me!') cleanup_user(login) post 'http://api.lvh.me:3000/1/users.json', - user: user_params(login: login, password: password), - format: :json + user_params(login: login, password: password) @user = User.find_by_login(login) @login = login @password = password @@ -44,14 +43,25 @@ class SrpTest < RackTest def update_user(params) put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', - :user => user_params(params), - :format => :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 delete "http://api.lvh.me:3000/1/logout.json", format: :json @@ -68,12 +78,17 @@ class SrpTest < RackTest end def user_params(params) - # if there is no srp magic needed just return the params - return params unless params.keys.include?(:password) + 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), + params.merge! :password_verifier => srp.verifier.to_s(16), :password_salt => @salt end diff --git a/users/test/integration/api/update_account_test.rb b/users/test/integration/api/update_account_test.rb index 16c2357..63429e7 100644 --- a/users/test/integration/api/update_account_test.rb +++ b/users/test/integration/api/update_account_test.rb @@ -12,6 +12,13 @@ class UpdateAccountTest < SrpTest 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." -- cgit v1.2.3 From c8fcd0d26c3ad5c1c3cfbaf6b57239f907925ed6 Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 8 Feb 2014 16:20:37 +0100 Subject: require token when logging out via API --- users/app/controllers/v1/sessions_controller.rb | 1 + users/test/integration/api/login_test.rb | 6 ++++++ users/test/integration/api/srp_test.rb | 5 +++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/users/app/controllers/v1/sessions_controller.rb b/users/app/controllers/v1/sessions_controller.rb index eb6c322..eae3a1e 100644 --- a/users/app/controllers/v1/sessions_controller.rb +++ b/users/app/controllers/v1/sessions_controller.rb @@ -2,6 +2,7 @@ module V1 class SessionsController < ApplicationController skip_before_filter :verify_authenticity_token + before_filter :require_token, only: :destroy def new @session = Session.new diff --git a/users/test/integration/api/login_test.rb b/users/test/integration/api/login_test.rb index d56dfd1..92d153f 100644 --- a/users/test/integration/api/login_test.rb +++ b/users/test/integration/api/login_test.rb @@ -41,4 +41,10 @@ class LoginTest < SrpTest 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/srp_test.rb b/users/test/integration/api/srp_test.rb index fcda187..946450e 100644 --- a/users/test/integration/api/srp_test.rb +++ b/users/test/integration/api/srp_test.rb @@ -62,9 +62,10 @@ class SrpTest < RackTest ActionController::HttpAuthentication::Token.encode_credentials(server_auth["token"]) end - def logout + def logout(params=nil, headers=nil) delete "http://api.lvh.me:3000/1/logout.json", - format: :json + params || {format: :json}, + headers || auth_headers end def cleanup_user(login = nil) -- cgit v1.2.3 From 3a478804aa48b08fbeded5144677744c427c112f Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 14:29:34 +0100 Subject: require token in messages controller --- users/app/controllers/v1/messages_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/app/controllers/v1/messages_controller.rb b/users/app/controllers/v1/messages_controller.rb index 1b994ca..90986e2 100644 --- a/users/app/controllers/v1/messages_controller.rb +++ b/users/app/controllers/v1/messages_controller.rb @@ -2,7 +2,7 @@ module V1 class MessagesController < ApplicationController skip_before_filter :verify_authenticity_token - before_filter :authorize + before_filter :require_token respond_to :json -- cgit v1.2.3 From b6c8279a39f933257be11fc29f5b7d59efff743f Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 14:34:17 +0100 Subject: require_token now checks for token and login --- users/app/controllers/controller_extension/token_authentication.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/users/app/controllers/controller_extension/token_authentication.rb b/users/app/controllers/controller_extension/token_authentication.rb index ee24f73..6e0a6ce 100644 --- a/users/app/controllers/controller_extension/token_authentication.rb +++ b/users/app/controllers/controller_extension/token_authentication.rb @@ -8,11 +8,11 @@ module ControllerExtension::TokenAuthentication end def token_authenticate - token.authenticate if token + @token_authenticated ||= token.authenticate if token end def require_token - access_denied unless token + access_denied unless token_authenticate end def logout -- cgit v1.2.3 From b4719619aabbe9ebf74563b62e1eb8e4fb248c21 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 15:01:11 +0100 Subject: ensure we are working on a string as the content type --- core/lib/extensions/testing.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/extensions/testing.rb b/core/lib/extensions/testing.rb index d9b6da8..8f7e73c 100644 --- a/core/lib/extensions/testing.rb +++ b/core/lib/extensions/testing.rb @@ -23,7 +23,7 @@ module LeapWebCore def assert_json_response(object) assert_equal 'application/json', - get_response.content_type.split(';').first + 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 -- cgit v1.2.3 From e3577906891f9ab0edd987544e3432f354bc1721 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 10 Feb 2014 19:41:02 +0100 Subject: fix unread_by? to check user_ids_to_show, remove TODO uniq! is probably not any faster than just checking include? --- users/app/models/message.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/users/app/models/message.rb b/users/app/models/message.rb index 7c05e06..424f094 100644 --- a/users/app/models/message.rb +++ b/users/app/models/message.rb @@ -17,7 +17,6 @@ class Message < CouchRest::Model::Base 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) - # TODO: is it quicker to call uniq! after adding rather than check if it is already included? end def read_by?(user) @@ -25,6 +24,6 @@ class Message < CouchRest::Model::Base end def unread_by?(user) - user_ids_to_shown.include?(user.id) + user_ids_to_show.include?(user.id) end end -- cgit v1.2.3 From ab820994950f8f43214bccd8dc4adf2cea40621c Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 27 Feb 2014 17:49:44 +0100 Subject: nagios test for logging into webapp --- test/nagios/webapp_login.py | 76 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 test/nagios/webapp_login.py diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py new file mode 100755 index 0000000..c046750 --- /dev/null +++ b/test/nagios/webapp_login.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# Test Authentication with the webapp API works. + +import requests +import json +import string +import random +import srp._pysrp as srp +import binascii +import yaml + + +safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) + +def read_config(): + stream = open("/etc/leap/hiera.yaml", 'r') + config = yaml.load(stream) + stream.close + user = config['webapp']['nagios_test_user'] + if ( 'username' not in user ): + fail('nagios test user lacks username') + if ( 'password' not in user ): + fail('nagios test user lacks password') + api = config['api'] + api['version'] = config['webapp']['api_version'] + return {'api': api, 'user': user} + +def run_tests(config): + user = config['user'] + api = config['api'] + usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) + try: + auth = parse(authenticate(api, usr)) + except requests.exceptions.ConnectionError: + fail('no connection to server') + exit(report(auth, usr)) + +# parse the server responses +def parse(response): + request = response.request + try: + return json.loads(response.text) + except ValueError: + return None + +def authenticate(api, usr): + api_url = 'https://' + api['domain'] + ':' + str(api['port']) + '/' + str(api['version']) + session = requests.session() + uname, A = usr.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + init = parse(session.post(api_url + '/sessions', data = params, verify=False)) + if ( 'errors' in init ): + fail('test user not found') + M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) + return session.put(api_url + '/sessions/' + uname, verify = False, + data = {'client_auth': binascii.hexlify(M)}) + +def report(auth, usr): + if ( 'errors' in auth ): + fail('srp password auth failed') + usr.verify_session( safe_unhexlify(auth["M2"]) ) + if usr.authenticated(): + print '0 webapp_login - OK - can login to webapp fine' + return 0 + print '1 webapp_login - WARNING - failed to verify webapp server' + return 1 + +def fail(reason): + print '2 webapp_login - CRITICAL - ' + reason + exit(2) + +run_tests(read_config()) -- cgit v1.2.3 From e4b42733d61887e2e7f01e9f159e963d50044465 Mon Sep 17 00:00:00 2001 From: Azul Date: Sun, 2 Mar 2014 08:25:24 +0100 Subject: autopep8 nagios webapp login test --- test/nagios/webapp_login.py | 109 ++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 50 deletions(-) mode change 100755 => 100644 test/nagios/webapp_login.py diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py old mode 100755 new mode 100644 index c046750..1239769 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -11,66 +11,75 @@ import binascii import yaml -safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x) +safe_unhexlify = lambda x: binascii.unhexlify(x) if ( + len(x) % 2 == 0) else binascii.unhexlify('0' + x) + def read_config(): - stream = open("/etc/leap/hiera.yaml", 'r') - config = yaml.load(stream) - stream.close - user = config['webapp']['nagios_test_user'] - if ( 'username' not in user ): - fail('nagios test user lacks username') - if ( 'password' not in user ): - fail('nagios test user lacks password') - api = config['api'] - api['version'] = config['webapp']['api_version'] - return {'api': api, 'user': user} + open("/etc/leap/hiera.yaml", 'r') as stream + config = yaml.load(stream) + user = config['webapp']['nagios_test_user'] + if ('username' not in user): + fail('nagios test user lacks username') + if ('password' not in user): + fail('nagios test user lacks password') + api = config['api'] + api['version'] = config['webapp']['api_version'] + return {'api': api, 'user': user} + def run_tests(config): - user = config['user'] - api = config['api'] - usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) - try: - auth = parse(authenticate(api, usr)) - except requests.exceptions.ConnectionError: - fail('no connection to server') - exit(report(auth, usr)) + user = config['user'] + api = config['api'] + usr = srp.User(user['username'], user['password'], srp.SHA256, srp.NG_1024) + try: + auth = parse(authenticate(api, usr)) + except requests.exceptions.ConnectionError: + fail('no connection to server') + exit(report(auth, usr)) # parse the server responses + + def parse(response): - request = response.request - try: - return json.loads(response.text) - except ValueError: - return None + request = response.request + try: + return json.loads(response.text) + except ValueError: + return None + def authenticate(api, usr): - api_url = 'https://' + api['domain'] + ':' + str(api['port']) + '/' + str(api['version']) - session = requests.session() - uname, A = usr.start_authentication() - params = { - 'login': uname, - 'A': binascii.hexlify(A) - } - init = parse(session.post(api_url + '/sessions', data = params, verify=False)) - if ( 'errors' in init ): - fail('test user not found') - M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) ) - return session.put(api_url + '/sessions/' + uname, verify = False, - data = {'client_auth': binascii.hexlify(M)}) - -def report(auth, usr): - if ( 'errors' in auth ): - fail('srp password auth failed') - usr.verify_session( safe_unhexlify(auth["M2"]) ) - if usr.authenticated(): - print '0 webapp_login - OK - can login to webapp fine' - return 0 - print '1 webapp_login - WARNING - failed to verify webapp server' - return 1 + api_url = 'https://' + api['domain'] + ':' + \ + str(api['port']) + '/' + str(api['version']) + session = requests.session() + uname, A = usr.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + init = parse( + session.post(api_url + '/sessions', data=params, verify=False)) + if ('errors' in init): + fail('test user not found') + M = usr.process_challenge( + safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) + return session.put(api_url + '/sessions/' + uname, verify=False, + data={'client_auth': binascii.hexlify(M)}) + + def report(auth, usr): + if ('errors' in auth): + fail('srp password auth failed') + usr.verify_session(safe_unhexlify(auth["M2"])) + if usr.authenticated(): + print '0 webapp_login - OK - can login to webapp fine' + return 0 + print '1 webapp_login - WARNING - failed to verify webapp server' + return 1 + def fail(reason): - print '2 webapp_login - CRITICAL - ' + reason - exit(2) + print '2 webapp_login - CRITICAL - ' + reason + exit(2) run_tests(read_config()) -- cgit v1.2.3 From 35b3387b99024a69402cc0d7b5d0dc7fed37a0fa Mon Sep 17 00:00:00 2001 From: Azul Date: Sun, 2 Mar 2014 08:38:10 +0100 Subject: including chiiphs comments --- test/nagios/webapp_login.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) mode change 100644 => 100755 test/nagios/webapp_login.py diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py old mode 100644 new mode 100755 index 1239769..afa3edf --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -16,12 +16,12 @@ safe_unhexlify = lambda x: binascii.unhexlify(x) if ( def read_config(): - open("/etc/leap/hiera.yaml", 'r') as stream - config = yaml.load(stream) + with open("/etc/leap/hiera.yaml", 'r') as stream: + config = yaml.load(stream) user = config['webapp']['nagios_test_user'] - if ('username' not in user): + if 'username' not in user: fail('nagios test user lacks username') - if ('password' not in user): + if 'password' not in user: fail('nagios test user lacks password') api = config['api'] api['version'] = config['webapp']['api_version'] @@ -50,8 +50,7 @@ def parse(response): def authenticate(api, usr): - api_url = 'https://' + api['domain'] + ':' + \ - str(api['port']) + '/' + str(api['version']) + api_url = "https://{domain}:{port}/{version}".format(**api) session = requests.session() uname, A = usr.start_authentication() params = { @@ -67,9 +66,10 @@ def authenticate(api, usr): return session.put(api_url + '/sessions/' + uname, verify=False, data={'client_auth': binascii.hexlify(M)}) - def report(auth, usr): - if ('errors' in auth): - fail('srp password auth failed') + +def report(auth, usr): + if ('errors' in auth): + fail('srp password auth failed') usr.verify_session(safe_unhexlify(auth["M2"])) if usr.authenticated(): print '0 webapp_login - OK - can login to webapp fine' -- cgit v1.2.3 From 453f5e91593c8fcef4a330016bfb0128ca37c204 Mon Sep 17 00:00:00 2001 From: db Date: Thu, 6 Mar 2014 10:53:54 -0300 Subject: Add script to check if soledad is working (#5239). --- test/nagios/soledad_sync.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ test/nagios/webapp_login.py | 3 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100755 test/nagios/soledad_sync.py diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py new file mode 100755 index 0000000..3f176b5 --- /dev/null +++ b/test/nagios/soledad_sync.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# Test Soledad sync +# +# This script performs a slightly modified U1DB sync to the Soledad server and +# returns whether that sync was succesful or not. + + +import tempfile +import requests +import os +import srp._pysrp as srp +import shutil +import u1db +from u1db.remote.http_target import HTTPSyncTarget +from webapp_login import read_config, parse, authenticate, fail + + +# monkey patch U1DB's HTTPSyncTarget to perform token based auth + +def set_token_credentials(self, uuid, token): + self._creds = {'token': (uuid, token)} + +def _sign_request(self, method, url_query, params): + uuid, token = self._creds['token'] + auth = '%s:%s' % (uuid, token) + return [('Authorization', 'Token %s' % auth.encode('base64')[:-1])] + +HTTPSyncTarget.set_token_credentials = set_token_credentials +HTTPSyncTarget._sign_request = _sign_request + + +# The following function could fetch all info needed to sync using soledad. +# Despite that, we won't use all that info because we are instead faking a +# Soledad sync by using U1DB slightly modified syncing capabilities. Part of +# the code is commented and left here for future reference, in case we decide +# to actually use the Soledad client in the future. + +def get_soledad_info(config, tempdir): + # get login and get user info + user = config['user'] + api = config['api'] + usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) + try: + auth = parse(authenticate(api, usr)) + except requests.exceptions.ConnectionError: + fail('no connection to server') + # get soledad server url + service_url = 'https://%s:%d/%d/config/soledad-service.json' % \ + (api['domain'], api['port'], api['version']) + soledad_hosts = requests.get(service_url).json['hosts'] + host = soledad_hosts.keys()[0] + server_url = 'https://%s:%d/user-%s' % \ + (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], + auth['id']) + # get provider ca certificate + #ca_cert = requests.get('https://127.0.0.1/ca.crt', verify=False).text + #cert_file = os.path.join(tempdir, 'ca.crt') + cert_file = None # not used for now + #with open(cert_file, 'w') as f: + # f.write(ca_cert) + return auth['id'], user['password'], server_url, cert_file, auth['token'] + + +def run_tests(): + tempdir = tempfile.mkdtemp() + uuid, password, server_url, cert_file, token = \ + get_soledad_info(read_config(), tempdir) + exc = None + try: + # in the future, we can replace the following by an actual Soledad + # client sync, if needed + db = u1db.open(os.path.join(tempdir, '%s.db' % uuid), True) + creds = {'token': {'uuid': uuid, 'token': token}} + db.sync(server_url, creds=creds, autocreate=False) + except Exception as e: + exc = e + shutil.rmtree(tempdir) + exit(report(exc)) + + +def report(exc): + if exc is None: + print '0 soledad_sync - OK - can sync soledad fine' + return 0 + if isinstance(exc, u1db.errors.U1DBError): + print '2 soledad_sync - CRITICAL - ' + exc.message + else: + print '2 soledad_sync - CRITICAL - ' + str(exc) + return 2 + + +if __name__ == '__main__': + run_tests() diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index afa3edf..1711238 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -82,4 +82,5 @@ def fail(reason): print '2 webapp_login - CRITICAL - ' + reason exit(2) -run_tests(read_config()) +if __name__ == '__main__': + run_tests(read_config()) -- cgit v1.2.3 From 9cffdf44ad4b398b27a5c9527ce2080ab6ca2b44 Mon Sep 17 00:00:00 2001 From: db Date: Wed, 2 Apr 2014 11:56:45 -0300 Subject: Fix soledad sync nagios plugin returned check_name (#5430). --- test/nagios/soledad_sync.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py index 3f176b5..94679b1 100755 --- a/test/nagios/soledad_sync.py +++ b/test/nagios/soledad_sync.py @@ -12,8 +12,10 @@ import os import srp._pysrp as srp import shutil import u1db +import webapp_login + + from u1db.remote.http_target import HTTPSyncTarget -from webapp_login import read_config, parse, authenticate, fail # monkey patch U1DB's HTTPSyncTarget to perform token based auth @@ -30,6 +32,14 @@ HTTPSyncTarget.set_token_credentials = set_token_credentials HTTPSyncTarget._sign_request = _sign_request +def fail(reason): + print '2 soledad_sync - CRITICAL - ' + reason + exit(2) + +# monkey patch webapp_login's fail function to report as soledad +webapp_login.fail = fail + + # The following function could fetch all info needed to sync using soledad. # Despite that, we won't use all that info because we are instead faking a # Soledad sync by using U1DB slightly modified syncing capabilities. Part of @@ -42,7 +52,7 @@ def get_soledad_info(config, tempdir): api = config['api'] usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) try: - auth = parse(authenticate(api, usr)) + auth = webapp_login.parse(webapp_login.authenticate(api, usr)) except requests.exceptions.ConnectionError: fail('no connection to server') # get soledad server url @@ -65,7 +75,7 @@ def get_soledad_info(config, tempdir): def run_tests(): tempdir = tempfile.mkdtemp() uuid, password, server_url, cert_file, token = \ - get_soledad_info(read_config(), tempdir) + get_soledad_info(webapp_login.read_config(), tempdir) exc = None try: # in the future, we can replace the following by an actual Soledad -- cgit v1.2.3 From 85aabe832eb3eec10a29054ef5575618686eef33 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 4 Apr 2014 11:48:24 +0200 Subject: 5382 - prevent crash when destroying tokens An expired token was removed (probably by automatic cleanup) while processing it. So the webapp crashed due to a couch 404. We're preventing that by rescueing from a 404 on Token.delete by default. --- users/app/models/token.rb | 8 ++++++++ users/test/unit/token_test.rb | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/users/app/models/token.rb b/users/app/models/token.rb index 001eb40..4856c31 100644 --- a/users/app/models/token.rb +++ b/users/app/models/token.rb @@ -40,6 +40,14 @@ class Token < CouchRest::Model::Base 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 diff --git a/users/test/unit/token_test.rb b/users/test/unit/token_test.rb index 6c9f209..a3c6cf6 100644 --- a/users/test/unit/token_test.rb +++ b/users/test/unit/token_test.rb @@ -78,6 +78,12 @@ class ClientCertificateTest < ActiveSupport::TestCase 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 -- cgit v1.2.3 From aeb5d8cf8dc6329906f14bf4595a229e002691c1 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 4 Apr 2014 15:40:22 +0200 Subject: redirect home when logged in visits /signup (#5446) --- users/app/controllers/controller_extension/authentication.rb | 7 +++++++ users/app/controllers/sessions_controller.rb | 3 ++- users/app/controllers/users_controller.rb | 1 + users/test/functional/users_controller_test.rb | 8 +++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb index e83d6b2..03d3989 100644 --- a/users/app/controllers/controller_extension/authentication.rb +++ b/users/app/controllers/controller_extension/authentication.rb @@ -19,6 +19,13 @@ module ControllerExtension::Authentication 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 diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb index 0195f30..8919a4d 100644 --- a/users/app/controllers/sessions_controller.rb +++ b/users/app/controllers/sessions_controller.rb @@ -1,7 +1,8 @@ class SessionsController < ApplicationController + before_filter :redirect_if_logged_in, :only => [:new] + def new - redirect_to home_url if logged_in? @session = Session.new if authentication_errors @errors = authentication_errors diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb index 6b32d49..c8e09b6 100644 --- a/users/app/controllers/users_controller.rb +++ b/users/app/controllers/users_controller.rb @@ -5,6 +5,7 @@ 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] diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb index 57ae94d..0713836 100644 --- a/users/test/functional/users_controller_test.rb +++ b/users/test/functional/users_controller_test.rb @@ -4,11 +4,17 @@ 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 -- cgit v1.2.3 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 From a14f66c2642ff43c2cc497b0597bfb17d19a7139 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 15 Apr 2014 09:47:07 +0200 Subject: refactor nagios tests, remove parse --- test/nagios/soledad_sync.py | 2 +- test/nagios/webapp_login.py | 19 +++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py index 94679b1..faf552a 100755 --- a/test/nagios/soledad_sync.py +++ b/test/nagios/soledad_sync.py @@ -52,7 +52,7 @@ def get_soledad_info(config, tempdir): api = config['api'] usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) try: - auth = webapp_login.parse(webapp_login.authenticate(api, usr)) + auth = webapp_login.authenticate(api, usr) except requests.exceptions.ConnectionError: fail('no connection to server') # get soledad server url diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index 1711238..86a4045 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -33,21 +33,11 @@ def run_tests(config): api = config['api'] usr = srp.User(user['username'], user['password'], srp.SHA256, srp.NG_1024) try: - auth = parse(authenticate(api, usr)) + auth = authenticate(api, usr) except requests.exceptions.ConnectionError: fail('no connection to server') exit(report(auth, usr)) -# parse the server responses - - -def parse(response): - request = response.request - try: - return json.loads(response.text) - except ValueError: - return None - def authenticate(api, usr): api_url = "https://{domain}:{port}/{version}".format(**api) @@ -57,14 +47,15 @@ def authenticate(api, usr): 'login': uname, 'A': binascii.hexlify(A) } - init = parse( - session.post(api_url + '/sessions', data=params, verify=False)) + response = session.post(api_url + '/sessions', data=params, verify=False) + init = response.json() if ('errors' in init): fail('test user not found') M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) - return session.put(api_url + '/sessions/' + uname, verify=False, + response = session.put(api_url + '/sessions/' + uname, verify=False, data={'client_auth': binascii.hexlify(M)}) + return response.json() def report(auth, usr): -- cgit v1.2.3 From 44d2d031555c889b94e9738cb45740b16a4071ce Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 15 Apr 2014 12:51:06 +0200 Subject: refactor reporting in webapp login nagios test --- test/nagios/report.py | 19 +++++++++++++++++++ test/nagios/soledad_sync.py | 2 +- test/nagios/webapp_login.py | 35 ++++++++++++----------------------- 3 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 test/nagios/report.py diff --git a/test/nagios/report.py b/test/nagios/report.py new file mode 100644 index 0000000..d2720a5 --- /dev/null +++ b/test/nagios/report.py @@ -0,0 +1,19 @@ +system = 'undefined' + +def report(code, message): + codes = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} + print "%d %s - %s - %s" % \ + (code, system, codes[code], message) + exit(code) + +def fail(message): + report(2, message) + +def warn(message): + report(1, message) + +def ok(message): + report(0, message) + +def unknown(message): + report(3, message) diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py index faf552a..a3c2d5a 100755 --- a/test/nagios/soledad_sync.py +++ b/test/nagios/soledad_sync.py @@ -58,7 +58,7 @@ def get_soledad_info(config, tempdir): # get soledad server url service_url = 'https://%s:%d/%d/config/soledad-service.json' % \ (api['domain'], api['port'], api['version']) - soledad_hosts = requests.get(service_url).json['hosts'] + soledad_hosts = requests.get(service_url).json()['hosts'] host = soledad_hosts.keys()[0] server_url = 'https://%s:%d/user-%s' % \ (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index 86a4045..a7e3473 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -9,20 +9,21 @@ import random import srp._pysrp as srp import binascii import yaml - +import report safe_unhexlify = lambda x: binascii.unhexlify(x) if ( len(x) % 2 == 0) else binascii.unhexlify('0' + x) +report.system = 'webapp login' def read_config(): with open("/etc/leap/hiera.yaml", 'r') as stream: config = yaml.load(stream) user = config['webapp']['nagios_test_user'] if 'username' not in user: - fail('nagios test user lacks username') + report.fail('nagios test user lacks username') if 'password' not in user: - fail('nagios test user lacks password') + report.fail('nagios test user lacks password') api = config['api'] api['version'] = config['webapp']['api_version'] return {'api': api, 'user': user} @@ -35,9 +36,13 @@ def run_tests(config): try: auth = authenticate(api, usr) except requests.exceptions.ConnectionError: - fail('no connection to server') - exit(report(auth, usr)) - + report.fail('no connection to server') + if ('errors' in auth): + report.fail('srp password auth failed') + usr.verify_session(safe_unhexlify(auth["M2"])) + if usr.authenticated(): + report.ok('can login to webapp fine') + report.warn('failed to verify webapp server') def authenticate(api, usr): api_url = "https://{domain}:{port}/{version}".format(**api) @@ -50,28 +55,12 @@ def authenticate(api, usr): response = session.post(api_url + '/sessions', data=params, verify=False) init = response.json() if ('errors' in init): - fail('test user not found') + report.fail('test user not found') M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) response = session.put(api_url + '/sessions/' + uname, verify=False, data={'client_auth': binascii.hexlify(M)}) return response.json() - -def report(auth, usr): - if ('errors' in auth): - fail('srp password auth failed') - usr.verify_session(safe_unhexlify(auth["M2"])) - if usr.authenticated(): - print '0 webapp_login - OK - can login to webapp fine' - return 0 - print '1 webapp_login - WARNING - failed to verify webapp server' - return 1 - - -def fail(reason): - print '2 webapp_login - CRITICAL - ' + reason - exit(2) - if __name__ == '__main__': run_tests(read_config()) -- cgit v1.2.3 From be2971c2e615cc8a808822317d049e99f5183bdc Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 15 Apr 2014 17:17:36 +0200 Subject: refactor: move nagios specifs to nagios_test nagios_test.run takes a function and executes it. If it returns nothing or 0 and OK nagios message is printed. If it returns sth. else this will be printed a a warning If it raises an exception that will result in a CRITICAL report. This way we can keep the nagios things outside the test cases and just write simple functions that either return 0, a warnign or raise a meaningful exception --- test/nagios/nagios_report.py | 24 +++++++++++ test/nagios/nagios_test.py | 49 ++++++++++++++++++++++ test/nagios/report.py | 19 --------- test/nagios/soledad_sync.py | 96 +++++++++++++++++--------------------------- test/nagios/webapp_login.py | 28 ++++++------- 5 files changed, 121 insertions(+), 95 deletions(-) create mode 100644 test/nagios/nagios_report.py create mode 100644 test/nagios/nagios_test.py delete mode 100644 test/nagios/report.py diff --git a/test/nagios/nagios_report.py b/test/nagios/nagios_report.py new file mode 100644 index 0000000..13cd551 --- /dev/null +++ b/test/nagios/nagios_report.py @@ -0,0 +1,24 @@ +def functions_for_system(under_test): + """ + returns a set of functions to use for nagios reporting: + >>> ok, warn, critical, unknown = functions_for_system("tested system") + + each of them will print a nagios line with its argument and + return the exit code: + >>> warn("that looks strange") + 1 tested system - WARNING - that looks strange + 1 + """ + def report_function(code): + return lambda message : report(under_test, code, message) + return map(report_function, [0,1,2,3]) + +def report(system, code, message): + codes = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} + print "%d %s - %s - %s" % \ + (code, system, codes[code], message) + return code + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/test/nagios/nagios_test.py b/test/nagios/nagios_test.py new file mode 100644 index 0000000..3eb8d55 --- /dev/null +++ b/test/nagios/nagios_test.py @@ -0,0 +1,49 @@ +import __main__ as main +import os +import sys +import nagios_report + +def run(test): + """ + run takes a function and tries it out. + If it returns nothing or 0 everything is fine and run prints an OK message + with the function name. + >>> def this_works_fine(): return + >>> run(this_works_fine) + 0 nagios_test.py - OK - this_works_fine + 0 + >>> def this_also_works_fine(): return 0 + >>> run(this_also_works_fine) + 0 nagios_test.py - OK - this_also_works_fine + 0 + + If the function returns something else it will be printed as a warning. + >>> run(lambda : "this is a warning") + 1 nagios_test.py - WARNING - this is a warning + 1 + + Errors raised will result in a CRITICAL nagios string. + >>> def failure(): raise Exception("something went wrong") + >>> run(failure) + 2 nagios_test.py - CRITICAL - something went wrong + 2 + """ + try: + name = os.path.basename(main.__file__) + except AttributeError: + name = sys.argv[0] + ok, warn, fail, unknown = nagios_report.functions_for_system(name) + try: + warning = test() + if warning and warning != 0: + code = warn(warning) + else: + code = ok(test.__name__) + except Exception as exc: + code = fail(exc.message or str(exc)) + return code + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/test/nagios/report.py b/test/nagios/report.py deleted file mode 100644 index d2720a5..0000000 --- a/test/nagios/report.py +++ /dev/null @@ -1,19 +0,0 @@ -system = 'undefined' - -def report(code, message): - codes = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} - print "%d %s - %s - %s" % \ - (code, system, codes[code], message) - exit(code) - -def fail(message): - report(2, message) - -def warn(message): - report(1, message) - -def ok(message): - report(0, message) - -def unknown(message): - report(3, message) diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py index a3c2d5a..9f51fd1 100755 --- a/test/nagios/soledad_sync.py +++ b/test/nagios/soledad_sync.py @@ -32,14 +32,6 @@ HTTPSyncTarget.set_token_credentials = set_token_credentials HTTPSyncTarget._sign_request = _sign_request -def fail(reason): - print '2 soledad_sync - CRITICAL - ' + reason - exit(2) - -# monkey patch webapp_login's fail function to report as soledad -webapp_login.fail = fail - - # The following function could fetch all info needed to sync using soledad. # Despite that, we won't use all that info because we are instead faking a # Soledad sync by using U1DB slightly modified syncing capabilities. Part of @@ -47,58 +39,42 @@ webapp_login.fail = fail # to actually use the Soledad client in the future. def get_soledad_info(config, tempdir): - # get login and get user info - user = config['user'] - api = config['api'] - usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) - try: + # get login and get user info + user = config['user'] + api = config['api'] + usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) auth = webapp_login.authenticate(api, usr) - except requests.exceptions.ConnectionError: - fail('no connection to server') - # get soledad server url - service_url = 'https://%s:%d/%d/config/soledad-service.json' % \ - (api['domain'], api['port'], api['version']) - soledad_hosts = requests.get(service_url).json()['hosts'] - host = soledad_hosts.keys()[0] - server_url = 'https://%s:%d/user-%s' % \ - (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], - auth['id']) - # get provider ca certificate - #ca_cert = requests.get('https://127.0.0.1/ca.crt', verify=False).text - #cert_file = os.path.join(tempdir, 'ca.crt') - cert_file = None # not used for now - #with open(cert_file, 'w') as f: - # f.write(ca_cert) - return auth['id'], user['password'], server_url, cert_file, auth['token'] - - -def run_tests(): - tempdir = tempfile.mkdtemp() - uuid, password, server_url, cert_file, token = \ - get_soledad_info(webapp_login.read_config(), tempdir) - exc = None - try: - # in the future, we can replace the following by an actual Soledad - # client sync, if needed - db = u1db.open(os.path.join(tempdir, '%s.db' % uuid), True) - creds = {'token': {'uuid': uuid, 'token': token}} - db.sync(server_url, creds=creds, autocreate=False) - except Exception as e: - exc = e - shutil.rmtree(tempdir) - exit(report(exc)) - - -def report(exc): - if exc is None: - print '0 soledad_sync - OK - can sync soledad fine' - return 0 - if isinstance(exc, u1db.errors.U1DBError): - print '2 soledad_sync - CRITICAL - ' + exc.message - else: - print '2 soledad_sync - CRITICAL - ' + str(exc) - return 2 - + # get soledad server url + service_url = 'https://%s:%d/%d/config/soledad-service.json' % \ + (api['domain'], api['port'], api['version']) + soledad_hosts = requests.get(service_url).json()['hosts'] + host = soledad_hosts.keys()[0] + server_url = 'https://%s:%d/user-%s' % \ + (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], + auth['id']) + # get provider ca certificate + #ca_cert = requests.get('https://127.0.0.1/ca.crt', verify=False).text + #cert_file = os.path.join(tempdir, 'ca.crt') + cert_file = None # not used for now + #with open(cert_file, 'w') as f: + # f.write(ca_cert) + return auth['id'], user['password'], server_url, cert_file, auth['token'] + + +def can_sync_soledad_fine(): + tempdir = tempfile.mkdtemp() + try: + uuid, password, server_url, cert_file, token = \ + get_soledad_info(webapp_login.read_config(), tempdir) + # in the future, we can replace the following by an actual Soledad + # client sync, if needed + db = u1db.open(os.path.join(tempdir, '%s.db' % uuid), True) + creds = {'token': {'uuid': uuid, 'token': token}} + db.sync(server_url, creds=creds, autocreate=False) + finally: + shutil.rmtree(tempdir) if __name__ == '__main__': - run_tests() + import nagios_test + exit_code = nagios_test.run(can_sync_soledad_fine) + exit(exit_code) diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index a7e3473..6d06438 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -9,40 +9,34 @@ import random import srp._pysrp as srp import binascii import yaml -import report safe_unhexlify = lambda x: binascii.unhexlify(x) if ( len(x) % 2 == 0) else binascii.unhexlify('0' + x) -report.system = 'webapp login' - def read_config(): with open("/etc/leap/hiera.yaml", 'r') as stream: config = yaml.load(stream) user = config['webapp']['nagios_test_user'] if 'username' not in user: - report.fail('nagios test user lacks username') + raise Exception('nagios test user lacks username') if 'password' not in user: - report.fail('nagios test user lacks password') + raise Exception('nagios test user lacks password') api = config['api'] api['version'] = config['webapp']['api_version'] return {'api': api, 'user': user} -def run_tests(config): +def login_successfully(config=None): + config = config or read_config() user = config['user'] api = config['api'] usr = srp.User(user['username'], user['password'], srp.SHA256, srp.NG_1024) - try: - auth = authenticate(api, usr) - except requests.exceptions.ConnectionError: - report.fail('no connection to server') + auth = authenticate(api, usr) if ('errors' in auth): - report.fail('srp password auth failed') + raise Exception('srp password auth failed') usr.verify_session(safe_unhexlify(auth["M2"])) - if usr.authenticated(): - report.ok('can login to webapp fine') - report.warn('failed to verify webapp server') + if not usr.authenticated(): + return 'failed to verify webapp server' def authenticate(api, usr): api_url = "https://{domain}:{port}/{version}".format(**api) @@ -55,7 +49,7 @@ def authenticate(api, usr): response = session.post(api_url + '/sessions', data=params, verify=False) init = response.json() if ('errors' in init): - report.fail('test user not found') + raise Exception('test user not found') M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) response = session.put(api_url + '/sessions/' + uname, verify=False, @@ -63,4 +57,6 @@ def authenticate(api, usr): return response.json() if __name__ == '__main__': - run_tests(read_config()) + import nagios_test + exit_code = nagios_test.run(login_successfully) + exit(exit_code) -- cgit v1.2.3 From 73bdb9a9ea8932e9a14f996391d348690da1a63c Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 10:13:28 +0200 Subject: nagios test: refactor webapp_login with classes --- test/nagios/support/nagios_report.py | 24 +++++++ test/nagios/support/nagios_test.py | 49 ++++++++++++++ test/nagios/webapp_login.py | 127 +++++++++++++++++++++++------------ 3 files changed, 158 insertions(+), 42 deletions(-) create mode 100644 test/nagios/support/nagios_report.py create mode 100644 test/nagios/support/nagios_test.py diff --git a/test/nagios/support/nagios_report.py b/test/nagios/support/nagios_report.py new file mode 100644 index 0000000..13cd551 --- /dev/null +++ b/test/nagios/support/nagios_report.py @@ -0,0 +1,24 @@ +def functions_for_system(under_test): + """ + returns a set of functions to use for nagios reporting: + >>> ok, warn, critical, unknown = functions_for_system("tested system") + + each of them will print a nagios line with its argument and + return the exit code: + >>> warn("that looks strange") + 1 tested system - WARNING - that looks strange + 1 + """ + def report_function(code): + return lambda message : report(under_test, code, message) + return map(report_function, [0,1,2,3]) + +def report(system, code, message): + codes = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} + print "%d %s - %s - %s" % \ + (code, system, codes[code], message) + return code + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/test/nagios/support/nagios_test.py b/test/nagios/support/nagios_test.py new file mode 100644 index 0000000..3eb8d55 --- /dev/null +++ b/test/nagios/support/nagios_test.py @@ -0,0 +1,49 @@ +import __main__ as main +import os +import sys +import nagios_report + +def run(test): + """ + run takes a function and tries it out. + If it returns nothing or 0 everything is fine and run prints an OK message + with the function name. + >>> def this_works_fine(): return + >>> run(this_works_fine) + 0 nagios_test.py - OK - this_works_fine + 0 + >>> def this_also_works_fine(): return 0 + >>> run(this_also_works_fine) + 0 nagios_test.py - OK - this_also_works_fine + 0 + + If the function returns something else it will be printed as a warning. + >>> run(lambda : "this is a warning") + 1 nagios_test.py - WARNING - this is a warning + 1 + + Errors raised will result in a CRITICAL nagios string. + >>> def failure(): raise Exception("something went wrong") + >>> run(failure) + 2 nagios_test.py - CRITICAL - something went wrong + 2 + """ + try: + name = os.path.basename(main.__file__) + except AttributeError: + name = sys.argv[0] + ok, warn, fail, unknown = nagios_report.functions_for_system(name) + try: + warning = test() + if warning and warning != 0: + code = warn(warning) + else: + code = ok(test.__name__) + except Exception as exc: + code = fail(exc.message or str(exc)) + return code + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index 6d06438..7e2efd7 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -13,48 +13,91 @@ import yaml safe_unhexlify = lambda x: binascii.unhexlify(x) if ( len(x) % 2 == 0) else binascii.unhexlify('0' + x) -def read_config(): - with open("/etc/leap/hiera.yaml", 'r') as stream: - config = yaml.load(stream) - user = config['webapp']['nagios_test_user'] - if 'username' not in user: - raise Exception('nagios test user lacks username') - if 'password' not in user: - raise Exception('nagios test user lacks password') - api = config['api'] - api['version'] = config['webapp']['api_version'] - return {'api': api, 'user': user} - - -def login_successfully(config=None): - config = config or read_config() - user = config['user'] - api = config['api'] - usr = srp.User(user['username'], user['password'], srp.SHA256, srp.NG_1024) - auth = authenticate(api, usr) - if ('errors' in auth): - raise Exception('srp password auth failed') - usr.verify_session(safe_unhexlify(auth["M2"])) - if not usr.authenticated(): - return 'failed to verify webapp server' - -def authenticate(api, usr): - api_url = "https://{domain}:{port}/{version}".format(**api) - session = requests.session() - uname, A = usr.start_authentication() - params = { - 'login': uname, - 'A': binascii.hexlify(A) - } - response = session.post(api_url + '/sessions', data=params, verify=False) - init = response.json() - if ('errors' in init): - raise Exception('test user not found') - M = usr.process_challenge( - safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) - response = session.put(api_url + '/sessions/' + uname, verify=False, - data={'client_auth': binascii.hexlify(M)}) - return response.json() +class Config(): + def __init__(self, filename="/etc/leap/hiera.yaml"): + with open("/etc/leap/hiera.yaml", 'r') as stream: + config = yaml.load(stream) + self.user = config['webapp']['nagios_test_user'] + if 'username' not in self.user: + raise Exception('nagios test user lacks username') + if 'password' not in self.user: + raise Exception('nagios test user lacks password') + self.api = config['api'] + self.api['version'] = config['webapp']['api_version'] + +class Api(): + def __init__(self, config, verify=True): + self.config = config.api + self.session = requests.session() + self.verify = verify + + def api_url(self, path): + return self.api_root() + path + + def api_root(self): + return "https://{domain}:{port}/{version}/".format(**self.config) + + def get(self, path, **args): + response = self.session.get(self.api_url(path), + verify=self.verify, + **args) + return response.json() + + def post(self, path, **args): + response = self.session.post(self.api_url(path), + verify=self.verify, + **args) + return response.json() + + def put(self, path, **args): + response = self.session.put(self.api_url(path), + verify=self.verify, + **args) + return response.json() + +class User(): + def __init__(self, config): + self.config = config.user + self.srp_user = srp.User(self.config['username'], self.config['password'], srp.SHA256, srp.NG_1024) + + def login(self, api): + init=self.init_authentication(api) + if ('errors' in init): + raise Exception('test user not found') + auth=self.authenticate(api, init) + if ('errors' in auth): + raise Exception('srp password auth failed') + self.verify_server(auth) + if not self.is_authenticated(): + raise Exception('user is not authenticated') + + def init_authentication(self, api): + uname, A = self.srp_user.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + return api.post('sessions', data=params) + + def authenticate(self, api, init): + M = self.srp_user.process_challenge( + safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) + auth = api.put('sessions/' + self.config["username"], + data={'client_auth': binascii.hexlify(M)}) + return auth + + def verify_server(self, auth): + self.srp_user.verify_session(safe_unhexlify(auth["M2"])) + + def is_authenticated(self): + return self.srp_user.authenticated() + + +def login_successfully(): + config = Config() + user = User(config) + api = Api(config, verify=False) + user.login(api) if __name__ == '__main__': import nagios_test -- cgit v1.2.3 From 0f0057e65c6bfcb98ce53e1a48aa1460d3a6716a Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 11:14:55 +0200 Subject: move support classes into their own package now the webapp_login test looks nice and clean. soledad next. --- test/nagios/nagios_report.py | 24 ----------- test/nagios/nagios_test.py | 49 ---------------------- test/nagios/support/__init__.py | 0 test/nagios/support/api.py | 33 +++++++++++++++ test/nagios/support/config.py | 14 +++++++ test/nagios/support/user.py | 43 +++++++++++++++++++ test/nagios/webapp_login.py | 93 ++--------------------------------------- 7 files changed, 94 insertions(+), 162 deletions(-) delete mode 100644 test/nagios/nagios_report.py delete mode 100644 test/nagios/nagios_test.py create mode 100644 test/nagios/support/__init__.py create mode 100644 test/nagios/support/api.py create mode 100644 test/nagios/support/config.py create mode 100644 test/nagios/support/user.py diff --git a/test/nagios/nagios_report.py b/test/nagios/nagios_report.py deleted file mode 100644 index 13cd551..0000000 --- a/test/nagios/nagios_report.py +++ /dev/null @@ -1,24 +0,0 @@ -def functions_for_system(under_test): - """ - returns a set of functions to use for nagios reporting: - >>> ok, warn, critical, unknown = functions_for_system("tested system") - - each of them will print a nagios line with its argument and - return the exit code: - >>> warn("that looks strange") - 1 tested system - WARNING - that looks strange - 1 - """ - def report_function(code): - return lambda message : report(under_test, code, message) - return map(report_function, [0,1,2,3]) - -def report(system, code, message): - codes = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} - print "%d %s - %s - %s" % \ - (code, system, codes[code], message) - return code - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/test/nagios/nagios_test.py b/test/nagios/nagios_test.py deleted file mode 100644 index 3eb8d55..0000000 --- a/test/nagios/nagios_test.py +++ /dev/null @@ -1,49 +0,0 @@ -import __main__ as main -import os -import sys -import nagios_report - -def run(test): - """ - run takes a function and tries it out. - If it returns nothing or 0 everything is fine and run prints an OK message - with the function name. - >>> def this_works_fine(): return - >>> run(this_works_fine) - 0 nagios_test.py - OK - this_works_fine - 0 - >>> def this_also_works_fine(): return 0 - >>> run(this_also_works_fine) - 0 nagios_test.py - OK - this_also_works_fine - 0 - - If the function returns something else it will be printed as a warning. - >>> run(lambda : "this is a warning") - 1 nagios_test.py - WARNING - this is a warning - 1 - - Errors raised will result in a CRITICAL nagios string. - >>> def failure(): raise Exception("something went wrong") - >>> run(failure) - 2 nagios_test.py - CRITICAL - something went wrong - 2 - """ - try: - name = os.path.basename(main.__file__) - except AttributeError: - name = sys.argv[0] - ok, warn, fail, unknown = nagios_report.functions_for_system(name) - try: - warning = test() - if warning and warning != 0: - code = warn(warning) - else: - code = ok(test.__name__) - except Exception as exc: - code = fail(exc.message or str(exc)) - return code - - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/test/nagios/support/__init__.py b/test/nagios/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/nagios/support/api.py b/test/nagios/support/api.py new file mode 100644 index 0000000..3b6a90f --- /dev/null +++ b/test/nagios/support/api.py @@ -0,0 +1,33 @@ +import requests +import json + +class Api(): + def __init__(self, config, verify=True): + self.config = config.api + self.session = requests.session() + self.verify = verify + + def api_url(self, path): + return self.api_root() + path + + def api_root(self): + return "https://{domain}:{port}/{version}/".format(**self.config) + + def get(self, path, **args): + response = self.session.get(self.api_url(path), + verify=self.verify, + **args) + return response.json() + + def post(self, path, **args): + response = self.session.post(self.api_url(path), + verify=self.verify, + **args) + return response.json() + + def put(self, path, **args): + response = self.session.put(self.api_url(path), + verify=self.verify, + **args) + return response.json() + diff --git a/test/nagios/support/config.py b/test/nagios/support/config.py new file mode 100644 index 0000000..afb4464 --- /dev/null +++ b/test/nagios/support/config.py @@ -0,0 +1,14 @@ +import yaml + +class Config(): + def __init__(self, filename="/etc/leap/hiera.yaml"): + with open("/etc/leap/hiera.yaml", 'r') as stream: + config = yaml.load(stream) + self.user = config['webapp']['nagios_test_user'] + if 'username' not in self.user: + raise Exception('nagios test user lacks username') + if 'password' not in self.user: + raise Exception('nagios test user lacks password') + self.api = config['api'] + self.api['version'] = config['webapp']['api_version'] + diff --git a/test/nagios/support/user.py b/test/nagios/support/user.py new file mode 100644 index 0000000..8e49c4b --- /dev/null +++ b/test/nagios/support/user.py @@ -0,0 +1,43 @@ +import srp._pysrp as srp +import binascii + +safe_unhexlify = lambda x: binascii.unhexlify(x) if ( + len(x) % 2 == 0) else binascii.unhexlify('0' + x) + +class User(): + def __init__(self, config): + self.config = config.user + self.srp_user = srp.User(self.config['username'], self.config['password'], srp.SHA256, srp.NG_1024) + + def login(self, api): + init=self.init_authentication(api) + if ('errors' in init): + raise Exception('test user not found') + auth=self.authenticate(api, init) + if ('errors' in auth): + raise Exception('srp password auth failed') + self.verify_server(auth) + if not self.is_authenticated(): + raise Exception('user is not authenticated') + + def init_authentication(self, api): + uname, A = self.srp_user.start_authentication() + params = { + 'login': uname, + 'A': binascii.hexlify(A) + } + return api.post('sessions', data=params) + + def authenticate(self, api, init): + M = self.srp_user.process_challenge( + safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) + auth = api.put('sessions/' + self.config["username"], + data={'client_auth': binascii.hexlify(M)}) + return auth + + def verify_server(self, auth): + self.srp_user.verify_session(safe_unhexlify(auth["M2"])) + + def is_authenticated(self): + return self.srp_user.authenticated() + diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index 7e2efd7..4e78836 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -2,96 +2,11 @@ # Test Authentication with the webapp API works. -import requests -import json import string import random -import srp._pysrp as srp -import binascii -import yaml - -safe_unhexlify = lambda x: binascii.unhexlify(x) if ( - len(x) % 2 == 0) else binascii.unhexlify('0' + x) - -class Config(): - def __init__(self, filename="/etc/leap/hiera.yaml"): - with open("/etc/leap/hiera.yaml", 'r') as stream: - config = yaml.load(stream) - self.user = config['webapp']['nagios_test_user'] - if 'username' not in self.user: - raise Exception('nagios test user lacks username') - if 'password' not in self.user: - raise Exception('nagios test user lacks password') - self.api = config['api'] - self.api['version'] = config['webapp']['api_version'] - -class Api(): - def __init__(self, config, verify=True): - self.config = config.api - self.session = requests.session() - self.verify = verify - - def api_url(self, path): - return self.api_root() + path - - def api_root(self): - return "https://{domain}:{port}/{version}/".format(**self.config) - - def get(self, path, **args): - response = self.session.get(self.api_url(path), - verify=self.verify, - **args) - return response.json() - - def post(self, path, **args): - response = self.session.post(self.api_url(path), - verify=self.verify, - **args) - return response.json() - - def put(self, path, **args): - response = self.session.put(self.api_url(path), - verify=self.verify, - **args) - return response.json() - -class User(): - def __init__(self, config): - self.config = config.user - self.srp_user = srp.User(self.config['username'], self.config['password'], srp.SHA256, srp.NG_1024) - - def login(self, api): - init=self.init_authentication(api) - if ('errors' in init): - raise Exception('test user not found') - auth=self.authenticate(api, init) - if ('errors' in auth): - raise Exception('srp password auth failed') - self.verify_server(auth) - if not self.is_authenticated(): - raise Exception('user is not authenticated') - - def init_authentication(self, api): - uname, A = self.srp_user.start_authentication() - params = { - 'login': uname, - 'A': binascii.hexlify(A) - } - return api.post('sessions', data=params) - - def authenticate(self, api, init): - M = self.srp_user.process_challenge( - safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) - auth = api.put('sessions/' + self.config["username"], - data={'client_auth': binascii.hexlify(M)}) - return auth - - def verify_server(self, auth): - self.srp_user.verify_session(safe_unhexlify(auth["M2"])) - - def is_authenticated(self): - return self.srp_user.authenticated() - +from support.api import Api +from support.config import Config +from support.user import User def login_successfully(): config = Config() @@ -100,6 +15,6 @@ def login_successfully(): user.login(api) if __name__ == '__main__': - import nagios_test + from support import nagios_test exit_code = nagios_test.run(login_successfully) exit(exit_code) -- cgit v1.2.3 From bfda2c55bd4824d94d384a39960c91b6e1f8d14b Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 11:16:00 +0200 Subject: remove unneeded imports --- test/nagios/webapp_login.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/nagios/webapp_login.py b/test/nagios/webapp_login.py index 4e78836..7741325 100755 --- a/test/nagios/webapp_login.py +++ b/test/nagios/webapp_login.py @@ -2,8 +2,6 @@ # Test Authentication with the webapp API works. -import string -import random from support.api import Api from support.config import Config from support.user import User -- cgit v1.2.3 From 2822aff5c22fb79490fd82426d81e68813417543 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 11:16:13 +0200 Subject: gitignore *.pyc --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ae80164..d2c630c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ bin */Gemfile.lock test/dummy/log/* test/dummy/tmp/* +*.pyc # ignore all deploy specific configuration config/couchdb.yml -- cgit v1.2.3 From 8907100f3ffe99a2a9110c90418c9e5844b4ab03 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 11:26:13 +0200 Subject: nagios test: use support classes in soledad sync --- test/nagios/soledad_sync.py | 25 +++++++++++-------------- test/nagios/support/user.py | 1 + 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/test/nagios/soledad_sync.py b/test/nagios/soledad_sync.py index 9f51fd1..617dd3a 100755 --- a/test/nagios/soledad_sync.py +++ b/test/nagios/soledad_sync.py @@ -7,12 +7,12 @@ import tempfile -import requests import os -import srp._pysrp as srp import shutil import u1db -import webapp_login +from support.api import Api +from support.config import Config +from support.user import User from u1db.remote.http_target import HTTPSyncTarget @@ -40,14 +40,11 @@ HTTPSyncTarget._sign_request = _sign_request def get_soledad_info(config, tempdir): # get login and get user info - user = config['user'] - api = config['api'] - usr = srp.User( user['username'], user['password'], srp.SHA256, srp.NG_1024 ) - auth = webapp_login.authenticate(api, usr) + user = User(config) + api = Api(config, verify=False) + auth = user.login(api) # get soledad server url - service_url = 'https://%s:%d/%d/config/soledad-service.json' % \ - (api['domain'], api['port'], api['version']) - soledad_hosts = requests.get(service_url).json()['hosts'] + soledad_hosts = api.get('config/soledad-service.json')['hosts'] host = soledad_hosts.keys()[0] server_url = 'https://%s:%d/user-%s' % \ (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], @@ -58,14 +55,14 @@ def get_soledad_info(config, tempdir): cert_file = None # not used for now #with open(cert_file, 'w') as f: # f.write(ca_cert) - return auth['id'], user['password'], server_url, cert_file, auth['token'] + return auth['id'], server_url, cert_file, auth['token'] def can_sync_soledad_fine(): tempdir = tempfile.mkdtemp() try: - uuid, password, server_url, cert_file, token = \ - get_soledad_info(webapp_login.read_config(), tempdir) + uuid, server_url, cert_file, token = \ + get_soledad_info(Config(), tempdir) # in the future, we can replace the following by an actual Soledad # client sync, if needed db = u1db.open(os.path.join(tempdir, '%s.db' % uuid), True) @@ -75,6 +72,6 @@ def can_sync_soledad_fine(): shutil.rmtree(tempdir) if __name__ == '__main__': - import nagios_test + from support import nagios_test exit_code = nagios_test.run(can_sync_soledad_fine) exit(exit_code) diff --git a/test/nagios/support/user.py b/test/nagios/support/user.py index 8e49c4b..912de89 100644 --- a/test/nagios/support/user.py +++ b/test/nagios/support/user.py @@ -19,6 +19,7 @@ class User(): self.verify_server(auth) if not self.is_authenticated(): raise Exception('user is not authenticated') + return auth def init_authentication(self, api): uname, A = self.srp_user.start_authentication() -- cgit v1.2.3 From 36e99d8b23263cffcd58988c40ca3217349a94f2 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 16 Apr 2014 12:37:03 +0200 Subject: nagios test: also test registering new users --- test/nagios/support/user.py | 28 ++++++++++++++++++++++++---- test/nagios/webapp_signup.py | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100755 test/nagios/webapp_signup.py diff --git a/test/nagios/support/user.py b/test/nagios/support/user.py index 912de89..9bf1d0a 100644 --- a/test/nagios/support/user.py +++ b/test/nagios/support/user.py @@ -1,13 +1,33 @@ import srp._pysrp as srp import binascii +import string +import random safe_unhexlify = lambda x: binascii.unhexlify(x) if ( len(x) % 2 == 0) else binascii.unhexlify('0' + x) +# let's have some random name and password +def id_generator(size=6, chars=string.ascii_lowercase + string.digits): + return ''.join(random.choice(chars) for x in range(size)) + class User(): - def __init__(self, config): - self.config = config.user - self.srp_user = srp.User(self.config['username'], self.config['password'], srp.SHA256, srp.NG_1024) + def __init__(self, config = None): + if config and config.user: + self.username = config.user["username"] + self.password = config.user["password"] + else: + self.username = 'test_' + id_generator() + self.password = id_generator() + id_generator() + self.srp_user = srp.User(self.username, self.password, srp.SHA256, srp.NG_1024) + + def signup(self, api): + salt, vkey = srp.create_salted_verification_key( self.username, self.password, srp.SHA256, srp.NG_1024 ) + user_params = { + 'user[login]': self.username, + 'user[password_verifier]': binascii.hexlify(vkey), + 'user[password_salt]': binascii.hexlify(salt) + } + return api.post('users.json', data = user_params) def login(self, api): init=self.init_authentication(api) @@ -32,7 +52,7 @@ class User(): def authenticate(self, api, init): M = self.srp_user.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) - auth = api.put('sessions/' + self.config["username"], + auth = api.put('sessions/' + self.username, data={'client_auth': binascii.hexlify(M)}) return auth diff --git a/test/nagios/webapp_signup.py b/test/nagios/webapp_signup.py new file mode 100755 index 0000000..3e7283e --- /dev/null +++ b/test/nagios/webapp_signup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# Test Signup and Login with the webapp API works. + +from support.api import Api +from support.config import Config +from support.user import User + +def signup_successfully(): + config = Config() + user = User() + api = Api(config, verify=False) + user.signup(api) + user.login(api) + +if __name__ == '__main__': + from support import nagios_test + exit_code = nagios_test.run(signup_successfully) + exit(exit_code) -- cgit v1.2.3 From d639e0a48599b30777b80c2809ded1efb3a6d926 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 10:04:12 +0200 Subject: add a try/except for older versions of requests they have response.json as a dict instead of response.json() --- test/nagios/support/api.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/nagios/support/api.py b/test/nagios/support/api.py index 3b6a90f..ec1af99 100644 --- a/test/nagios/support/api.py +++ b/test/nagios/support/api.py @@ -17,17 +17,23 @@ class Api(): response = self.session.get(self.api_url(path), verify=self.verify, **args) - return response.json() + return self.parse_json(response) def post(self, path, **args): response = self.session.post(self.api_url(path), verify=self.verify, **args) - return response.json() + return self.parse_json(response) def put(self, path, **args): response = self.session.put(self.api_url(path), verify=self.verify, **args) - return response.json() + return self.parse_json(response) + + def parse_json(self, response): + try: + return response.json() + except TypeError: + return response.json # older versions of requests -- cgit v1.2.3 From 8cc5ba134f6c5a1a06d91407aa78b962545c54ac Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 11:42:13 +0200 Subject: initial commit for the service level api :api/service will return a hash of the current users service level This is failiing if the user is not logged in. Instead it should return the service description for an anonymous user. --- app/controllers/v1/services_controller.rb | 8 ++++++++ app/models/service_level.rb | 1 + config/routes.rb | 1 + test/functional/v1/services_controller_test.rb | 23 +++++++++++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 app/controllers/v1/services_controller.rb create mode 100644 test/functional/v1/services_controller_test.rb diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/v1/services_controller.rb new file mode 100644 index 0000000..594940e --- /dev/null +++ b/app/controllers/v1/services_controller.rb @@ -0,0 +1,8 @@ +class V1::ServicesController < ApplicationController + + respond_to :json + + def show + respond_with current_user.effective_service_level + end +end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 299aaf1..31a713b 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -16,4 +16,5 @@ class ServiceLevel APP_CONFIG[:service_levels][@id] end + delegate :to_json, to: :config_hash end diff --git a/config/routes.rb b/config/routes.rb index b930bd1..f612b47 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,7 @@ LeapWeb::Application.routes.draw do resources :users, :only => [:create, :update, :destroy, :index] resources :messages, :only => [:index, :update] resource :cert, :only => [:show] + resource :service, :only => [:show] end scope "(:locale)", :locale => MATCH_LOCALE do diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/v1/services_controller_test.rb new file mode 100644 index 0000000..35a9de6 --- /dev/null +++ b/test/functional/v1/services_controller_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class V1::ServicesControllerTest < ActionController::TestCase + + test "anonymous user can request service info" do + get :show, format: :json + assert_json_response name: 'anonymous', + cert_prefix: 'LIMITED', + description: 'anonymous account, with rate limited VPN' + end + + test "user can see their service info" do + login + get :show, format: :json + assert_json_response name: 'free', + cert_prefix: 'LIMITED', + description: 'free account, with rate limited VPN', + cost: 0, + quota: 100 + end + +end + -- cgit v1.2.3 From 614745c84cab37dd03f2bd8f06160fd01c7fabdb Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 12:06:38 +0200 Subject: UnauthenticatedUser as current_user this still allows us to do current_user.service_level. Have not gone through the rest of the code yet. Only made sure logged_in? now tests for is_a? User instead of !!current_user --- app/controllers/controller_extension/authentication.rb | 12 +++++++++--- app/models/unauthenticated_user.rb | 7 +++++++ config/defaults.yml | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 03d3989..2bc0aee 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -8,11 +8,11 @@ module ControllerExtension::Authentication end def current_user - @current_user ||= token_authenticate || warden.user + @current_user ||= token_authenticate || warden.user || unauthenticated end def logged_in? - !!current_user + current_user.is_a? User end def require_login @@ -42,7 +42,7 @@ module ControllerExtension::Authentication end def admin? - current_user && current_user.is_admin? + current_user.is_admin? end def require_admin @@ -72,4 +72,10 @@ module ControllerExtension::Authentication request.env['warden.options'] && request.env['warden.options'][:attempted_path] end + + protected + + def unauthenticated + UnauthenticatedUser.new + end end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb index 0fc17d2..ba6470a 100644 --- a/app/models/unauthenticated_user.rb +++ b/app/models/unauthenticated_user.rb @@ -3,4 +3,11 @@ 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. + def is_admin? + false + end + + def effective_service_level + ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + end end diff --git a/config/defaults.yml b/config/defaults.yml index e7d0f5e..47c3ad7 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -67,6 +67,7 @@ service_levels: &service_levels USD: 10 EUR: 10 default_service_level: 1 + unauthenticated_service_level: 0 development: <<: *downloads -- cgit v1.2.3 From 7a9ece43bd61246b450471ed6bb1089570321e38 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 19:27:47 +0200 Subject: make use of the UnauthorizedUser Null Pattern for current_user - use it to get rid of some conditionals --- app/controllers/v1/certs_controller.rb | 44 +++--------------- app/controllers/v1/messages_controller.rb | 5 +- app/models/service_level.rb | 14 +++++- app/models/unauthenticated_user.rb | 20 ++++++-- config/defaults.yml | 13 ++++-- .../support/app/controllers/tickets_controller.rb | 36 +++++++++------ engines/support/app/views/tickets/new.html.haml | 18 ++------ engines/support/app/views/tickets/show.html.haml | 4 +- test/functional/v1/certs_controller_test.rb | 54 +++++++++++----------- 9 files changed, 104 insertions(+), 104 deletions(-) diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index 64cfa7f..580c90c 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,50 +1,20 @@ class V1::CertsController < ApplicationController - before_filter :require_login, :unless => :anonymous_certs_allowed? + before_filter :require_eip_access # GET /cert def show - @cert = ClientCertificate.new(:prefix => certificate_prefix) + @cert = ClientCertificate.new(:prefix => service_level.cert_prefix) render text: @cert.to_s, content_type: 'text/plain' end protected - def anonymous_certs_allowed? - APP_CONFIG[:allow_anonymous_certs] + def require_eip_access + access_denied unless service_level.provides?(:eip) 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 + + def service_level + current_user.effective_service_level end end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb index f71d0f1..85156b7 100644 --- a/app/controllers/v1/messages_controller.rb +++ b/app/controllers/v1/messages_controller.rb @@ -7,12 +7,11 @@ module V1 respond_to :json def index - render json: (current_user ? current_user.messages : [] ) + render json: current_user.messages end def update - message = Message.find(params[:id]) - if (message and current_user) + if message = Message.find(params[:id]) message.mark_as_read_by(current_user) message.save render json: true diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 31a713b..d0bd9b3 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -13,8 +13,20 @@ class ServiceLevel end def config_hash - APP_CONFIG[:service_levels][@id] + @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access end delegate :to_json, to: :config_hash + + def provides?(service) + services.include? service.to_s + end + + def services + config_hash[:services] || [] + end + + def cert_prefix + config_hash[:cert_prefix] + end end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb index ba6470a..7845a6f 100644 --- a/app/models/unauthenticated_user.rb +++ b/app/models/unauthenticated_user.rb @@ -1,13 +1,27 @@ # 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. + def effective_service_level + ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + end def is_admin? false end - def effective_service_level - ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + def id + nil + end + + def email_address + nil + end + + def login + nil + end + + def messages + [] end end diff --git a/config/defaults.yml b/config/defaults.yml index 47c3ad7..383aa1c 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -7,11 +7,6 @@ cert_options: &cert_options client_cert_lifespan: 2 client_cert_bit_size: 2024 client_cert_hash: "SHA256" - allow_limited_certs: false - allow_unlimited_certs: true - allow_anonymous_certs: false - limited_cert_prefix: "LIMITED" - unlimited_cert_prefix: "UNLIMITED" downloads: &downloads client_download_domain: https://dl.bitmask.net @@ -53,12 +48,17 @@ service_levels: &service_levels name: anonymous cert_prefix: "LIMITED" description: "anonymous account, with rate limited VPN" + services: + - eip 1: name: free cert_prefix: "LIMITED" description: "free account, with rate limited VPN" cost: 0 quota: 100 + services: + - eip + - email 2: name: premium cert_prefix: "UNLIMITED" @@ -66,6 +66,9 @@ service_levels: &service_levels cost: USD: 10 EUR: 10 + services: + - eip + - email default_service_level: 1 unauthenticated_service_level: 0 diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index d65ee43..cf8743a 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -5,7 +5,8 @@ class TicketsController < ApplicationController #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_ticket, :only => [:show, :update, :destroy] + before_filter :require_ticket_access, :only => [:show, :update, :destroy] before_filter :fetch_user before_filter :set_title @@ -17,11 +18,11 @@ class TicketsController < ApplicationController 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. + #protecting posted_by isn't working, so this should protect it: + @ticket.comments.last.posted_by = current_user.id @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 - + @ticket.created_by = current_user.id + @ticket.email = current_user.email_address if current_user.email_address if @ticket.save flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) end @@ -58,7 +59,7 @@ class TicketsController < ApplicationController end if @ticket.comments_changed? - @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) + @ticket.comments.last.posted_by = current_user.id @ticket.comments.last.private = false unless admin? end @@ -120,19 +121,28 @@ class TicketsController < ApplicationController 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 + if !@ticket + if admin? + redirect_to auto_tickets_path, + alert: t(:no_such_thing, thing: 'ticket') + else + access_denied + end end + end + + def require_ticket_access access_denied unless ticket_access? end + def ticket_access? + admin? or + @ticket.created_by.blank? or + current_user.id == @ticket.created_by + end + def fetch_user if params[:user_id] @user = User.find(params[:user_id]) diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index 8f217a5..e391499 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -2,22 +2,14 @@ = 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 +- user = @user if admin? +- user ||= current_user = simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| = hidden_ticket_fields = f.input :subject - - 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.input :email, input_html: {value: user.email} + = f.input :regarding_user, input_html: {value: user.login} = 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? @@ -27,4 +19,4 @@ - 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 + = link_to t(:cancel), home_path, :class => 'btn' diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml index bfdb773..edb6e6f 100644 --- a/engines/support/app/views/tickets/show.html.haml +++ b/engines/support/app/views/tickets/show.html.haml @@ -7,6 +7,6 @@ = render :partial => 'tickets/comment', :collection => @ticket.comments %tr %td.user - = logged_in? ? current_user.login : t(:anonymous) + = current_user.login || t(:anonymous) %td.comment - = render 'tickets/new_comment_form' \ No newline at end of file + = render 'tickets/new_comment_form' diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/v1/certs_controller_test.rb index 2c70e52..3631947 100644 --- a/test/functional/v1/certs_controller_test.rb +++ b/test/functional/v1/certs_controller_test.rb @@ -3,42 +3,42 @@ 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 + cert = expect_cert('LIMITED') + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end + + test "send limited cert" do + login + cert = expect_cert('LIMITED') + get :show + assert_response :success + assert_equal cert.to_s, @response.body 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 + login effective_service_level: ServiceLevel.new(id: 2) + cert = expect_cert('UNLIMITED') + get :show + assert_response :success + assert_equal cert.to_s, @response.body end - test "login required if anonymous certs disabled" do - with_config allow_anonymous_certs: false do + test "redirect if no eip service offered" do + with_config({service_levels: {0 => {services: []}}}) 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 + protected + def expect_cert(prefix) + cert = stub :to_s => "#{prefix.downcase} cert" + ClientCertificate.expects(:new). + with(:prefix => prefix). + returns(cert) + return cert + end end -- cgit v1.2.3 From fca9752315a0b46b52facf0e54c35214198fe8ae Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 20:03:47 +0200 Subject: adjust test to service list in config --- test/functional/v1/services_controller_test.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/v1/services_controller_test.rb index 35a9de6..bcb7abc 100644 --- a/test/functional/v1/services_controller_test.rb +++ b/test/functional/v1/services_controller_test.rb @@ -6,7 +6,8 @@ class V1::ServicesControllerTest < ActionController::TestCase get :show, format: :json assert_json_response name: 'anonymous', cert_prefix: 'LIMITED', - description: 'anonymous account, with rate limited VPN' + description: 'anonymous account, with rate limited VPN', + services: ["eip"] end test "user can see their service info" do @@ -16,7 +17,8 @@ class V1::ServicesControllerTest < ActionController::TestCase cert_prefix: 'LIMITED', description: 'free account, with rate limited VPN', cost: 0, - quota: 100 + quota: 100, + services: ["eip", "email"] end end -- cgit v1.2.3 From 40dfa63aa6fc7aa3614f2a7952d088d8ff067f70 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 10:29:07 +0200 Subject: minor fix: User#email_address not User#email --- engines/support/app/views/tickets/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index e391499..8a89703 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -8,7 +8,7 @@ = simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| = hidden_ticket_fields = f.input :subject - = f.input :email, input_html: {value: user.email} + = f.input :email, input_html: {value: user.email_address} = f.input :regarding_user, input_html: {value: user.login} = f.simple_fields_for :comments, @comment do |c| = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5} -- cgit v1.2.3 From 9216ab8252246a263c5d17f6755a7d3887145f94 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 11:55:40 +0200 Subject: change service level configuration strategy The changes to the configuration required some non minor changes to the platform and also added some flexibility we don't require yet - and thus some new possibilities for errors. So instead we still use the allow_..._certs and ..._cert_prefix options. They basically provide the framework in which service levels can operate. The service level configuration will not include the cert prefix anymore. It only states if the service level is rate limited or not. This avoids conflicts between the two configuration options. I also removed the anonymous service level entirely. It was also turning a boolean decision (do we provide anonymous eip or not) into something way more complex. Instead I added the AnonymousServiceLevel class to handle the corner cases for people who are not logged in. Furthermore i renamed the UnauthenticatedUser to AnonymousUser so it matches the Anonymous Service Level nicely. It's also shorter and more intuitive. --- .../controller_extension/authentication.rb | 6 ++--- app/controllers/v1/certs_controller.rb | 6 ++--- app/models/anonymous_service_level.rb | 31 ++++++++++++++++++++++ app/models/anonymous_user.rb | 27 +++++++++++++++++++ app/models/service_level.rb | 30 ++++++++++++--------- app/models/unauthenticated_user.rb | 27 ------------------- app/views/users/_change_service_level.html.haml | 4 +-- config/defaults.yml | 21 +++++---------- test/functional/v1/certs_controller_test.rb | 30 +++++++++++---------- test/functional/v1/services_controller_test.rb | 23 +++++++++++----- test/unit/anonymous_user_test.rb | 23 ++++++++++++++++ test/unit/unauthenticated_user_test.rb | 7 ----- 12 files changed, 145 insertions(+), 90 deletions(-) create mode 100644 app/models/anonymous_service_level.rb create mode 100644 app/models/anonymous_user.rb delete mode 100644 app/models/unauthenticated_user.rb create mode 100644 test/unit/anonymous_user_test.rb delete mode 100644 test/unit/unauthenticated_user_test.rb diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 2bc0aee..1f73f38 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -8,7 +8,7 @@ module ControllerExtension::Authentication end def current_user - @current_user ||= token_authenticate || warden.user || unauthenticated + @current_user ||= token_authenticate || warden.user || anonymous end def logged_in? @@ -75,7 +75,7 @@ module ControllerExtension::Authentication protected - def unauthenticated - UnauthenticatedUser.new + def anonymous + AnonymousUser.new end end diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index 580c90c..73409ef 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,6 +1,6 @@ class V1::CertsController < ApplicationController - before_filter :require_eip_access + before_filter :require_login, :unless => :anonymous_certs_allowed? # GET /cert def show @@ -10,8 +10,8 @@ class V1::CertsController < ApplicationController protected - def require_eip_access - access_denied unless service_level.provides?(:eip) + def anonymous_certs_allowed? + APP_CONFIG[:allow_anonymous_certs] end def service_level diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb new file mode 100644 index 0000000..c51ce9e --- /dev/null +++ b/app/models/anonymous_service_level.rb @@ -0,0 +1,31 @@ +class AnonymousServiceLevel + + delegate :to_json, to: :config_hash + + def cert_prefix + if APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:limited_cert_prefix] + else + APP_CONFIG[:unlimited_cert_prefix] + end + end + + def description + if APP_CONFIG[:allow_anonymous_certs] + "anonymous access to the VPN" + else + "please login to access our services" + end + end + + protected + + def config_hash + { name: "anonymous", + description: description, + cost: 0, + eip_rate_limit: APP_CONFIG[:allow_limited_certs] + } + end + +end diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb new file mode 100644 index 0000000..360a577 --- /dev/null +++ b/app/models/anonymous_user.rb @@ -0,0 +1,27 @@ +# The nil object for the user class +class AnonymousUser < Object + + def effective_service_level + AnonymousServiceLevel.new + end + + def is_admin? + false + end + + def id + nil + end + + def email_address + nil + end + + def login + nil + end + + def messages + [] + end +end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index d0bd9b3..06ad202 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -4,29 +4,35 @@ class ServiceLevel @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 + def self.select_options + APP_CONFIG[:service_levels].map do |id,config_hash| + [config_hash[:description], id] + end end def id @id end - def config_hash - @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access - end - delegate :to_json, to: :config_hash - def provides?(service) - services.include? service.to_s + def cert_prefix + if limited_cert? + APP_CONFIG[:limited_cert_prefix] + else + APP_CONFIG[:unlimited_cert_prefix] + end end - def services - config_hash[:services] || [] + protected + + def limited_cert? + APP_CONFIG[:allow_limited_certs] && + (!APP_CONFIG[:allow_unlimited_certs] || config_hash[:eip_rate_limit]) end - def cert_prefix - config_hash[:cert_prefix] + def config_hash + @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access end + end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb deleted file mode 100644 index 7845a6f..0000000 --- a/app/models/unauthenticated_user.rb +++ /dev/null @@ -1,27 +0,0 @@ -# The nil object for the user class -class UnauthenticatedUser < Object - - def effective_service_level - ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] - end - - def is_admin? - false - end - - def id - nil - end - - def email_address - nil - end - - def login - nil - end - - def messages - [] - end -end diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml index 61e67d9..42315a2 100644 --- a/app/views/users/_change_service_level.html.haml +++ b/app/views/users/_change_service_level.html.haml @@ -8,11 +8,11 @@ %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 + = f.select :desired_service_level_code, ServiceLevel.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 + = f.select :effective_service_level_code, ServiceLevel.select_options, :selected => @user.effective_service_level.id .control-group .controls = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/config/defaults.yml b/config/defaults.yml index 383aa1c..a7b70a3 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -7,6 +7,11 @@ cert_options: &cert_options client_cert_lifespan: 2 client_cert_bit_size: 2024 client_cert_hash: "SHA256" + allow_limited_certs: false + allow_unlimited_certs: true + allow_anonymous_certs: false + limited_cert_prefix: "LIMITED" + unlimited_cert_prefix: "UNLIMITED" downloads: &downloads client_download_domain: https://dl.bitmask.net @@ -44,33 +49,19 @@ common: &common service_levels: &service_levels service_levels: - 0: - name: anonymous - cert_prefix: "LIMITED" - description: "anonymous account, with rate limited VPN" - services: - - eip 1: name: free - cert_prefix: "LIMITED" description: "free account, with rate limited VPN" cost: 0 + eip_rate_limit: true quota: 100 - services: - - eip - - email 2: name: premium - cert_prefix: "UNLIMITED" description: "premium account, with unlimited vpn" cost: USD: 10 EUR: 10 - services: - - eip - - email default_service_level: 1 - unauthenticated_service_level: 0 development: <<: *downloads diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/v1/certs_controller_test.rb index 3631947..fb8e9c4 100644 --- a/test/functional/v1/certs_controller_test.rb +++ b/test/functional/v1/certs_controller_test.rb @@ -2,19 +2,23 @@ require 'test_helper' class V1::CertsControllerTest < ActionController::TestCase - test "send limited cert without login" do - cert = expect_cert('LIMITED') - get :show - assert_response :success - assert_equal cert.to_s, @response.body + test "send unlimited cert without login" do + with_config allow_anonymous_certs: true do + cert = expect_cert('UNLIMITED') + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end end test "send limited cert" do - login - cert = expect_cert('LIMITED') - get :show - assert_response :success - assert_equal cert.to_s, @response.body + with_config allow_limited_certs: true do + login + cert = expect_cert('LIMITED') + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end end test "send unlimited cert" do @@ -26,10 +30,8 @@ class V1::CertsControllerTest < ActionController::TestCase end test "redirect if no eip service offered" do - with_config({service_levels: {0 => {services: []}}}) do - get :show - assert_response :redirect - end + get :show + assert_response :redirect end protected diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/v1/services_controller_test.rb index bcb7abc..b81103f 100644 --- a/test/functional/v1/services_controller_test.rb +++ b/test/functional/v1/services_controller_test.rb @@ -2,23 +2,32 @@ require 'test_helper' class V1::ServicesControllerTest < ActionController::TestCase - test "anonymous user can request service info" do + test "anonymous user gets login required service info" do get :show, format: :json assert_json_response name: 'anonymous', - cert_prefix: 'LIMITED', - description: 'anonymous account, with rate limited VPN', - services: ["eip"] + eip_rate_limit: false, + description: 'please login to access our services', + cost: 0 + end + + test "anonymous user gets vpn service info" do + with_config allow_anonymous_certs: true do + get :show, format: :json + assert_json_response name: 'anonymous', + eip_rate_limit: false, + description: 'anonymous access to the VPN', + cost: 0 + end end test "user can see their service info" do login get :show, format: :json assert_json_response name: 'free', - cert_prefix: 'LIMITED', + eip_rate_limit: true, description: 'free account, with rate limited VPN', cost: 0, - quota: 100, - services: ["eip", "email"] + quota: 100 end end diff --git a/test/unit/anonymous_user_test.rb b/test/unit/anonymous_user_test.rb new file mode 100644 index 0000000..6e94d39 --- /dev/null +++ b/test/unit/anonymous_user_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class AnonymousUserTest < ActiveSupport::TestCase + + setup do + @anonymous = AnonymousUser.new + end + + test "has nil values" do + assert_nil @anonymous.id + assert_nil @anonymous.email_address + assert_nil @anonymous.login + end + + test "has no messages" do + assert_equal [], @anonymous.messages + end + + test "has anonymous service level" do + assert @anonymous.effective_service_level.is_a? AnonymousServiceLevel + end + +end diff --git a/test/unit/unauthenticated_user_test.rb b/test/unit/unauthenticated_user_test.rb deleted file mode 100644 index e5fafb8..0000000 --- a/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 -- cgit v1.2.3 From 966e390d401b84dad98127e647d2ec634f1cbc15 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 12:39:27 +0200 Subject: bringing back empty cert prefixes if neither limited nor unlimited certs are allowed there will be no prefix. Not sure if this is desired - but it's the way things used to be before the refactoring --- app/models/anonymous_service_level.rb | 2 +- app/models/service_level.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb index c51ce9e..47b7cfb 100644 --- a/app/models/anonymous_service_level.rb +++ b/app/models/anonymous_service_level.rb @@ -5,7 +5,7 @@ class AnonymousServiceLevel def cert_prefix if APP_CONFIG[:allow_limited_certs] APP_CONFIG[:limited_cert_prefix] - else + elsif APP_CONFIG[:allow_unlimited_certs] APP_CONFIG[:unlimited_cert_prefix] end end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 06ad202..5dd8838 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -19,7 +19,7 @@ class ServiceLevel def cert_prefix if limited_cert? APP_CONFIG[:limited_cert_prefix] - else + elsif APP_CONFIG[:allow_unlimited_certs] APP_CONFIG[:unlimited_cert_prefix] end end -- cgit v1.2.3 From be81b7430e0a2046125be7c3a4b01b8725f4afe6 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 12:51:18 +0200 Subject: adopt service_level config to platform settings cost -> rate quota -> storage --- app/models/anonymous_service_level.rb | 1 - config/defaults.yml | 5 ++--- test/functional/v1/services_controller_test.rb | 9 +++------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb index 47b7cfb..4366a4a 100644 --- a/app/models/anonymous_service_level.rb +++ b/app/models/anonymous_service_level.rb @@ -23,7 +23,6 @@ class AnonymousServiceLevel def config_hash { name: "anonymous", description: description, - cost: 0, eip_rate_limit: APP_CONFIG[:allow_limited_certs] } end diff --git a/config/defaults.yml b/config/defaults.yml index a7b70a3..47cb641 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -52,13 +52,12 @@ service_levels: &service_levels 1: name: free description: "free account, with rate limited VPN" - cost: 0 eip_rate_limit: true - quota: 100 + storage: 100 2: name: premium description: "premium account, with unlimited vpn" - cost: + rate: USD: 10 EUR: 10 default_service_level: 1 diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/v1/services_controller_test.rb index b81103f..e4058c0 100644 --- a/test/functional/v1/services_controller_test.rb +++ b/test/functional/v1/services_controller_test.rb @@ -6,8 +6,7 @@ class V1::ServicesControllerTest < ActionController::TestCase get :show, format: :json assert_json_response name: 'anonymous', eip_rate_limit: false, - description: 'please login to access our services', - cost: 0 + description: 'please login to access our services' end test "anonymous user gets vpn service info" do @@ -15,8 +14,7 @@ class V1::ServicesControllerTest < ActionController::TestCase get :show, format: :json assert_json_response name: 'anonymous', eip_rate_limit: false, - description: 'anonymous access to the VPN', - cost: 0 + description: 'anonymous access to the VPN' end end @@ -26,8 +24,7 @@ class V1::ServicesControllerTest < ActionController::TestCase assert_json_response name: 'free', eip_rate_limit: true, description: 'free account, with rate limited VPN', - cost: 0, - quota: 100 + storage: 100 end end -- cgit v1.2.3 From dad200e2cb0b03995eb06024d3fcf788ad2486ff Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 10:24:24 +0200 Subject: add signup and login info on the forms --- app/views/sessions/new.html.haml | 1 + app/views/users/new.html.haml | 2 +- config/locales/en.yml | 2 +- config/locales/users.en.yml | 2 -- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 771dc97..316eec1 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -2,6 +2,7 @@ .span9 = render :partial => 'users/warnings' %h2=t :login + .lead=t :login_info = 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 } diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index aecf831..173dd8c 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -8,8 +8,8 @@ .span9 = render :partial => 'warnings' %h2=t :signup + .lead=t :signup_info = 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 } diff --git a/config/locales/en.yml b/config/locales/en.yml index ac154d6..cebf075 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,7 +30,7 @@ en: 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)." + signup_info: "Get a user account via this website. We recommend registering via the Bitmask application instead unless you are only using Bitmask for Android." welcome: "Welcome to %{provider}." get_help: "Get Help" help_info: "Can't login? Create a new support ticket anonymously." diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index ed6653a..b9889b6 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -3,13 +3,11 @@ en: 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)." -- cgit v1.2.3 From 1da12752c3b3aa3d4a03e67ede133e807e9434df Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 11:10:01 +0200 Subject: cleanup homepage buttons some let's devide the partials rather than having super specific parameters (on_user_page) --- app/helpers/core_helper.rb | 4 ++-- app/views/common/_action_buttons.html.haml | 11 +++++++++++ app/views/common/_download_button.html.haml | 6 ++++++ app/views/common/_home_page_buttons.html.haml | 27 ++++++--------------------- app/views/home/_content.html.haml | 2 -- app/views/users/show.html.haml | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 app/views/common/_action_buttons.html.haml create mode 100644 app/views/common/_download_button.html.haml diff --git a/app/helpers/core_helper.rb b/app/helpers/core_helper.rb index a6c7479..4126906 100644 --- a/app/helpers/core_helper.rb +++ b/app/helpers/core_helper.rb @@ -6,8 +6,8 @@ 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} + def home_page_buttons + render 'common/home_page_buttons' end end diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml new file mode 100644 index 0000000..c74fcd1 --- /dev/null +++ b/app/views/common/_action_buttons.html.haml @@ -0,0 +1,11 @@ +.home-buttons + .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/common/_download_button.html.haml b/app/views/common/_download_button.html.haml new file mode 100644 index 0000000..157c40a --- /dev/null +++ b/app/views/common/_download_button.html.haml @@ -0,0 +1,6 @@ +.home-buttons + .row-fluid.first + .span2 + .download.span8 + = render partial: 'common/download_for_os', collection: available_clients + ['other'] + .span2 diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml index c9ea7a2..8c47983 100644 --- a/app/views/common/_home_page_buttons.html.haml +++ b/app/views/common/_home_page_buttons.html.haml @@ -1,23 +1,8 @@ - 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) += render 'common/download_button' +- if local_assigns[:divider] + .row-fluid + .span12 + = render local_assigns[:divider] += render 'common/action_buttons', icon_color: icon_color diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml index 3d351e9..e47fdaf 100644 --- a/app/views/home/_content.html.haml +++ b/app/views/home/_content.html.haml @@ -8,7 +8,5 @@ - if Rails.env == 'development' .row-fluid %hr - %p - = link_to "fetch a cert", cert_path %p = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present? diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c587017..3c73147 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -22,4 +22,4 @@ %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 + = render 'common/download_button' -- cgit v1.2.3 From 0968d0009070dea86cb312a2f0ce4a2e8cbac94c Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 13:07:30 +0200 Subject: unified wording: destroy account --- config/locales/users.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index ed6653a..5747225 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -48,7 +48,7 @@ en: 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." + overview_account: "Destroy your account." # # rails -- cgit v1.2.3 From 79b14e638d14b02d9747f42984433fd7d77b0ea8 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 22 Apr 2014 14:34:17 +0200 Subject: cert_path now is api_cert_path --- app/views/home/_content.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml index 3d351e9..384abb2 100644 --- a/app/views/home/_content.html.haml +++ b/app/views/home/_content.html.haml @@ -9,6 +9,6 @@ .row-fluid %hr %p - = link_to "fetch a cert", cert_path + = link_to "fetch a cert", api_cert_path %p = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present? -- cgit v1.2.3 From c275f43147e7a8ab54623bdc5b9a8124b8592330 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 22 Apr 2014 14:39:43 +0200 Subject: return nil as auto_ticket_path for invalid tickets The auto_ticket_path is referenced from the tickets_controller like this: respond_with(@ticket, :location => auto_ticket_path(@ticket)) While this only uses the location parameter when the ticket is valid it will still attampt to calculate it if not. During the create action this will lead to crashes because the ticket_path can not be calculated for a ticket without an id. This led to https://leap.se/code/issues/5552 and probably https://leap.se/code/issues/5545. --- engines/support/app/helpers/auto_tickets_path_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engines/support/app/helpers/auto_tickets_path_helper.rb b/engines/support/app/helpers/auto_tickets_path_helper.rb index 93f3cb9..5638222 100644 --- a/engines/support/app/helpers/auto_tickets_path_helper.rb +++ b/engines/support/app/helpers/auto_tickets_path_helper.rb @@ -23,6 +23,7 @@ module AutoTicketsPathHelper end def auto_ticket_path(ticket, options={}) + return unless ticket.persisted? options = ticket_view_options.merge options if @user user_ticket_path(@user, ticket, options) @@ -50,4 +51,4 @@ module AutoTicketsPathHelper hsh end -end \ No newline at end of file +end -- cgit v1.2.3 From c5c54ec2035813949a81e8b5977a8f2538897260 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 22 Apr 2014 14:40:27 +0200 Subject: let's only add the flash notice if the ticket has been created --- engines/support/app/controllers/tickets_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index d65ee43..4be3493 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -24,11 +24,11 @@ class TicketsController < ApplicationController 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)) + # 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 end respond_with(@ticket, :location => auto_ticket_path(@ticket)) end -- cgit v1.2.3 From 7df11eed6aa48d1f61ee7f3700295c78b62358f0 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 22 Apr 2014 14:43:19 +0200 Subject: check user_id param for present? instead of !nil? when rendered from teh create action due to an error user_id param will sometimes be an empty string. We should still avoid rendering the navigation because the path's can not be resolved without a user_id. --- engines/support/app/views/tickets/index.html.haml | 2 +- engines/support/app/views/tickets/new.html.haml | 4 ++-- engines/support/app/views/tickets/show.html.haml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml index c02a326..a4df6e3 100644 --- a/engines/support/app/views/tickets/index.html.haml +++ b/engines/support/app/views/tickets/index.html.haml @@ -1,4 +1,4 @@ -- @show_navigation = !params[:user_id].nil? +- @show_navigation = params[:user_id].present? = render 'tickets/tabs' diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index 8f217a5..ba86ac3 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -1,4 +1,4 @@ -- @show_navigation = !params[:user_id].nil? +- @show_navigation = params[:user_id].present? = render 'tickets/tabs' @@ -27,4 +27,4 @@ - 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 + = link_to t(:cancel), home_path, :class => 'btn' diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml index bfdb773..bbca4bf 100644 --- a/engines/support/app/views/tickets/show.html.haml +++ b/engines/support/app/views/tickets/show.html.haml @@ -1,4 +1,4 @@ -- @show_navigation = !params[:user_id].nil? +- @show_navigation = params[:user_id].present? .ticket = render 'tickets/edit_form' @@ -9,4 +9,4 @@ %td.user = logged_in? ? current_user.login : t(:anonymous) %td.comment - = render 'tickets/new_comment_form' \ No newline at end of file + = render 'tickets/new_comment_form' -- cgit v1.2.3 From 5d6a82a67bffc0930ea480164be2aca60ea0d6a1 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 22 Apr 2014 14:44:11 +0200 Subject: add tests for invalid ticket creation --- .../test/functional/tickets_controller_test.rb | 11 +++++++++ .../support/test/integration/create_ticket_test.rb | 28 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 engines/support/test/integration/create_ticket_test.rb diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index 416fb73..d746b59 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -72,6 +72,17 @@ class TicketsControllerTest < ActionController::TestCase end + test "handle invalid ticket" do + params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}, :email => 'a'} + + assert_no_difference('Ticket.count') do + post :create, :ticket => params + end + + assert_template :new + assert_equal params[:subject], assigns(:ticket).subject + end + test "should create authenticated ticket" do params = {:subject => "auth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb new file mode 100644 index 0000000..2583fc7 --- /dev/null +++ b/engines/support/test/integration/create_ticket_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class CreateTicketTest < BrowserIntegrationTest + + test "can submit ticket anonymously" do + visit '/' + click_on 'Get Help' + fill_in 'Subject', with: 'test ticket' + fill_in 'Description', with: 'description of the problem goes here' + click_on 'Create Ticket' + assert page.has_content?("Ticket was successfully created.") + assert page.has_content?("You can later access this ticket at the URL") + assert page.has_content?(current_url) + assert ticket = Ticket.last + ticket.destroy + end + + test "get help when creating ticket with invalid email" do + visit '/' + click_on 'Get Help' + fill_in 'Subject', with: 'test ticket' + fill_in 'Email', with: 'invalid data' + fill_in 'Description', with: 'description of the problem goes here' + click_on 'Create Ticket' + assert page.has_content?("is invalid") + end + +end -- cgit v1.2.3 From 83379a78b0e14d0938f452ef1d23ad94754350ae Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 20:37:44 +0200 Subject: remove cert link - has no purpose anymore --- app/views/home/_content.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml index 384abb2..e47fdaf 100644 --- a/app/views/home/_content.html.haml +++ b/app/views/home/_content.html.haml @@ -8,7 +8,5 @@ - if Rails.env == 'development' .row-fluid %hr - %p - = link_to "fetch a cert", api_cert_path %p = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present? -- cgit v1.2.3 From 689c10b618c6c469ce82a7f09e117567def577de Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 14:58:30 +0200 Subject: simple form: add wrapped and loading... buttons #5542 the loading... text on the buttons was not capitalized before. So in order to change this in a (more or less) single place i added new button types to simple_form: button :wrapped - normal button, with loading and an optional cancel button wrapped in the classical bootstrap action div. cancel option contains the url to go to when canceling. button :loading - simple button with loading text capitalized by using i18n (simple_form.buttons.loading) Conflicts: engines/support/app/views/tickets/new.html.haml --- app/views/sessions/new.html.haml | 4 +--- app/views/users/new.html.haml | 4 +--- config/initializers/simple_form.rb | 2 ++ config/locales/simple_form.en.yml | 3 +++ .../app/views/tickets/_new_comment_form.html.haml | 4 ++-- engines/support/app/views/tickets/_tabs.html.haml | 4 ++-- engines/support/app/views/tickets/new.html.haml | 7 +----- lib/extensions/simple_form.rb | 28 ++++++++++++++++++++++ 8 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 lib/extensions/simple_form.rb diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 316eec1..6f3b324 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -6,6 +6,4 @@ = 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' + = f.button :wrapped, value: t(:login), cancel: home_path diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index 173dd8c..3478989 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -13,7 +13,5 @@ = 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' + = f.button :wrapped, value: t(:signup), cancel: home_path diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index e3f8d09..710dacc 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -1,3 +1,5 @@ +require 'extensions/simple_form' + # 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 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 0df11fe..4beeb7d 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -10,6 +10,9 @@ en: # html: '*' error_notification: default_message: "Please review the problems below:" + buttons: + cancel: 'Cancel' + loading: 'Loading...' # Labels and hints examples # labels: # defaults: diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml index 8418e01..40c737f 100644 --- a/engines/support/app/views/tickets/_new_comment_form.html.haml +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -7,7 +7,7 @@ = 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' + = f.button :loading, t(:post_reply), class: 'btn-primary', 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' + = f.button :loading, t(:reply_and_close), 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 index b7b5d3a..445a909 100644 --- a/engines/support/app/views/tickets/_tabs.html.haml +++ b/engines/support/app/views/tickets/_tabs.html.haml @@ -1,7 +1,7 @@ -# -# SORT ORDER TABS -# -- unless action?(:new) +- unless action?(:new) or action?(:create) %ul.nav.nav-pills.pull-right.slim %li{:class=> ("active" if search_order.start_with? 'created_at')} = link_to_order('created') @@ -19,5 +19,5 @@ = link_to_status 'closed' %li{:class => ("active" if search_status == 'all')} = link_to_status 'all' - %li{:class => ("active" if action?(:new))} + %li{:class => ("active" if action?(:new) || action?(:create))} = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index 65ed67b..ab008d2 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -14,9 +14,4 @@ = 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' + = f.button :wrapped, cancel: (logged_in? ? auto_tickets_path : home_path) diff --git a/lib/extensions/simple_form.rb b/lib/extensions/simple_form.rb new file mode 100644 index 0000000..f28e18f --- /dev/null +++ b/lib/extensions/simple_form.rb @@ -0,0 +1,28 @@ +module WrappedButton + def wrapped_button(*args, &block) + template.content_tag :div, :class => "form-actions" do + options = args.extract_options! + options[:class] = ['btn-primary', options[:class]].compact + args.unshift :loading + args << options + if cancel = options.delete(:cancel) + cancel_link = template.link_to I18n.t('simple_form.buttons.cancel'), + cancel, class: :btn + button(*args, &block) + ' ' + cancel_link + else + button(*args, &block) + end + end + end +end +SimpleForm::FormBuilder.send :include, WrappedButton + +module LoadingButton + def loading_button(*args, &block) + options = args.extract_options! + options[:"data-loading-text"] = I18n.t('simple_form.buttons.loading') + args << options + button_button(*args, &block) + end +end +SimpleForm::FormBuilder.send :include, LoadingButton -- cgit v1.2.3 From 6c13d3323c180a333fd0f32d17a62adce9fcf2bb Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 15:27:40 +0200 Subject: using simple_form for the last form that was not using it. --- engines/support/app/controllers/tickets_controller.rb | 8 ++++---- engines/support/app/views/tickets/_edit_form.html.haml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 650f628..d552209 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -43,18 +43,18 @@ class TicketsController < ApplicationController end def update - if params[:commit] == 'close' + if params[:button] == 'close' @ticket.is_open = false @ticket.save redirect_to_tickets - elsif params[:commit] == 'open' + elsif params[:button] == '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' + if params[:button] == 'reply_and_close' @ticket.close end @@ -98,7 +98,7 @@ class TicketsController < ApplicationController # def redirect_to_tickets if logged_in? - if params[:commit] == t(:reply_and_close) + if params[:button] == t(:reply_and_close) redirect_to auto_tickets_path else redirect_to auto_ticket_path(@ticket) diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index 714f8ff..7958334 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -17,7 +17,7 @@ regarding_user_link = '' end -= form_for @ticket do |f| += simple_form_for @ticket do |f| = hidden_ticket_fields %p.first - if @ticket.is_open? @@ -39,10 +39,10 @@ = t(:regarding_account) = regarding_user_link = f.text_field :regarding_user - = f.button t(:save), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'save' + = f.button :loading, t(:save), :value => 'save' - if @ticket.is_open? - = f.button t(:close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'close' + = f.button :loading, t(:close), :value => 'close' - else - = f.button t(:open), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'open' + = f.button :loading, t(:open), :value => 'open' - if admin? = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' -- cgit v1.2.3 From e727d211630591a3aaed5eba0eb50c7d4964b6f8 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 16:54:40 +0200 Subject: ensure buttons are properly loading... and reset --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/buttons.js | 39 +++++++++++++++++++++++++++++++++++ app/assets/javascripts/users.js | 21 +++---------------- 3 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/buttons.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ab07e1f..9af373d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -20,3 +20,4 @@ //= require platform //= require tickets //= require users +//= require buttons diff --git a/app/assets/javascripts/buttons.js b/app/assets/javascripts/buttons.js new file mode 100644 index 0000000..7142957 --- /dev/null +++ b/app/assets/javascripts/buttons.js @@ -0,0 +1,39 @@ +/* + * Buttons.js + * + * Use bootstrap loading state after submitting a form. + * + * Some form inputs are validaded before the submission + * so triggering loading state on click events is not a + * good idea. If the validation fails the errors will + * be displayed but the button would be in loading state. + * + * We used to trigger it based on form submission but + * we have a few forms that contain multiple buttons. + * So now we mark the buttons as clicked on click and + * put the clicked button into loading state on submit. + * + */ + +(function() { + markAsClicked = function () { + var btn = $(this) + btn.addClass('clicked') + setTimeout(function () { + btn.removeClass('clicked') + }, 1000) + }; + + markAsLoading = function(submitEvent) { + var form = submitEvent.target; + $(form).addClass('submitted') + // bootstrap loading state: + $(form).find('.btn.clicked[type="submit"]').button('loading'); + }; + + $(document).ready(function() { + $('form').submit(markAsLoading); + $('.btn[type="submit"]').click(markAsClicked); + }); + +}).call(this); diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 8486756..4a86ef5 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -35,24 +35,10 @@ data: $(form).serialize() }); req.done( function() { - $(form).find('input[type="submit"]').button('reset'); + $(form).find('.btn[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 // @@ -79,7 +65,7 @@ clear_errors(); var errors = extractErrors(message); displayErrors(errors); - resetButtons(); + $('.btn[type="submit"]').button('reset'); } function extractErrors(message) { @@ -115,7 +101,6 @@ // $(document).ready(function() { - $('form').submit(markAsSubmitted); $('#new_user').submit(prevent_default); $('#new_user').submit(srp.signup); $('#new_session').submit(prevent_default); -- cgit v1.2.3 From 615261a6f0d1bae5d999e3014f18191ed1ba1008 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 17:14:30 +0200 Subject: move open and close buttons into status display They do not save any changes. So i think it's better to keep them separated from the Save button that does save changes. --- engines/support/app/views/tickets/_edit_form.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index 7958334..bf175fe 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -21,9 +21,13 @@ = hidden_ticket_fields %p.first - if @ticket.is_open? - %span.label.label-info= t(:open) + %span.label.label-info + %b{style: 'padding:10px'}= t(:open) + = f.button :loading, t(:close), value: 'close', class: 'btn-mini' - else - %span.label.label-success= t(:closed) + %span.label.label-success + %b{style: 'padding:10px'}= t(:closed) + = f.button :loading, t(:open), value: 'open', class: 'btn-mini' %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' @@ -40,9 +44,5 @@ = regarding_user_link = f.text_field :regarding_user = f.button :loading, t(:save), :value => 'save' - - if @ticket.is_open? - = f.button :loading, t(:close), :value => 'close' - - else - = f.button :loading, t(:open), :value => 'open' - if admin? = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' -- cgit v1.2.3 From f23ca91c01ce14d75c221ccddb7d8b1b7e2c0cef Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 13:17:06 +0200 Subject: make test independent of button tag input or button can be used --- test/integration/browser/account_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index a5677ad..6d5f7f9 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -131,9 +131,9 @@ class AccountTest < BrowserIntegrationTest end def assert_invalid_login(page) - assert page.has_selector? 'input.btn-primary.disabled' + assert page.has_selector? '.btn-primary.disabled' assert page.has_content? I18n.t(:invalid_user_pass) - assert page.has_no_selector? 'input.btn-primary.disabled' + assert page.has_no_selector? '.btn-primary.disabled' end def inject_malicious_js -- cgit v1.2.3 From ed3575856276f25b7c8253e302646dc7684a5da8 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 25 Apr 2014 08:09:10 +0200 Subject: use i18n keys for signup & login buttons --- config/locales/simple_form.en.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4beeb7d..5d0c675 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -26,4 +26,11 @@ en: # defaults: # username: 'User name to sign in.' # password: 'No special characters, please.' + helpers: + submit: + user: + create: "Sign up" + update: "Save" + session: + create: "Log in" -- cgit v1.2.3 From a2909d781a790e47acbdbb8b4560177100ad9942 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 25 Apr 2014 16:51:03 +0200 Subject: basic password validation without client side gem The client_side_validations gem is not maintained anymore and the validations were not working lately. So instead of trying to fix it I started working on independent validations for the password as it can't be validated on the server due to SRP. So far these validations are very primitive. They require 8 characters length and a matching confirmation. --- app/assets/javascripts/users.js | 80 +++++++++++++++++++--- .../browser/password_validation_test.rb | 31 +++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 test/integration/browser/password_validation_test.rb diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 4a86ef5..fa20399 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -1,13 +1,24 @@ (function() { + + + var settings = { + "error_class":"help-inline", + "error_tag":"span", + "wrapper_error_class":"error", + "wrapper_tag":"div", + "wrapper_class":"control-group" + } + // // LOCAL FUNCTIONS // - var poll_users, - prevent_default, - form_failed, - form_passed, + var poll_users, + prevent_default, clear_errors, + clear_field_errors, + validate_password_confirmation, + validate_password_length, update_user; prevent_default = function(event) { @@ -24,6 +35,10 @@ return $('#messages').empty(); }; + clear_field_errors = function() { + return $(settings.error_tag + '.' + settings.error_class).remove(); + }; + update_user = function(submitEvent) { var form = submitEvent.target; var token = form.dataset.token; @@ -38,7 +53,34 @@ $(form).find('.btn[type="submit"]').button('reset'); }); }; - + + validate_password_confirmation = function(submitEvent) { + var form = submitEvent.target; + var password = $(form).find('input#srp_password').val(); + var confirmation = $(form).find('input#srp_password_confirmation').val(); + if (password === confirmation) { + return true; + } + else { + displayFieldError("password_confirmation", "does not match."); + submitEvent.stopImmediatePropagation() + return false; + } + }; + + validate_password_length = function(submitEvent) { + var form = submitEvent.target; + var password = $(form).find('input#srp_password').val(); + if (password.length > 7) { + return true; + } + else { + displayFieldError("password", "needs to be at least 8 characters long."); + submitEvent.stopImmediatePropagation() + return false; + } + }; + // // PUBLIC FUNCTIONS // @@ -90,18 +132,38 @@ } function displayFieldError(field, error) { + var message = $.isArray(error) ? error[0] : error; var element = $('form input[name$="[' + field + ']"]'); if (element) { - element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false); + addError(element, settings, message); } }; - // - // INIT - // + function addError(element, settings, message) { + var errorElement, wrapper; + + errorElement = element.parent().find("" + settings.error_tag + "." + settings.error_class); + wrapper = element.closest(settings.wrapper_tag + '.' + settings.wrapper_class); + if (errorElement[0] == null) { + errorElement = $("<" + settings.error_tag + "/>", { + "class": settings.error_class, + text: message + }); + element.after(errorElement); + } + wrapper.addClass(settings.wrapper_error_class); + return errorElement.text(message); + } + +// +// INIT +// $(document).ready(function() { $('#new_user').submit(prevent_default); + $('#new_user').submit(clear_field_errors); + $('#new_user').submit(validate_password_length); + $('#new_user').submit(validate_password_confirmation); $('#new_user').submit(srp.signup); $('#new_session').submit(prevent_default); $('#new_session').submit(srp.login); diff --git a/test/integration/browser/password_validation_test.rb b/test/integration/browser/password_validation_test.rb new file mode 100644 index 0000000..45eb0bf --- /dev/null +++ b/test/integration/browser/password_validation_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' + +class PasswordValidationTest < BrowserIntegrationTest + + test "password confirmation is validated" do + username ||= "test_#{SecureRandom.urlsafe_base64}".downcase + password ||= SecureRandom.base64 + visit '/users/new' + fill_in 'Username', with: username + fill_in 'Password', with: password + fill_in 'Password confirmation', with: password + "-typo" + click_on 'Sign Up' + assert page.has_content? "does not match." + assert_equal '/users/new', current_path + assert page.has_selector? ".error #srp_password_confirmation" + end + + test "password needs to be at least 8 chars long" do + username ||= "test_#{SecureRandom.urlsafe_base64}".downcase + password ||= SecureRandom.base64[0,7] + visit '/users/new' + fill_in 'Username', with: username + fill_in 'Password', with: password + fill_in 'Password confirmation', with: password + click_on 'Sign Up' + assert page.has_content? "needs to be at least 8 characters long" + assert_equal '/users/new', current_path + assert page.has_selector? ".error #srp_password" + end +end + -- cgit v1.2.3 From c75268538a63692a371fd03a999721f252af2a98 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:23:22 +0200 Subject: simplify download button --- app/assets/stylesheets/leap.scss | 2 +- app/views/common/_download_button.html.haml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 4c0dfe3..2a0c100 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -231,13 +231,13 @@ input, textarea { .download { a.btn { width: 14em; + font-weight: bold; small { font-weight: normal; } } } a.btn { - font-weight: bold; width: 11em; margin: 10px auto; display: block; diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml index 157c40a..e57c56b 100644 --- a/app/views/common/_download_button.html.haml +++ b/app/views/common/_download_button.html.haml @@ -2,5 +2,7 @@ .row-fluid.first .span2 .download.span8 - = render partial: 'common/download_for_os', collection: available_clients + ['other'] + = link_to client_download_url, class: "btn btn-large btn-primary" do + = big_icon('download') + = t(:download_client) .span2 -- cgit v1.2.3 From 8219db0c388f8b8caf7293cbd60086a1aeef8eb4 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:31:24 +0200 Subject: smaller download button with context on dash To use bitmask services: Download Bitmask --- app/views/users/show.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3c73147..ddc33ab 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -20,6 +20,11 @@ - # %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 - = render 'common/download_button' + %h4 To use bitmask services: + = link_to client_download_url, class: "btn btn-primary" do + %i.icon-arrow-down.icon-white + = t(:download_client) -- cgit v1.2.3 From 5b3d92e40000e14c913c52227b5a717201cc5074 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:34:32 +0200 Subject: remove os specific download buttons keeping the platform detection for now. --- app/assets/stylesheets/leap.scss | 59 ----------------------------- app/views/common/_download_for_os.html.haml | 17 --------- 2 files changed, 76 deletions(-) delete mode 100644 app/views/common/_download_for_os.html.haml diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 2a0c100..d4db879 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -43,65 +43,6 @@ display: none; } -// -// OS specific -// - -.os-android { - display: none !important; -} - -html.android .os-android { - display: inherit !important; -} - -.os-linux { - display: none !important; -} - -html.linux .os-linux { - display: inherit !important; -} - -// .os-linux32 { -// display: none !important; -// } -// html.linux32 .os-linux32 { -// display: inherit !important; -// } -// .os-linux64 { -// display: none !important; -// } -// html.linux64 .os-linux64 { -// display: inherit !important; -// } - -.os-windows { - display: none !important; -} - -html.windows .os-windows { - display: inherit !important; -} - -.os-osx { - display: none !important; -} - -html.osx .os-osx { - display: inherit !important; -} - -.os-other { - display: none !important; -} - -html.oldmac, html.oldwin, html.ios, html.fxos, html.other { - .os-other { - display: inherit !important; - } -} - // // ICONS // diff --git a/app/views/common/_download_for_os.html.haml b/app/views/common/_download_for_os.html.haml deleted file mode 100644 index 3a11d10..0000000 --- a/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 -- cgit v1.2.3 From 229b93f58787d86a41cba2a894f04d5c351d7a56 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 13:56:14 +0200 Subject: adopt pricing view to current service_level format --- app/views/pages/pricing.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/pages/pricing.html.haml b/app/views/pages/pricing.html.haml index 77699d8..ce4b379 100644 --- a/app/views/pages/pricing.html.haml +++ b/app/views/pages/pricing.html.haml @@ -13,9 +13,9 @@ %td= level[:name] %td= level[:description] %td - - if level[:cost].nil? || level[:cost] == 0 + - if level[:rate].nil? || level[:rate] == 0 = t(:free) - else - = level[:cost].map{|currency,cost| "#{cost} #{currency}"}.join(', ') + = level[:rate].join(', ') - else - No service levels are configured. \ No newline at end of file + No service levels are configured. -- cgit v1.2.3 From 71814734c0a0f2e384f2e72e4d7c70aea04c01ab Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 30 Apr 2014 09:21:18 +0200 Subject: remove outdated os detection test --- test/integration/os_detection_test.rb | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 test/integration/os_detection_test.rb diff --git a/test/integration/os_detection_test.rb b/test/integration/os_detection_test.rb deleted file mode 100644 index 6d9a648..0000000 --- a/test/integration/os_detection_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'test_helper' - -class OsDetectionTest < BrowserIntegrationTest - - test "old windows shows deactivated download" do - page.driver.add_headers "User-Agent" => "Win98" - visit '/' - assert_selector "html.oldwin" - assert has_text? "not available" - end - - test "android shows android download" do - page.driver.add_headers "User-Agent" => "Android" - visit '/' - assert_selector "html.android" - assert has_no_text? "not available" - assert_selector "small", text: "Android" - end - -end -- cgit v1.2.3 From f1c45566aaa06a1a4b2d2bb1f9b5abb4ab645dab Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 30 Apr 2014 10:38:09 +0200 Subject: upgrade debugger so it works with p545 on travis --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a2a1785..c7d10db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,7 +74,7 @@ GEM coffee-script-source execjs coffee-script-source (1.6.3) - columnize (0.3.6) + columnize (0.8.9) couchrest (1.1.3) mime-types (~> 1.15) multi_json (~> 1.0) @@ -89,12 +89,12 @@ GEM couchrest couchrest_model daemons (1.1.9) - debugger (1.6.3) + debugger (1.6.6) columnize (>= 0.3.1) debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.2.4) + debugger-ruby_core_source (~> 1.3.2) debugger-linecache (1.2.0) - debugger-ruby_core_source (1.2.4) + debugger-ruby_core_source (1.3.2) erubis (2.7.0) eventmachine (1.0.3) execjs (2.0.2) -- cgit v1.2.3 From 3952a7dfcd13993bd51ac7dc07cb3a01b4658e25 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 30 Apr 2014 16:35:39 +0200 Subject: hide srp forms when no js is available Hiding them using two mechanisms in case one fails: .hidden class - bootstrap hides them then style='display:none' - so they are hidden even if css load fails --- app/assets/javascripts/users.js | 2 ++ app/helpers/users_helper.rb | 2 +- app/views/sessions/new.html.haml | 4 ++-- app/views/users/_warnings.html.haml | 2 +- app/views/users/new.html.haml | 8 +++++--- test/integration/browser/account_test.rb | 14 ++++++++++++++ test/support/browser_integration_test.rb | 4 +++- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index fa20399..e6c2fcc 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -160,6 +160,8 @@ // $(document).ready(function() { + $('.hidden.js-show').removeClass('hidden'); + $('.js-show').show(); $('#new_user').submit(prevent_default); $('#new_user').submit(clear_field_errors); $('#new_user').submit(validate_password_length); diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index f56faab..1b2dc5d 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,7 +1,7 @@ module UsersHelper def user_form_class(*classes) - (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') + (classes + ['user', 'hidden', 'js-show', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') end def wrapped(item, options = {}) diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 6f3b324..bb7e4bd 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,9 +1,9 @@ .span1 .span9 - = render :partial => 'users/warnings' %h2=t :login .lead=t :login_info - = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| + = render :partial => 'users/warnings' + = simple_form_for [:api, @session], validate: true, html: { id: :new_session, class: 'form-horizontal hidden js-show', style: "display:none;" } do |f| = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } = f.input :password, :required => false, :input_html => { :id => :srp_password } = f.button :wrapped, value: t(:login), cancel: home_path diff --git a/app/views/users/_warnings.html.haml b/app/views/users/_warnings.html.haml index 79ab103..baf80a4 100644 --- a/app/views/users/_warnings.html.haml +++ b/app/views/users/_warnings.html.haml @@ -9,4 +9,4 @@ 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/new.html.haml b/app/views/users/new.html.haml index 3478989..bf7ae31 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -1,14 +1,16 @@ -# --# This form is handled entirely by javascript, so take care when changing element ids. +-# This form is handled entirely by javascript +-# Please take care when changing element ids. +-# The form hidden when no js is available to prevent submission in the clear. -# -- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} +- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} .span1 .span9 - = render :partial => 'warnings' %h2=t :signup .lead=t :signup_info + = render :partial => 'warnings' = simple_form_for(@user, form_options) do |f| = f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username } = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password } diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index 6d5f7f9..4e11520 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -123,6 +123,20 @@ class AccountTest < BrowserIntegrationTest assert page.has_content?("server failed") end + test "does not render signup form without js" do + Capybara.current_driver = :rack_test # no js + visit '/signup' + assert page.has_no_content?("Username") + assert page.has_no_content?("Password") + end + + test "does not render login form without js" do + Capybara.current_driver = :rack_test # no js + visit '/login' + assert page.has_no_content?("Username") + assert page.has_no_content?("Password") + end + def attempt_login(username, password) click_on 'Log In' fill_in 'Username', with: username diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb index 2885c3a..9cae8cb 100644 --- a/test/support/browser_integration_test.rb +++ b/test/support/browser_integration_test.rb @@ -60,7 +60,6 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest 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 "=========================" @@ -76,6 +75,9 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest test_log.puts "------------------------" test_log.puts `tail log/test.log -n 200` end + page.save_screenshot screenshot_path + # some drivers do not support screenshots + rescue Capybara::NotSupportedByDriverError end end -- cgit v1.2.3 From 616313b406e561b45a73e34f54fd5fc7595f9658 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 6 May 2014 09:34:38 +0200 Subject: minor: fixing comment --- app/views/users/new.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index bf7ae31..bc36068 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -1,7 +1,9 @@ -# -# This form is handled entirely by javascript -# Please take care when changing element ids. --# The form hidden when no js is available to prevent submission in the clear. +-# +-# The form is hidden when no js is available +-# to prevent submission in the clear. -# - form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} -- cgit v1.2.3 From cf6aa0a6c8852424e39e4785a1bd783ab475376b Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 8 May 2014 15:12:15 -0700 Subject: minor fix to default pricing chart --- app/views/pages/pricing.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/pages/pricing.html.haml b/app/views/pages/pricing.html.haml index ce4b379..e339d27 100644 --- a/app/views/pages/pricing.html.haml +++ b/app/views/pages/pricing.html.haml @@ -16,6 +16,6 @@ - if level[:rate].nil? || level[:rate] == 0 = t(:free) - else - = level[:rate].join(', ') + = level[:rate].collect{|currency, price| "#{currency} #{price}"}.join(', ') - else No service levels are configured. -- cgit v1.2.3 From 86eb9062f1e81302647bf18ce0f5fd981202b68a Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 09:51:36 +0200 Subject: allow for usernames with dots preparing for #5664 with some test improvements i ran into this issue This commit includes a fix and the test improvements. In particular it adds BrowserIntegrationTest#login - so there is no need to go through the signup procedure everytime you want a user to be logged in. --- app/controllers/v1/sessions_controller.rb | 2 +- app/models/identity.rb | 6 ++++++ app/models/token.rb | 4 ++++ config/routes.rb | 3 ++- test/integration/browser/account_test.rb | 27 +++++++++++++++------------ test/integration/browser/session_test.rb | 20 +++++--------------- test/support/browser_integration_test.rb | 16 ++++++++++++++++ 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index eae3a1e..d88fcdc 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -38,7 +38,7 @@ module V1 def login_response handshake = session.delete(:handshake) || {} - handshake.to_hash.merge(:id => current_user.id, :token => @token.id) + handshake.to_hash.merge(:id => current_user.id, :token => @token.to_s) end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 9b97b51..ad8c01e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -70,6 +70,12 @@ class Identity < CouchRest::Model::Base end end + def self.destroy_all_for(user) + Identity.by_user_id.key(user.id).each do |identity| + identity.destroy + end + end + def self.destroy_all_disabled Identity.disabled.each do |identity| identity.destroy diff --git a/app/models/token.rb b/app/models/token.rb index 4856c31..e759ee3 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -30,6 +30,10 @@ class Token < CouchRest::Model::Base end end + def to_s + id + end + def authenticate if expired? destroy diff --git a/config/routes.rb b/config/routes.rb index f612b47..745b97d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,7 +20,8 @@ LeapWeb::Application.routes.draw do namespace "api", { module: "v1", path: "/1/", defaults: {format: 'json'} } do - resources :sessions, :only => [:new, :create, :update] + resources :sessions, :only => [:new, :create, :update], + :constraints => { :id => /[^\/]+(?=\.json\z)|[^\/]+/ } delete "logout" => "sessions#destroy", :as => "logout" resources :users, :only => [:create, :update, :destroy, :index] resources :messages, :only => [:index, :update] diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index 4e11520..491a9e1 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -6,7 +6,7 @@ class AccountTest < BrowserIntegrationTest Identity.destroy_all_disabled end - test "normal account workflow" do + test "signup successfully" do username, password = submit_signup assert page.has_content?("Welcome #{username}") click_on 'Logout' @@ -16,6 +16,12 @@ class AccountTest < BrowserIntegrationTest user.account.destroy end + test "signup with username ending in dot json" do + username = Faker::Internet.user_name + '.json' + submit_signup username + assert page.has_content?("Welcome #{username}") + end + test "successful login" do username, password = submit_signup click_on 'Logout' @@ -51,7 +57,7 @@ class AccountTest < BrowserIntegrationTest end test "default user actions" do - username, password = submit_signup + login click_on "Account Settings" assert page.has_content? I18n.t('destroy_my_account') assert page.has_no_css? '#update_login_and_password' @@ -59,8 +65,8 @@ class AccountTest < BrowserIntegrationTest end test "default admin actions" do - username, password = submit_signup - with_config admins: [username] do + login + with_config admins: [@user.login] do click_on "Account Settings" assert page.has_content? I18n.t('destroy_my_account') assert page.has_no_css? '#update_login_and_password' @@ -70,7 +76,7 @@ class AccountTest < BrowserIntegrationTest test "change password" do with_config user_actions: ['change_password'] do - username, password = submit_signup + login click_on "Account Settings" within('#update_login_and_password') do fill_in 'Password', with: "other password" @@ -78,16 +84,15 @@ class AccountTest < BrowserIntegrationTest 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 + attempt_login(@user.login, "other password") + assert page.has_content?("Welcome #{@user.login}") 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 + login click_on "Account Settings" within('#update_pgp_key') do fill_in 'Public key', with: pgp_key @@ -97,9 +102,7 @@ class AccountTest < BrowserIntegrationTest # 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 + assert_equal pgp_key, @user.reload.public_key end end diff --git a/test/integration/browser/session_test.rb b/test/integration/browser/session_test.rb index 3a41b3a..fb20847 100644 --- a/test/integration/browser/session_test.rb +++ b/test/integration/browser/session_test.rb @@ -2,26 +2,16 @@ 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}") + login + assert page.has_content?("Logout") end test "expired session" do - assert page.has_content?("Welcome #{@username}") - pretend_now_is(Time.now + 40.minutes) do + login + pretend_now_is(Time.now + 80.minutes) do visit '/' - assert page.has_no_content?("Welcome #{@username}") + assert page.has_content?("Log In") end end end diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb index 9cae8cb..836eb63 100644 --- a/test/support/browser_integration_test.rb +++ b/test/support/browser_integration_test.rb @@ -53,6 +53,22 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest return username, password end + # currently this only works for tests with poltergeist. + def login(user = nil) + user ||= @user ||= FactoryGirl.create(:user) + token = Token.create user_id: user.id + page.driver.add_header "Authorization", + 'Token token="' + token.to_s + '"' + visit '/' + end + + teardown do + if @user && @user.reload + Identity.destroy_all_for @user + @user.destroy + end + end + add_teardown_hook do |testcase| unless testcase.passed? testcase.save_state -- cgit v1.2.3 From 0261e82686ec4fcfc8b633664fadb1dd6d9c8070 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 10:52:55 +0200 Subject: keep empty email field if user removed prefill We should respect the users choice. We can still get their email from the user id if we really need to. --- app/models/service_level.rb | 8 +++++++ app/models/user.rb | 4 +++- config/defaults.yml | 5 ++++ .../support/app/controllers/tickets_controller.rb | 1 - .../support/test/integration/create_ticket_test.rb | 28 ++++++++++++++++++++++ test/factories.rb | 5 ++++ test/support/browser_integration_test.rb | 2 +- 7 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 5dd8838..a8df55b 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -24,6 +24,14 @@ class ServiceLevel end end + def provides?(service) + services.include? service + end + + def services + config_hash[:services] || [] + end + protected def limited_cert? diff --git a/app/models/user.rb b/app/models/user.rb index c297ac8..a809879 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -62,7 +62,9 @@ class User < CouchRest::Model::Base end def email_address - LocalEmail.new(login) + if effective_service_level.provides?('email') + LocalEmail.new(login) + end end # Since we are storing admins by login, we cannot allow admins to change their login. diff --git a/config/defaults.yml b/config/defaults.yml index 47cb641..1c7e694 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -54,12 +54,17 @@ service_levels: &service_levels description: "free account, with rate limited VPN" eip_rate_limit: true storage: 100 + services: + - eip 2: name: premium description: "premium account, with unlimited vpn" rate: USD: 10 EUR: 10 + services: + - eip + - email default_service_level: 1 development: diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index d552209..8ec8e4d 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -22,7 +22,6 @@ class TicketsController < ApplicationController @ticket.comments.last.posted_by = current_user.id @ticket.comments.last.private = false unless admin? @ticket.created_by = current_user.id - @ticket.email = current_user.email_address if current_user.email_address if @ticket.save flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb index 2583fc7..59b263e 100644 --- a/engines/support/test/integration/create_ticket_test.rb +++ b/engines/support/test/integration/create_ticket_test.rb @@ -25,4 +25,32 @@ class CreateTicketTest < BrowserIntegrationTest assert page.has_content?("is invalid") end + test "prefills email when user has email service" do + login FactoryGirl.create(:premium_user) + visit '/' + click_on "Support Tickets" + click_on "New Ticket" + email = "#{@user.login}@#{APP_CONFIG[:domain]}" + assert_equal email, find_field('Email').value + end + + test "no prefill of email without email service" do + login + visit '/' + click_on "Support Tickets" + click_on "New Ticket" + assert_equal "", find_field('Email').value + end + + test "cleared email field should remain clear" do + login FactoryGirl.create(:premium_user) + visit '/' + click_on "Support Tickets" + click_on "New Ticket" + fill_in 'Subject', with: 'test ticket' + fill_in 'Email', with: '' + fill_in 'Description', with: 'description of the problem goes here' + click_on 'Create Ticket' + assert_nil Ticket.last.email + end end diff --git a/test/factories.rb b/test/factories.rb index ac9333c..bebda5c 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -22,6 +22,11 @@ FactoryGirl.define do admin.stubs(:is_admin?).returns(true) end end + + factory :premium_user do + effective_service_level_code 2 + end + end factory :token do diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb index 836eb63..dbd56a9 100644 --- a/test/support/browser_integration_test.rb +++ b/test/support/browser_integration_test.rb @@ -55,7 +55,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest # currently this only works for tests with poltergeist. def login(user = nil) - user ||= @user ||= FactoryGirl.create(:user) + @user ||= user ||= FactoryGirl.create(:user) token = Token.create user_id: user.id page.driver.add_header "Authorization", 'Token token="' + token.to_s + '"' -- cgit v1.2.3 From 84ce597ad0516b92d6633c1f81c03517b5d74004 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 11:01:10 +0200 Subject: minor: use %Q for interpolated string with " --- test/support/browser_integration_test.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb index dbd56a9..1c872ff 100644 --- a/test/support/browser_integration_test.rb +++ b/test/support/browser_integration_test.rb @@ -57,8 +57,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest def login(user = nil) @user ||= user ||= FactoryGirl.create(:user) token = Token.create user_id: user.id - page.driver.add_header "Authorization", - 'Token token="' + token.to_s + '"' + page.driver.add_header "Authorization", %Q(Token token="#{token}") visit '/' end -- cgit v1.2.3 From 81a4a0527639fe4b560b8d98f977f6dbac67bb41 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 13:52:16 +0200 Subject: prefill ticket form from the model - fixes #5657 email and regarding user fields can be set to defaults based on created_by user. If these fields are emptied by the submitting user they will be set to whereas they are nil if they have not been initialized. In that case we will use meaningful defaults from the user who created the ticket. --- .../support/app/controllers/tickets_controller.rb | 5 ++--- engines/support/app/models/ticket.rb | 21 ++++++++++++--------- .../support/app/views/tickets/_edit_form.html.haml | 2 +- engines/support/app/views/tickets/new.html.haml | 4 ++-- .../support/test/integration/create_ticket_test.rb | 12 ++++++++++-- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 8ec8e4d..99357ab 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -12,6 +12,7 @@ class TicketsController < ApplicationController def new @ticket = Ticket.new + @ticket.created_by = current_user.id @ticket.comments.build end @@ -24,9 +25,7 @@ class TicketsController < ApplicationController @ticket.created_by = current_user.id if @ticket.save flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) - - # cannot set this until ticket has been saved, as @ticket.id will not be set - if !logged_in? and flash[:notice] + if !logged_in? flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) end end diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb index cd22758..d5a0b5d 100644 --- a/engines/support/app/models/ticket.rb +++ b/engines/support/app/models/ticket.rb @@ -19,8 +19,6 @@ class Ticket < CouchRest::Model::Base timestamps! - before_validation :set_email, :set_regarding_user, :on => :create - design do view :by_updated_at view :by_created_at @@ -34,7 +32,12 @@ class Ticket < CouchRest::Model::Base end validates :subject, :presence => true - validates :email, :allow_blank => true, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ + + # email can have three states: + # * nil - prefilled with created_by's email + # * "" - cleared + # * valid email address + validates :email, :allow_blank => true, :format => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/ def self.search(options = {}) @selection = TicketSelection.new(options) @@ -48,15 +51,15 @@ class Ticket < CouchRest::Model::Base end def is_creator_validated? - !!created_by + created_by_user.is_a? User end - def set_email - self.email = nil if self.email == "" + def email + read_attribute(:email) || created_by_user.email end - def set_regarding_user - self.regarding_user = nil if self.regarding_user == "" + def regarding_user + read_attribute(:regarding_user) || created_by_user.login end def close @@ -95,7 +98,7 @@ class Ticket < CouchRest::Model::Base end def created_by_user - User.find(self.created_by) + User.find(self.created_by) || AnonymousUser.new end def regarding_user_actual_user diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index bf175fe..fb279fb 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -1,6 +1,6 @@ :ruby # created by user link - if @ticket.created_by_user + if @ticket.is_creator_validated? created_by = link_to @ticket.created_by_user.login, @ticket.created_by_user else created_by = t(:anonymous) diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index ab008d2..3de5fe9 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -8,8 +8,8 @@ = simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| = hidden_ticket_fields = f.input :subject - = f.input :email, input_html: {value: user.email_address} - = f.input :regarding_user, input_html: {value: user.login} + = 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? diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb index 59b263e..0f8453c 100644 --- a/engines/support/test/integration/create_ticket_test.rb +++ b/engines/support/test/integration/create_ticket_test.rb @@ -20,18 +20,22 @@ class CreateTicketTest < BrowserIntegrationTest click_on 'Get Help' fill_in 'Subject', with: 'test ticket' fill_in 'Email', with: 'invalid data' + fill_in 'Regarding user', with: 'some user' fill_in 'Description', with: 'description of the problem goes here' click_on 'Create Ticket' assert page.has_content?("is invalid") + assert_equal 'invalid data', find_field('Email').value + assert_equal 'some user', find_field('Regarding user').value end - test "prefills email when user has email service" do + test "prefills fields" do login FactoryGirl.create(:premium_user) visit '/' click_on "Support Tickets" click_on "New Ticket" email = "#{@user.login}@#{APP_CONFIG[:domain]}" assert_equal email, find_field('Email').value + assert_equal @user.login, find_field('Regarding user').value end test "no prefill of email without email service" do @@ -40,6 +44,7 @@ class CreateTicketTest < BrowserIntegrationTest click_on "Support Tickets" click_on "New Ticket" assert_equal "", find_field('Email').value + assert_equal @user.login, find_field('Regarding user').value end test "cleared email field should remain clear" do @@ -51,6 +56,9 @@ class CreateTicketTest < BrowserIntegrationTest fill_in 'Email', with: '' fill_in 'Description', with: 'description of the problem goes here' click_on 'Create Ticket' - assert_nil Ticket.last.email + ticket = Ticket.last + assert_equal "", ticket.email + ticket.destroy end + end -- cgit v1.2.3 From bbe9de73352b5aa937173b4158267f6a37e9ca5f Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 14:03:53 +0200 Subject: destinguish user.email from user.email_address use the former if you want a working email account or nil, the latter if you want the email address associated with a given user no matter if the user actually has an email account or not. --- app/models/anonymous_user.rb | 4 ++++ app/models/user.rb | 11 +++++++++-- test/unit/account_test.rb | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb index 360a577..87239eb 100644 --- a/app/models/anonymous_user.rb +++ b/app/models/anonymous_user.rb @@ -13,6 +13,10 @@ class AnonymousUser < Object nil end + def email + nil + end + def email_address nil end diff --git a/app/models/user.rb b/app/models/user.rb index a809879..6678de6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -61,12 +61,19 @@ class User < CouchRest::Model::Base login end - def email_address + # use this if you want to get a working email address only. + def email if effective_service_level.provides?('email') - LocalEmail.new(login) + email_address end end + # use this if you want the email address associated with a + # user no matter if the user actually has a local email account + 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 diff --git a/test/unit/account_test.rb b/test/unit/account_test.rb index 4fb3c3d..b2bfe27 100644 --- a/test/unit/account_test.rb +++ b/test/unit/account_test.rb @@ -8,7 +8,7 @@ class AccountTest < ActiveSupport::TestCase test "create a new account" do user = Account.create(FactoryGirl.attributes_for(:user)) - assert user.valid? + assert user.valid?, "unexpected errors: #{user.errors.inspect}" assert user.persisted? assert id = user.identity assert_equal user.email_address, id.address -- cgit v1.2.3 From 503fa755ee7f5ec52e833a05fb640b1c16e89a5f Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 14:45:49 +0200 Subject: move User Control Panel heading out of masthead the masthead can only handle content of a limited width. Alternatively we could make the masthead title wider and add padding on the left so it still aligns nicely with the mask. However the wider we make it the worse it looks on small width displays. Another option would be to make the masthead contain multiple lines. However vertical aligment to the middle of 1 OR 2 lines of text is a real pain. So I went with a super simple masthead and the Caption goes below. --- app/views/layouts/_header.html.haml | 2 +- app/views/layouts/_masthead.html.haml | 2 -- app/views/layouts/application.html.haml | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 157f1df..a1dd47a 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -8,5 +8,5 @@ %li = link_to t(:logout), logout_path, :method => :delete - if @user && @show_navigation - .user_heading + .lead = @user.email_address diff --git a/app/views/layouts/_masthead.html.haml b/app/views/layouts/_masthead.html.haml index 35225a1..fde5915 100644 --- a/app/views/layouts/_masthead.html.haml +++ b/app/views/layouts/_masthead.html.haml @@ -2,5 +2,3 @@ .title %span.sitename %a{:href => home_path}= APP_CONFIG[:domain] - - if @show_navigation - = t(:user_control_panel) \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 1cd4ec3..d213fe1 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -14,6 +14,9 @@ = render 'layouts/masthead' #main .container-fluid + - if @show_navigation + .row-fluid + %h1= t(:user_control_panel) - if logged_in? .row-fluid .span12 -- cgit v1.2.3 From 41055098941481611d4b9fa80c769092ed9bd79d Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 17:10:33 +0200 Subject: open/close toggle and fields in different forms - fixes #5659 --- engines/support/app/views/tickets/_edit_form.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index bf175fe..3675822 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -29,6 +29,8 @@ %b{style: 'padding:10px'}= t(:closed) = f.button :loading, t(:open), value: 'open', class: 'btn-mini' %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe += simple_form_for @ticket do |f| + = hidden_ticket_fields %div= t(:subject) = f.text_field :subject, :class => 'large full-width' .row-fluid -- cgit v1.2.3 From 3278e474a32ef4926b1dab0d97ca4df1c59aa2a0 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 17:21:08 +0200 Subject: adjust tests to new config and method implementation Ticket.is_creator_vlidated? now actually fetches the user from the db and returns false if it does not exist. --- engines/support/app/models/ticket.rb | 6 +++++- engines/support/test/functional/tickets_controller_test.rb | 4 ++-- engines/support/test/unit/ticket_test.rb | 4 ++-- test/functional/v1/services_controller_test.rb | 6 ++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb index d5a0b5d..bf5df53 100644 --- a/engines/support/app/models/ticket.rb +++ b/engines/support/app/models/ticket.rb @@ -98,7 +98,11 @@ class Ticket < CouchRest::Model::Base end def created_by_user - User.find(self.created_by) || AnonymousUser.new + if self.created_by + User.find(self.created_by) || AnonymousUser.new + else + AnonymousUser.new + end end def regarding_user_actual_user diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index d746b59..fc4a6f8 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -85,7 +85,7 @@ class TicketsControllerTest < ActionController::TestCase test "should create authenticated ticket" do - params = {:subject => "auth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} + params = {:subject => "auth ticket test subject",:email => "", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} login @@ -97,7 +97,7 @@ class TicketsControllerTest < ActionController::TestCase 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 "", assigns(:ticket).email assert_equal 1, assigns(:ticket).comments.count assert_not_nil assigns(:ticket).comments.first.posted_by diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb index f5e6ea7..678d8dc 100644 --- a/engines/support/test/unit/ticket_test.rb +++ b/engines/support/test/unit/ticket_test.rb @@ -27,10 +27,10 @@ class TicketTest < ActiveSupport::TestCase end test "creation validated" do + user = FactoryGirl.create :user @sample = Ticket.new assert !@sample.is_creator_validated? - #p current_user - @sample.created_by = 22 #current_user + @sample.created_by = user.id assert @sample.is_creator_validated? end diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/v1/services_controller_test.rb index e4058c0..cde7d9f 100644 --- a/test/functional/v1/services_controller_test.rb +++ b/test/functional/v1/services_controller_test.rb @@ -21,10 +21,8 @@ class V1::ServicesControllerTest < ActionController::TestCase test "user can see their service info" do login get :show, format: :json - assert_json_response name: 'free', - eip_rate_limit: true, - description: 'free account, with rate limited VPN', - storage: 100 + default_level = APP_CONFIG[:default_service_level] + assert_json_response APP_CONFIG[:service_levels][default_level] end end -- cgit v1.2.3 From eb74538841c6cf687b87f5373b90a22847e974fa Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 14 May 2014 16:46:38 +0200 Subject: make download button wide enough for helvetica #5039 --- app/assets/stylesheets/leap.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index d4db879..77104e5 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -171,7 +171,7 @@ input, textarea { } .download { a.btn { - width: 14em; + width: 15em; font-weight: bold; small { font-weight: normal; -- cgit v1.2.3 From 6a998514be4d8a1eb2252870946ace1b7c354586 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 14 May 2014 16:40:05 +0200 Subject: upgrade rails to 3.2.18 for security fixes --- Gemfile | 2 +- Gemfile.lock | 62 ++++++++++++++++++++++++++++++------------------------------ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Gemfile b/Gemfile index b8b4568..816cc4a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem "rails", "~> 3.2.11" +gem "rails", "~> 3.2.18" gem "couchrest", "~> 1.1.3" gem "couchrest_model", "~> 2.0.0" gem "couchrest_session_store", "~> 0.2.4" diff --git a/Gemfile.lock b/Gemfile.lock index c7d10db..67c04de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,12 +20,12 @@ GEM remote: https://rubygems.org/ specs: SyslogLogger (2.0) - actionmailer (3.2.16) - actionpack (= 3.2.16) + actionmailer (3.2.18) + actionpack (= 3.2.18) mail (~> 2.5.4) - actionpack (3.2.16) - activemodel (= 3.2.16) - activesupport (= 3.2.16) + actionpack (3.2.18) + activemodel (= 3.2.18) + activesupport (= 3.2.18) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -33,18 +33,18 @@ GEM rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) - activemodel (3.2.16) - activesupport (= 3.2.16) + activemodel (3.2.18) + activesupport (= 3.2.18) builder (~> 3.0.0) - activerecord (3.2.16) - activemodel (= 3.2.16) - activesupport (= 3.2.16) + activerecord (3.2.18) + activemodel (= 3.2.18) + activesupport (= 3.2.18) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.16) - activemodel (= 3.2.16) - activesupport (= 3.2.16) - activesupport (3.2.16) + activeresource (3.2.18) + activemodel (= 3.2.18) + activesupport (= 3.2.18) + activesupport (3.2.18) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) addressable (2.3.5) @@ -143,7 +143,7 @@ GEM minitest-stub-const (0.1) mocha (0.13.3) metaclass (~> 0.0.1) - multi_json (1.8.2) + multi_json (1.10.0) nokogiri (1.6.1) mini_portile (~> 0.5.0) phantomjs-binaries (1.9.2.3) @@ -153,7 +153,7 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyglot (0.3.3) + polyglot (0.3.4) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) rack (1.4.5) @@ -161,31 +161,31 @@ GEM rack (>= 0.4) rack-protection (1.5.1) rack - rack-ssl (1.3.3) + rack-ssl (1.3.4) rack rack-test (0.6.2) rack (>= 1.0) - rails (3.2.16) - actionmailer (= 3.2.16) - actionpack (= 3.2.16) - activerecord (= 3.2.16) - activeresource (= 3.2.16) - activesupport (= 3.2.16) + rails (3.2.18) + actionmailer (= 3.2.18) + actionpack (= 3.2.18) + activerecord (= 3.2.18) + activeresource (= 3.2.18) + activesupport (= 3.2.18) bundler (~> 1.0) - railties (= 3.2.16) + railties (= 3.2.18) rails-i18n (3.0.0) i18n (~> 0.5) rails (>= 3.0.0, < 4.0.0) rails_warden (0.5.7) warden (>= 1.0.0) - railties (3.2.16) - actionpack (= 3.2.16) - activesupport (= 3.2.16) + railties (3.2.18) + actionpack (= 3.2.18) + activesupport (= 3.2.18) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - rake (10.1.1) + rake (10.3.1) rdiscount (2.0.7) rdoc (3.12.2) json (~> 1.4) @@ -217,12 +217,12 @@ GEM daemons (>= 1.0.9) eventmachine (>= 1.0.0) rack (>= 1.0.0) - thor (0.18.1) + thor (0.19.1) tilt (1.4.1) treetop (1.4.15) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.38) + tzinfo (0.3.39) uglifier (1.2.7) execjs (>= 0.3.0) multi_json (~> 1.3) @@ -265,7 +265,7 @@ DEPENDENCIES phantomjs-binaries poltergeist quiet_assets - rails (~> 3.2.11) + rails (~> 3.2.18) rails-i18n rails_warden rdiscount -- cgit v1.2.3 From 702e444e57d7904e429d1628e600d11e2e793a62 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 14 May 2014 19:47:26 +0200 Subject: check for presence of service_levels This way the pricing link will not be shown for an empty hash. Which is easier to get into the config file than nil. And we can later verify the configuration to have a hash as the service_levels. --- app/views/layouts/_footer.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 5909bdd..340d36c 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -6,5 +6,5 @@ = link_to icon('info-sign') + t(:about), about_path - if lookup_context.exists?('pages/contact') = link_to icon('comment') + t(:contact), contact_path - - if APP_CONFIG[:service_levels] - = link_to icon('shopping-cart') + t(:pricing), pricing_path \ No newline at end of file + - if APP_CONFIG[:service_levels].present? + = link_to icon('shopping-cart') + t(:pricing), pricing_path -- cgit v1.2.3