diff --git a/lib/generators/authentication/authentication_generator.rb b/lib/generators/authentication/authentication_generator.rb index c9953a3..cdad1b7 100644 --- a/lib/generators/authentication/authentication_generator.rb +++ b/lib/generators/authentication/authentication_generator.rb @@ -90,7 +90,12 @@ def create_models end def create_fixture_file - copy_file "test_unit/users.yml", "test/fixtures/users.yml" + case test_framework + when :test_unit + copy_file "test_unit/users.yml", "test/fixtures/users.yml" + when :rspec + copy_file "rspec/users.yml", "spec/fixtures/users.yml" + end end def create_controllers @@ -220,11 +225,21 @@ def add_routes end def create_test_files - directory "test_unit/controllers/#{format}", "test/controllers" - directory "test_unit/mailers/", "test/mailers" - directory "test_unit/system", "test/system" unless options.api? - template "test_unit/test_helper.rb", "test/test_helper.rb", force: true - template "test_unit/application_system_test_case.rb", "test/application_system_test_case.rb", force: true unless options.api? + case test_framework + when :test_unit + directory "test_unit/controllers/#{format}", "test/controllers" + directory "test_unit/mailers/", "test/mailers" + directory "test_unit/system", "test/system" unless options.api? + template "test_unit/test_helper.rb", "test/test_helper.rb", force: true + template "test_unit/application_system_test_case.rb", "test/application_system_test_case.rb", force: true unless options.api? + when :rspec + directory "rspec/requests/#{format}", "spec/requests" + directory "rspec/mailers/", "spec/mailers" + directory "rspec/system", "spec/system" unless options.api? + + uncomment_lines("spec/rails_helper.rb", /'spec', 'support'/) + template "rspec/authentication.rb", "spec/support/authentication.rb", force: true + end end private @@ -272,6 +287,10 @@ def node? Rails.root.join("package.json").exist? end + def test_framework + Rails.application.config.generators.test_framework + end + def ratelimit_block <<~CODE # Rate limit general requests by IP address in a rate of 1000 requests per minute diff --git a/lib/generators/authentication/templates/rspec/authentication.rb.tt b/lib/generators/authentication/templates/rspec/authentication.rb.tt new file mode 100644 index 0000000..460fe55 --- /dev/null +++ b/lib/generators/authentication/templates/rspec/authentication.rb.tt @@ -0,0 +1,36 @@ +module Helpers + module Authentication + module Request + <%- if options.api? -%> + def sign_in_as(user) + post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }) + response.headers["X-Session-Token"] + end + <%- else -%> + def sign_in_as(user) + post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }) + end + <%- end -%> + end + + module System + def sign_in_as(user) + visit sign_in_url + fill_in :email, with: user.email + fill_in :password, with: "Secret1*3*5*" + click_on "Sign in" + + expect(current_path).to eq(root_url) + user + end + end + end +end + +RSpec.configure do |config| + config.include Helpers::Authentication::Request, type: :request + config.include Helpers::Authentication::System, type: :system + + config.include Capybara::RSpecMatchers, type: :request + config.include ActiveSupport::Testing::TimeHelpers +end diff --git a/lib/generators/authentication/templates/rspec/requests/api/sessions_spec.rb b/lib/generators/authentication/templates/rspec/requests/api/sessions_spec.rb new file mode 100644 index 0000000..f983aec --- /dev/null +++ b/lib/generators/authentication/templates/rspec/requests/api/sessions_spec.rb @@ -0,0 +1,49 @@ +require "rails_helper" + +RSpec.describe "Sessions", type: :request do + let(:user) { users(:lazaro_nixon) } + let(:token) { sign_in_as(user) } + let(:default_headers) { { "Authorization" => "Bearer #{token}" } } + + describe "GET #index" do + it "returns HTTP success" do + get sessions_url, headers: default_headers + + expect(response).to have_http_status(:success) + end + end + + describe "GET #show" do + it "returns HTTP success" do + get session_url(user.sessions.last), headers: default_headers + + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + context "with valid credentials" do + it "returns HTTP created" do + post sign_in_url, params: { email: user.email, password: "Secret1*3*5*" } + + expect(response).to have_http_status(:created) + end + end + + context "with invalid credentials" do + it "returns HTTP unauthorized" do + post sign_in_url, params: { email: user.email, password: "SecretWrong1*3" } + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe "DELETE #destroy" do + it "returns HTTP no content" do + delete session_url(user.sessions.last), headers: default_headers + + expect(response).to have_http_status(:no_content) + end + end +end diff --git a/lib/generators/authentication/templates/rspec/requests/html/identity/password_resets_spec.rb.tt b/lib/generators/authentication/templates/rspec/requests/html/identity/password_resets_spec.rb.tt new file mode 100644 index 0000000..830dcf2 --- /dev/null +++ b/lib/generators/authentication/templates/rspec/requests/html/identity/password_resets_spec.rb.tt @@ -0,0 +1,83 @@ +require "rails_helper" + +RSpec.describe Identity::PasswordResetsController, type: :request do + fixtures :users + let(:user) { users(:lazaro_nixon) } + + describe "GET #new" do + it "returns HTTP success" do + get new_identity_password_reset_url + + expect(response).to have_http_status(:success) + end + end + + describe "GET #edit" do + let(:sid) { user.generate_token_for(:password_reset) } + + it "returns HTTP success" do + get edit_identity_password_reset_url(sid: sid) + + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + context "with a valid email" do + it "sends a password reset email" do + expect { + post identity_password_reset_url, params: { email: user.email } + }.to have_enqueued_mail(UserMailer, :password_reset) + + expect(response).to redirect_to(sign_in_url) + end + end + + context "with a nonexistent email" do + it "does not send a password reset email" do + expect { + post identity_password_reset_url, params: { email: "invalid_email@hey.com" } + }.to_not have_enqueued_mail(UserMailer, :password_reset) + + expect(response).to redirect_to(new_identity_password_reset_url) + expect(flash[:alert]).to eq("You can't reset your password until you verify your email") + end + end + + context "with an unverified email" do + it "does not send a password reset email" do + user.update!(verified: false) + + expect { + post identity_password_reset_url, params: { email: user.email } + }.to_not have_enqueued_mail(UserMailer, :password_reset) + + expect(response).to redirect_to(new_identity_password_reset_url) + expect(flash[:alert]).to eq("You can't reset your password until you verify your email") + end + end + end + + describe "PATCH #update" do + let!(:sid) { user.generate_token_for(:password_reset) } + + context "with a valid token" do + it "updates the password" do + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + expect(response).to redirect_to(sign_in_url) + end + end + + context "with an expired token" do + it "does not update the password" do + travel 30.minutes + + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + expect(response).to redirect_to(new_identity_password_reset_url) + expect(flash[:alert]).to eq("That password reset link is invalid") + end + end + end +end diff --git a/lib/generators/authentication/templates/rspec/requests/html/passwords_spec.rb.tt b/lib/generators/authentication/templates/rspec/requests/html/passwords_spec.rb.tt new file mode 100644 index 0000000..28fd4a7 --- /dev/null +++ b/lib/generators/authentication/templates/rspec/requests/html/passwords_spec.rb.tt @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe PasswordsController, type: :request do + fixtures :users + let(:user) { users(:lazaro_nixon) } + + before do + sign_in_as(user) + end + + describe "GET #edit" do + it "returns HTTP success" do + get edit_password_url + + expect(response).to have_http_status(:success) + end + end + + describe "PATCH #update" do + context "with correct password challenge" do + it "updates the password" do + patch password_url, params: { password_challenge: "Secret1*3*5*", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + expect(response).to redirect_to(root_url) + end + end + + context "with wrong password challenge" do + it "returns an error" do + patch password_url, params: { password_challenge: "SecretWrong1*3", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to have_selector("li", text: /Password challenge is invalid/) + end + end + end +end diff --git a/lib/generators/authentication/templates/rspec/requests/html/registrations_spec.rb.tt b/lib/generators/authentication/templates/rspec/requests/html/registrations_spec.rb.tt new file mode 100644 index 0000000..8ac118d --- /dev/null +++ b/lib/generators/authentication/templates/rspec/requests/html/registrations_spec.rb.tt @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe RegistrationsController, type: :request do + describe "GET #new" do + it "returns HTTP success" do + get sign_up_url + + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + it "creates a new user" do + expect { + post sign_up_url, params: { email: "lazaronixon@hey.com", password: "Secret1*3*5*", password_confirmation: "Secret1*3*5*" } + }.to change{ User.count }.by(1) + + expect(response).to redirect_to(root_url) + end + end +end diff --git a/lib/generators/authentication/templates/rspec/requests/html/sessions_spec.rb.tt b/lib/generators/authentication/templates/rspec/requests/html/sessions_spec.rb.tt new file mode 100644 index 0000000..85dd438 --- /dev/null +++ b/lib/generators/authentication/templates/rspec/requests/html/sessions_spec.rb.tt @@ -0,0 +1,58 @@ +require "rails_helper" + +RSpec.describe SessionsController, type: :request do + fixtures :users + let(:user) { users(:lazaro_nixon) } + + describe "GET #index" do + it "returns HTTP success" do + sign_in_as(user) + + get sessions_url + + expect(response).to have_http_status(:success) + end + end + + describe "GET #new" do + it "returns HTTP success" do + get sign_in_url + + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + context "with valid credentials" do + it "signs the user in" do + post sign_in_url, params: { email: user.email, password: "Secret1*3*5*" } + expect(response).to redirect_to(root_url) + + get root_url + expect(response).to have_http_status(:success) + end + end + + context "with invalid credentials" do + it "does not sign the user in" do + post sign_in_url, params: { email: user.email, password: "SecretWrong1*3" } + expect(response).to redirect_to(sign_in_url(email_hint: user.email)) + + get root_url + expect(response).to redirect_to(sign_in_url) + end + end + end + + describe "DELETE #destroy" do + it "signs the user out" do + sign_in_as(user) + + delete session_url(user.sessions.last) + expect(response).to redirect_to(sessions_url) + + follow_redirect! + expect(response).to redirect_to(sign_in_url) + end + end +end diff --git a/lib/generators/authentication/templates/rspec/users.yml b/lib/generators/authentication/templates/rspec/users.yml new file mode 100644 index 0000000..4c5f5d7 --- /dev/null +++ b/lib/generators/authentication/templates/rspec/users.yml @@ -0,0 +1,6 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +lazaro_nixon: + email: lazaronixon@hotmail.com + password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %> + verified: true