diff --git a/Gemfile b/Gemfile index 5be6fca..d21c87c 100644 --- a/Gemfile +++ b/Gemfile @@ -14,14 +14,16 @@ gem "sprockets-rails" # Start debugger with binding.b [https://github.com/ruby/debug] # gem "debug", ">= 1.0.0" -# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] -gem "rubocop-rails-omakase", require: false group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem # gem "brakeman", "~> 6.1" # gem "debug", platforms: %i[mri windows] + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + gem 'rails-controller-testing' + # gem "standard", require: false # gem "erb_lint", require: false # gem "letter_opener_web", "~> 2.0" diff --git a/app/views/kiqr/sessions/new.html.erb b/app/views/kiqr/sessions/new.html.erb new file mode 100644 index 0000000..e31cc21 --- /dev/null +++ b/app/views/kiqr/sessions/new.html.erb @@ -0,0 +1,36 @@ +<% title "Sign in to your account" %> + +

<%= t(".title", app_name: Kiqr.config.app_name) %>

+

<%= t(".description") %>

+ + +<%#= render "partials/social_accounts_login" %> + +<%= form_with model: resource, url: session_path(resource_name), local: true do |f| %> +
+ <%= f.label :email, t(".email.placeholder") %> + <%= f.email_field :email, placeholder: t(".email.placeholder"), required: true, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password, t(".password.placeholder") %> + <%= f.password_field :password, placeholder: t(".password.placeholder"), required: true, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me, t(".remember_me.label") %> +
+ <% end %> + +
+ <%= f.submit t(".submit_button") %> +
+ +
+

<%= t(".no_account") %> + <%= link_to t(".sign_up"), new_user_registration_path %> +

+
+<% end %> diff --git a/bin/rubocop b/bin/rubocop deleted file mode 100755 index 40330c0..0000000 --- a/bin/rubocop +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -require "rubygems" -require "bundler/setup" - -# explicit rubocop config increases performance slightly while avoiding config confusion. -ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) - -load Gem.bin_path("rubocop", "rubocop") diff --git a/config/locales/kiqr/en.yml b/config/locales/kiqr/en.yml index 2a487e7..37f4eac 100644 --- a/config/locales/kiqr/en.yml +++ b/config/locales/kiqr/en.yml @@ -157,17 +157,15 @@ en: confirmation_message: "Are you sure? This will DELETE all of your data." sessions: new: - heading: - title: "Sign in to your %{app_name} account" - description: "Enter with your email address and password or select any of the other available options below." - form: - email: - placeholder: "Enter your email" - password: - placeholder: "Enter your password" - remember_me: - label: "Remember me" - submit_button: "Sign in" + title: "Sign in to your %{app_name} account" + description: "Enter with your email address and password or select any of the other available options below." + email: + placeholder: "Enter your email" + password: + placeholder: "Enter your password" + remember_me: + label: "Remember me" + submit_button: "Sign in" no_account: "Don't have an account?" sign_up: "Sign up!" otp: diff --git a/test/controllers/kiqr/account_users_controller_test.rb b/test/controllers/kiqr/account_users_controller_test.rb new file mode 100644 index 0000000..abc8e51 --- /dev/null +++ b/test/controllers/kiqr/account_users_controller_test.rb @@ -0,0 +1,57 @@ +require "test_helper" + +class Kiqr::AccountUsersControllerTest < ActionDispatch::IntegrationTest + test "can't show members as personal account" do + user = create(:user) + sign_in user + get account_users_path + + assert_redirected_to edit_account_path(account_id: nil) + end + + test "can show edit member page" do + user = create(:user) + some_user = create(:user) + + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user: user, owner: true) + account.account_users << AccountUser.create(user: some_user) + + sign_in user + get edit_account_user_path(account_id: account, id: some_user.account_users.first) + + assert_response :success + end + + test "can show members as team account" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + get account_users_path(account_id: account) + + assert_response :success + end + + test "can remove a member from team" do + user = create(:user) + some_user = create(:user) + + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user: user, owner: true) + account.account_users << AccountUser.create(user: some_user) + + assert_includes account.reload.users, some_user + + sign_in user + delete account_user_path(account_id: account, id: some_user.account_users.first) + + assert_redirected_to account_users_path(account_id: account) + assert_not_includes account.reload.users, some_user + end + + test "can not remove the team owner" do + skip "This test is not implemented yet" + end +end diff --git a/test/controllers/kiqr/accounts/invitations_controller_test.rb b/test/controllers/kiqr/accounts/invitations_controller_test.rb new file mode 100644 index 0000000..dd794a3 --- /dev/null +++ b/test/controllers/kiqr/accounts/invitations_controller_test.rb @@ -0,0 +1,60 @@ +require "test_helper" + +class InvitationsControllerTest < ActionDispatch::IntegrationTest + test "can invite a user" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + + assert_difference -> { account.account_invitations.count } do + post account_invitations_path(account_id: account), params: { account_invitation: { email: "foobar@agag.com" } } + end + + assert_redirected_to account_invitations_path(account_id: account) + end + + test "can't invite a user to someone else team" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + foreign_account = create(:account, name: "Foreign account") + + sign_in user + + assert_raises(PublicUid::RecordNotFound) do + post account_invitations_path(account_id: foreign_account), params: { account_invitation: { email: "foobar@agag.com" } } + end + end + + test "can only invite the same user once" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + + post account_invitations_path(account_id: account), params: { account_invitation: { email: "foobar@foobar.com" } } + assert_redirected_to account_invitations_path(account_id: account) + + assert_no_difference -> { account.account_invitations.count } do + post account_invitations_path(account_id: account), params: { account_invitation: { email: "foobar@foobar.com" } } + end + end + + test "invitations validates email" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + + assert_no_difference -> { account.account_invitations.count } do + post account_invitations_path(account_id: account), params: { account_invitation: { email: "foo" } } + end + + assert_response :unprocessable_entity + end +end diff --git a/test/controllers/kiqr/accounts_controller_test.rb b/test/controllers/kiqr/accounts_controller_test.rb new file mode 100644 index 0000000..c071e91 --- /dev/null +++ b/test/controllers/kiqr/accounts_controller_test.rb @@ -0,0 +1,59 @@ +require "test_helper" + +class Kiqr::AccountsControllerTest < ActionDispatch::IntegrationTest + test "can render new account page" do + sign_in create(:user) + get new_account_path + assert_response :success + end + + test "redirect to profile edit if personal account" do + sign_in create(:user) + get edit_account_path + assert_redirected_to edit_settings_path + end + + test "can create new team account" do + user = create(:user) + sign_in user + + post accounts_path, params: { account: { name: "Foobar team" } } + new_team = Account.find_by(name: "Foobar team") + + assert_redirected_to dashboard_path(account_id: Account.last) + assert user.reload.accounts.include?(new_team) + end + + test "shows error on invalid team account creation" do + user = create(:user) + sign_in user + + post accounts_path, params: { account: { name: "no" } } + assert_response :unprocessable_entity + # assert_template :new + end + + test "can update team accounts" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + patch account_path(account_id: account), params: { account: { name: "New company name" } } + account.reload + + assert_redirected_to edit_account_path + assert_equal "New company name", account.name + end + + test "re-renders form if form data is invalid" do + user = create(:user) + account = create(:account, name: "Team account") + account.account_users << AccountUser.create(user:, owner: true) + + sign_in user + + patch account_path(account_id: account), params: { account: { name: "no" } } + assert_response :unprocessable_entity + end +end diff --git a/test/controllers/kiqr/dashboard_controller_test.rb b/test/controllers/kiqr/dashboard_controller_test.rb new file mode 100644 index 0000000..d14faa1 --- /dev/null +++ b/test/controllers/kiqr/dashboard_controller_test.rb @@ -0,0 +1,14 @@ +require "test_helper" + +class Kiqr::DashboardControllerTest < ActionDispatch::IntegrationTest + test "should redirect unauthenticated users to sign in form" do + get dashboard_path + assert_redirected_to new_user_session_path + end + + test "should allow authenticated users to visit" do + sign_in create(:user) + get dashboard_path + assert_response :success + end +end diff --git a/test/controllers/kiqr/invitations_controller_test.rb b/test/controllers/kiqr/invitations_controller_test.rb new file mode 100644 index 0000000..a3b3e01 --- /dev/null +++ b/test/controllers/kiqr/invitations_controller_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +class Kiqr::InvitationsControllerTest < ActionDispatch::IntegrationTest + test "can view page without beeing signed in" do + invitation = create(:account_invitation) + get invitation_path(invitation) + assert_response :success + end + + test "can accept invitation" do + user = create(:user) + invitation = create(:account_invitation) + sign_in user + + assert_difference -> { user.accounts.count } do + post accept_invitation_path(invitation) + end + + assert_redirected_to dashboard_path(account_id: invitation.account) + assert_not_nil invitation.reload.accepted_at + end + + test "cant be accepted twice" do + user = create(:user) + invitation = create(:account_invitation, accepted_at: Time.now) + sign_in user + + get invitation_path(invitation) + assert_redirected_to dashboard_path + + assert_no_difference -> { user.accounts.count } do + post accept_invitation_path(invitation) + end + end + + test "can reject invitation" do + user = create(:user) + invitation = create(:account_invitation) + sign_in user + + post reject_invitation_path(invitation) + + assert_redirected_to dashboard_path + assert_nil AccountInvitation.find_by(id: invitation.id) + end +end diff --git a/test/controllers/kiqr/onboarding_controller_test.rb b/test/controllers/kiqr/onboarding_controller_test.rb new file mode 100644 index 0000000..7fa578f --- /dev/null +++ b/test/controllers/kiqr/onboarding_controller_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +class Kiqr::OnboardingControllerTest < ActionDispatch::IntegrationTest + test "redirects to dashboard_path if user is already onboarded" do + sign_in create(:user) + get onboarding_path + assert_redirected_to dashboard_path + end + + test "renders the onboarding form if user is not onboarded" do + sign_in create(:user, personal_account: nil) + get onboarding_path + assert_response :success + end + + test "can onboard user" do + user = create(:user, personal_account: nil) + sign_in user + post onboarding_path, params: { user: { personal_account_attributes: { name: "Foobar zoo" } } } + assert_redirected_to dashboard_path + assert user.reload.personal_account.personal? + end + + test "validates user onboarding" do + sign_in create(:user, personal_account: nil) + post onboarding_path, params: { user: { personal_account_attributes: { name: "no" } } } + assert_response :unprocessable_entity + assert_template :new + end +end diff --git a/test/controllers/kiqr/registrations_controller_test.rb b/test/controllers/kiqr/registrations_controller_test.rb new file mode 100644 index 0000000..3e9c50d --- /dev/null +++ b/test/controllers/kiqr/registrations_controller_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +class Kiqr::RegistrationControllerTest < ActionDispatch::IntegrationTest + test "can view cancellation page" do + sign_in create(:user) + get cancel_user_registration_path + assert_response :success + end + + test "can delete a user without teams" do + user = create(:user) + sign_in user + delete user_registration_path + + assert_redirected_to root_path + assert_nil User.find_by(id: user.id) + end + + test "can't delete a user with owned teams" do + user = create(:user) + team_account = create(:account, name: "Team account") + team_account.account_users << AccountUser.create(user: user, owner: true) + + sign_in user + delete user_registration_path + + assert_redirected_to cancel_user_registration_path + assert User.find_by(id: user.id) + end +end diff --git a/test/controllers/kiqr/sessions_controller_test.rb b/test/controllers/kiqr/sessions_controller_test.rb new file mode 100644 index 0000000..75f587a --- /dev/null +++ b/test/controllers/kiqr/sessions_controller_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +class Kiqr::SessionsControllerTest < ActionDispatch::IntegrationTest + test "can sign in if two factor is disabled" do + user = create(:user) + post user_session_path, params: { user: { email: user.email, password: user.password } } + assert_response :redirect + assert_redirected_to dashboard_path + end + + test "can sign in with otp if two factor is enabled" do + user = create(:user, :otp_enabled) + post user_session_path, params: { user: { email: user.email, password: user.password } } + assert_response :unprocessable_entity + assert_template "kiqr/sessions/otp" + + post user_session_path, params: { user: { otp_attempt: user.current_otp } } + assert_redirected_to dashboard_path + end + + test "can't sign in with invalid otp if two factor is enabled" do + user = create(:user, :otp_enabled) + post user_session_path, params: { user: { email: user.email, password: user.password } } + post user_session_path, params: { user: { otp_attempt: "123456" } } + assert_response :unprocessable_entity + assert_template "kiqr/sessions/otp" + end + + test "renders form again if invalid email" do + post user_session_path, params: { user: { email: "unknown.email", password: "randompassword" } } + assert_response :unprocessable_entity + assert_template "kiqr/sessions/new" + end + + test "renders form again if invalid password" do + user = create(:user) + post user_session_path, params: { user: { email: user.email, password: "randompassword" } } + assert_response :unprocessable_entity + assert_template "kiqr/sessions/new" + end +end diff --git a/test/controllers/kiqr/settings_controller_test.rb b/test/controllers/kiqr/settings_controller_test.rb new file mode 100644 index 0000000..0ccfee7 --- /dev/null +++ b/test/controllers/kiqr/settings_controller_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +class Kiqr::PreferencesControllerTest < ActionDispatch::IntegrationTest + test "should get edit page" do + user = create(:user) + sign_in(user) + get edit_settings_path + assert_response :success + end + + test "can update user fields" do + user = create(:user, time_zone: "UTC", locale: "en") + sign_in(user) + + patch settings_path, params: { user: { time_zone: "Stockholm", locale: "sv" } } + assert_redirected_to edit_settings_path + assert_equal "Stockholm", user.reload.time_zone + assert_equal "sv", user.reload.locale + end + + test "can update personal account fields" do + user = create(:user) + sign_in user + + patch settings_path, params: { user: { personal_account_attributes: { name: "Personal account name" } } } + user.reload + + assert_redirected_to edit_settings_path + assert_equal "Personal account name", user.personal_account.name + end +end diff --git a/test/controllers/kiqr/two_factor_controller_test.rb b/test/controllers/kiqr/two_factor_controller_test.rb new file mode 100644 index 0000000..81845e8 --- /dev/null +++ b/test/controllers/kiqr/two_factor_controller_test.rb @@ -0,0 +1,65 @@ +require "test_helper" + +class Kiqr::TwoFactorControllerTest < ActionDispatch::IntegrationTest + test "should not be able to setup 2fa if already enabled" do + sign_in create(:user, :otp_enabled) + get setup_two_factor_path + assert_redirected_to edit_two_factor_path + end + + test "secret code is refreshed on new setup" do + user = create(:user) + sign_in user + get new_two_factor_path + + user.reload + assert user.otp_secret + + get new_two_factor_path + assert_not_equal user.otp_secret, user.reload.otp_secret + end + + test "can view setup page" do + sign_in create(:user) + get setup_two_factor_path + assert_response :success + end + + test "redirects to show page if two factor is alredy disabled" do + user = create(:user) + sign_in user + get disable_two_factor_path + assert_redirected_to edit_two_factor_path + + delete destroy_two_factor_path + assert_redirected_to edit_two_factor_path + end + + test "does not activate 2fa with invalid verification code" do + user = create(:user, otp_secret: User.generate_otp_secret) + sign_in user + post verify_two_factor_path, params: { user: { otp_attempt: "123456" } } + assert_response :unprocessable_entity + end + + test "activates 2fa with valid verification code" do + user = create(:user, otp_secret: User.generate_otp_secret) + sign_in user + post verify_two_factor_path, params: { user: { otp_attempt: user.current_otp } } + assert_redirected_to edit_two_factor_path + end + + test "requires valid otp code to disable two factor authentication" do + user = create(:user, :otp_enabled) + sign_in user + + delete destroy_two_factor_path, params: { user: { otp_attempt: "123456" } } + assert_response :unprocessable_entity + assert_template "two_factor/disable" + assert user.reload.otp_required_for_login? + + delete destroy_two_factor_path, params: { user: { otp_attempt: user.current_otp } } + assert_redirected_to edit_two_factor_path + assert_not user.reload.otp_required_for_login? + end +end diff --git a/test/factories/user.rb b/test/factories/user.rb index 883a1f3..bcb03ff 100644 --- a/test/factories/user.rb +++ b/test/factories/user.rb @@ -16,13 +16,14 @@ user.account_users.create(account: evaluator.with_account, owner: true) end end - # trait :unconfirmed do - # confirmed_at { nil } - # end - # trait :otp_enabled do - # otp_required_for_login { true } - # otp_secret { User.generate_otp_secret } - # end + trait :unconfirmed do + confirmed_at { nil } + end + + trait :otp_enabled do + otp_required_for_login { true } + otp_secret { User.generate_otp_secret } + end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9391124..fce43de 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,7 +18,7 @@ module ActiveSupport class TestCase - # include Devise::Test::IntegrationHelpers + include Devise::Test::IntegrationHelpers include FactoryBot::Syntax::Methods # Run tests in parallel with specified workers