diff options
Diffstat (limited to 'test')
50 files changed, 2384 insertions, 2 deletions
diff --git a/test/factories.rb b/test/factories.rb index 6c671f8..ac9333c 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -1,3 +1,39 @@ -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 + 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/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/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/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/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..26adc8c --- /dev/null +++ b/test/integration/api/srp_test.rb @@ -0,0 +1,105 @@ +class SrpTest < RackTest + include AssertResponses + + 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/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/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/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..806339a --- /dev/null +++ b/test/support/rack_test.rb @@ -0,0 +1,38 @@ +require_relative 'assert_responses' + +class RackTest < ActiveSupport::TestCase + include Rack::Test::Methods + include Warden::Test::Helpers + + 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/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 <<self + attr_accessor :testing_offset + + def now_with_testing_offset + now_without_testing_offset - testing_offset + end + alias_method_chain :now, :testing_offset + end +end +Time.testing_offset = 0 + +module TimeTestHelper + # Time warp to the specified time for the duration of the passed block + def pretend_now_is(time) + begin + Time.testing_offset = Time.now - time + yield + ensure + Time.testing_offset = 0 + end + end +end + +class ActiveSupport::TestCase + include TimeTestHelper +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/test/test_helper.rb b/test/test_helper.rb index f63591f..d001ac7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,8 +4,11 @@ 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 } +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... diff --git a/test/unit/account_test.rb b/test/unit/account_test.rb new file mode 100644 index 0000000..4fb3c3d --- /dev/null +++ b/test/unit/account_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +class AccountTest < ActiveSupport::TestCase + + teardown do + Identity.destroy_all_disabled + end + + test "create a new account" do + user = Account.create(FactoryGirl.attributes_for(:user)) + assert user.valid? + assert user.persisted? + assert id = user.identity + assert_equal user.email_address, id.address + assert_equal user.email_address, id.destination + user.account.destroy + end + + test "create and remove a user account" do + # We keep an identity that will block the handle from being reused. + assert_difference "Identity.count" do + assert_no_difference "User.count" do + user = Account.create(FactoryGirl.attributes_for(:user)) + user.account.destroy + end + end + end + + test "change username and create alias" do + user = Account.create(FactoryGirl.attributes_for(:user)) + old_id = user.identity + old_email = user.email_address + user.account.update(FactoryGirl.attributes_for(:user)) + user.reload + old_id.reload + assert user.valid? + assert user.persisted? + assert id = user.identity + assert id.persisted? + assert_equal user.email_address, id.address + assert_equal user.email_address, id.destination + assert_equal user.email_address, old_id.destination + assert_equal old_email, old_id.address + user.account.destroy + 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 diff --git a/test/unit/helpers/session_helper_test.rb b/test/unit/helpers/session_helper_test.rb new file mode 100644 index 0000000..2824733 --- /dev/null +++ b/test/unit/helpers/session_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class SessionHelperTest < ActionView::TestCase +end diff --git a/test/unit/helpers/users_helper_test.rb b/test/unit/helpers/users_helper_test.rb new file mode 100644 index 0000000..96af37a --- /dev/null +++ b/test/unit/helpers/users_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class UsersHelperTest < ActionView::TestCase +end diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb new file mode 100644 index 0000000..eca104f --- /dev/null +++ b/test/unit/identity_test.rb @@ -0,0 +1,133 @@ +require 'test_helper' + +class IdentityTest < ActiveSupport::TestCase + include StubRecordHelper + + setup do + @user = find_record :user + end + + test "initial identity for a user" do + id = Identity.for(@user) + assert_equal @user.email_address, id.address + assert_equal @user.email_address, id.destination + assert_equal @user, id.user + end + + test "add alias" do + id = Identity.for @user, address: alias_name + assert_equal LocalEmail.new(alias_name), id.address + assert_equal @user.email_address, id.destination + assert_equal @user, id.user + end + + test "add forward" do + id = Identity.for @user, destination: forward_address + assert_equal @user.email_address, id.address + assert_equal Email.new(forward_address), id.destination + assert_equal @user, id.user + end + + test "forward alias" do + id = Identity.for @user, address: alias_name, destination: forward_address + assert_equal LocalEmail.new(alias_name), id.address + assert_equal Email.new(forward_address), id.destination + assert_equal @user, id.user + end + + test "prevents duplicates" do + id = Identity.create_for @user, address: alias_name, destination: forward_address + dup = Identity.build_for @user, address: alias_name, destination: forward_address + assert !dup.valid? + assert_equal ["This alias already exists"], dup.errors[:base] + id.destroy + end + + test "validates availability" do + other_user = find_record :user + id = Identity.create_for @user, address: alias_name, destination: forward_address + taken = Identity.build_for other_user, address: alias_name + assert !taken.valid? + assert_equal ["This email has already been taken"], taken.errors[:base] + id.destroy + end + + test "setting and getting pgp key" do + id = Identity.for(@user) + id.set_key(:pgp, pgp_key_string) + assert_equal pgp_key_string, id.keys[:pgp] + end + + test "querying pgp key via couch" do + id = Identity.for(@user) + id.set_key(:pgp, pgp_key_string) + id.save + view = Identity.pgp_key_by_email.key(id.address) + assert_equal 1, view.rows.count + assert result = view.rows.first + assert_equal id.address, result["key"] + assert_equal id.keys[:pgp], result["value"] + id.destroy + end + + test "fail to add non-local email address as identity address" do + id = Identity.for @user, address: forward_address + assert !id.valid? + assert_match /needs to end in/, id.errors[:address].first + end + + test "alias must meet same conditions as login" do + id = Identity.create_for @user, address: alias_name.capitalize + assert !id.valid? + #hacky way to do this, but okay for now: + assert id.errors.messages.flatten(2).include? "Must begin with a lowercase letter" + assert id.errors.messages.flatten(2).include? "Only lowercase letters, digits, . - and _ allowed." + end + + test "destination must be valid email address" do + id = Identity.create_for @user, address: @user.email_address, destination: 'ASKJDLFJD' + assert !id.valid? + assert id.errors.messages[:destination].include? "needs to be a valid email address" + end + + test "disabled identity" do + id = Identity.for(@user) + id.disable + assert_equal @user.email_address, id.address + assert_equal nil, id.destination + assert_equal nil, id.user + assert !id.enabled? + assert id.valid? + end + + test "disabled identity blocks handle" do + id = Identity.for(@user) + id.disable + id.save + other_user = find_record :user + taken = Identity.build_for other_user, address: id.address + assert !taken.valid? + Identity.destroy_all_disabled + end + + test "destroy all disabled identities" do + id = Identity.for(@user) + id.disable + id.save + assert Identity.count > 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 |