Skip to content

Commit

Permalink
feat: browser_authentication_src_template specs
Browse files Browse the repository at this point in the history
  • Loading branch information
mdwagner committed Sep 22, 2023
1 parent 7f07b2c commit 05825ef
Show file tree
Hide file tree
Showing 36 changed files with 556 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require "../spec_helper"

describe "Authentication flow", tags: "flow" do
it "works" do
flow = AuthenticationFlow.new("[email protected]")

flow.sign_up "password"
flow.should_be_signed_in
flow.sign_out
flow.sign_in "wrong-password"
flow.should_have_password_error
flow.sign_in "password"
flow.should_be_signed_in
end

# This is to show you how to sign in as a user during tests.
# Use the `visit` method's `as` option in your tests to sign in as that user.
#
# Feel free to delete this once you have other tests using the 'as' option.
it "allows sign in through backdoor when testing" do
user = UserFactory.create
flow = BaseFlow.new

flow.visit Me::Show, as: user
should_be_signed_in(flow)
end
end

private def should_be_signed_in(flow)
flow.should have_element("@sign-out-button")
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require "../spec_helper"

describe "Reset password flow", tags: "flow" do
it "works" do
user = UserFactory.create
flow = ResetPasswordFlow.new(user)

flow.request_password_reset
flow.should_have_sent_reset_email
flow.reset_password "new-password"
flow.should_be_signed_in
flow.sign_out
flow.sign_in "wrong-password"
flow.should_have_password_error
flow.sign_in "new-password"
flow.should_be_signed_in
end
end
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class AuthenticationFlow < BaseFlow
private getter email

def initialize(@email : String)
end

def sign_up(password)
visit SignUps::New
fill_form SignUpUser,
email: email,
password: password,
password_confirmation: password
click "@sign-up-button"
end

def sign_out
visit Me::Show
sign_out_button.click
end

def sign_in(password)
visit SignIns::New
fill_form SignInUser,
email: email,
password: password
click "@sign-in-button"
end

def should_be_signed_in
current_page.should have_element("@sign-out-button")
end

def should_have_password_error
current_page.should have_element("body", text: "Password is wrong")
end

private def sign_out_button
el("@sign-out-button")
end

# NOTE: this is a shim for readability
private def current_page
self
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class ResetPasswordFlow < BaseFlow
private getter user, authentication_flow
delegate sign_in, sign_out, should_have_password_error, should_be_signed_in,
to: authentication_flow
delegate email, to: user

def initialize(@user : User)
@authentication_flow = AuthenticationFlow.new(user.email)
end

def request_password_reset
with_fake_token do
visit PasswordResetRequests::New
fill_form RequestPasswordReset,
email: email
click "@request-password-reset-button"
end
end

def should_have_sent_reset_email
with_fake_token do
user = UserQuery.new.email(email).first
PasswordResetRequestEmail.new(user).should be_delivered
end
end

def reset_password(password)
user = UserQuery.new.email(email).first
token = Authentic.generate_password_reset_token(user)
visit PasswordResets::New.with(user.id, token)
fill_form ResetPassword,
password: password,
password_confirmation: password
click "@update-password-button"
end

private def with_fake_token(&)
PasswordResetRequestEmail.temp_config(stubbed_token: "fake") do
yield
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Me::Show < BrowserAction
get "/me" do
html ShowPage
end
end
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Auth::AllowGuests
macro included
skip require_sign_in
end

# Since sign in is not required, current_user might be nil
def current_user : User?
current_user?
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Auth::PasswordResets::Base
macro included
include Auth::RedirectSignedInUsers
include Auth::PasswordResets::FindUser
include Auth::PasswordResets::RequireToken
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Auth::PasswordResets::FindUser
private def user : User
UserQuery.find(user_id)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Auth::PasswordResets::RequireToken
macro included
before require_valid_password_reset_token
end

abstract def token : String
abstract def user : User

private def require_valid_password_reset_token
if Authentic.valid_password_reset_token?(user, token)
continue
else
flash.failure = "The password reset link is incorrect or expired. Please try again."
redirect to: PasswordResetRequests::New
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Auth::PasswordResets::TokenFromSession
private def token : String
session.get?(:password_reset_token) || raise "Password reset token not found in session"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Auth::RedirectSignedInUsers
macro included
include Auth::AllowGuests
before redirect_signed_in_users
end

private def redirect_signed_in_users
if current_user?
flash.success = "You are already signed in"
redirect to: Home::Index
else
continue
end
end

# current_user returns nil because signed in users are redirected.
def current_user
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Auth::RequireSignIn
macro included
before require_sign_in
end

private def require_sign_in
if current_user?
continue
else
Authentic.remember_requested_path(self)
flash.info = "Please sign in first"
redirect to: SignIns::New
end
end

# Tells the compiler that the current_user is not nil since we have checked
# that the user is signed in
private def current_user : User
current_user?.as(User)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Auth::TestBackdoor
macro included
before test_backdoor
end

private def test_backdoor
if LuckyEnv.test? && (user_id = params.get?(:backdoor_user_id))
user = UserQuery.find(user_id)
sign_in user
end
continue
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class PasswordResetRequests::Create < BrowserAction
include Auth::RedirectSignedInUsers

post "/password_reset_requests" do
RequestPasswordReset.run(params) do |operation, user|
if user
PasswordResetRequestEmail.new(user).deliver
flash.success = "You should receive an email on how to reset your password shortly"
redirect SignIns::New
else
html NewPage, operation: operation
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class PasswordResetRequests::New < BrowserAction
include Auth::RedirectSignedInUsers

get "/password_reset_requests/new" do
html NewPage, operation: RequestPasswordReset.new
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class PasswordResets::Create < BrowserAction
include Auth::PasswordResets::Base
include Auth::PasswordResets::TokenFromSession

post "/password_resets/:user_id" do
ResetPassword.update(user, params) do |operation, user|
if operation.saved?
session.delete(:password_reset_token)
sign_in user
flash.success = "Your password has been reset"
redirect to: Home::Index
else
html NewPage, operation: operation, user_id: user_id.to_i64
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class PasswordResets::Edit < BrowserAction
include Auth::PasswordResets::Base
include Auth::PasswordResets::TokenFromSession

get "/password_resets/:user_id/edit" do
html NewPage, operation: ResetPassword.new, user_id: user_id.to_i64
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class PasswordResets::New < BrowserAction
include Auth::PasswordResets::Base

param token : String

get "/password_resets/:user_id" do
redirect_to_edit_form_without_token_param
end

# This is to prevent password reset tokens from being scraped in the HTTP Referer header
# See more info here: https://github.com/thoughtbot/clearance/pull/707
private def redirect_to_edit_form_without_token_param
make_token_available_to_future_actions
redirect to: PasswordResets::Edit.with(user_id)
end

private def make_token_available_to_future_actions
session.set(:password_reset_token, token)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class SignIns::Create < BrowserAction
include Auth::RedirectSignedInUsers

post "/sign_in" do
SignInUser.run(params) do |operation, authenticated_user|
if authenticated_user
sign_in(authenticated_user)
flash.success = "You're now signed in"
Authentic.redirect_to_originally_requested_path(self, fallback: Home::Index)
else
flash.failure = "Sign in failed"
html NewPage, operation: operation
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class SignIns::Delete < BrowserAction
delete "/sign_out" do
sign_out
flash.info = "You have been signed out"
redirect to: SignIns::New
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class SignIns::New < BrowserAction
include Auth::RedirectSignedInUsers

get "/sign_in" do
html NewPage, operation: SignInUser.new
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class SignUps::Create < BrowserAction
include Auth::RedirectSignedInUsers

post "/sign_up" do
SignUpUser.create(params) do |operation, user|
if user
flash.info = "Thanks for signing up"
sign_in(user)
redirect to: Home::Index
else
flash.info = "Couldn't sign you up"
html NewPage, operation: operation
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class SignUps::New < BrowserAction
include Auth::RedirectSignedInUsers

get "/sign_up" do
html NewPage, operation: SignUpUser.new
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class PasswordResetRequestEmail < BaseEmail
Habitat.create { setting stubbed_token : String? }
delegate stubbed_token, to: :settings

def initialize(@user : User)
@token = stubbed_token || Authentic.generate_password_reset_token(@user)
end

to @user
from "[email protected]" # or set a default in src/emails/base_email.cr
subject "Reset your password"
templates html, text
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>Please reset your password</h1>

<a href="<%= PasswordResets::New.url(@user.id, @token) %>">Reset password</a>
Loading

0 comments on commit 05825ef

Please sign in to comment.