authentication-zero 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/CI.yml +0 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/README.md +6 -0
- data/lib/authentication_zero/version.rb +1 -1
- data/lib/generators/authentication/authentication_generator.rb +4 -6
- data/lib/generators/authentication/templates/controllers/api/application_controller.rb.tt +0 -10
- data/lib/generators/authentication/templates/controllers/api/identity/password_resets_controller.rb.tt +1 -1
- data/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt +0 -10
- data/lib/generators/authentication/templates/controllers/html/identity/password_resets_controller.rb.tt +1 -1
- data/lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt +1 -1
- data/lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js +111 -0
- data/lib/generators/authentication/templates/models/user.rb.tt +8 -2
- metadata +4 -10
- data/lib/generators/authentication/templates/javascript/controllers/application.js +0 -11
- data/lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt +0 -15
- data/lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt +0 -26
- data/lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt +0 -28
- data/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt +0 -18
- data/lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt +0 -14
- data/lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10cffeda250c03651c8f0d1f60ae0c726fe9884c4ac40b62de7a7deceb27374c
|
4
|
+
data.tar.gz: 1b3421b4b3053fd76db8500d332e8ee0db4478bb8bf547988192ba646b1a8534
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f8c89fb438dfc43fd47416f5320b4a98e14617896e30e3e0060245f71e2f0e356b1b8d9f709b2c0c930a9cffc8a20e9eff168ffe48cf74c288a3ce191e5a1d6
|
7
|
+
data.tar.gz: 37f2810abcec42035ece7447836eb966a5de77359ef907bdbd336d6918d4ef2b4ca2330e80824084ec4941a250a55f31f21863f23d21fa9e89503b5b3f5fc94f
|
data/.github/workflows/CI.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## Authentication Zero 4.0.0 ##
|
2
|
+
|
3
|
+
* Remove system tests
|
4
|
+
* Use native rate_limit for lockable
|
5
|
+
* Copy web_authn_controller.js instead of depend on stimulus-web-authn
|
6
|
+
|
7
|
+
## Authentication Zero 3.0.2 ##
|
8
|
+
|
9
|
+
* Fix bug where token is not expired/invalid
|
10
|
+
|
1
11
|
## Authentication Zero 3.0.0 ##
|
2
12
|
|
3
13
|
* Use the new normalizes API
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,12 @@ The purpose of authentication zero is to generate a pre-built authentication sys
|
|
8
8
|
$ bundle add authentication-zero
|
9
9
|
```
|
10
10
|
|
11
|
+
If you are using Rails < 8, you must use version 3.
|
12
|
+
|
13
|
+
```
|
14
|
+
$ bundle add authentication-zero --version "~> 3"
|
15
|
+
```
|
16
|
+
|
11
17
|
If you are using Rails < 7.1, you must use version 2.
|
12
18
|
|
13
19
|
```
|
@@ -123,9 +123,9 @@ class AuthenticationGenerator < Rails::Generators::Base
|
|
123
123
|
|
124
124
|
def install_javascript
|
125
125
|
return unless webauthn?
|
126
|
-
copy_file "javascript/controllers/
|
127
|
-
run "bin/importmap pin
|
128
|
-
run "yarn add
|
126
|
+
copy_file "javascript/controllers/web_authn_controller.js", "app/javascript/controllers/web_authn_controller.js"
|
127
|
+
run "bin/importmap pin @rails/request.js" if importmaps?
|
128
|
+
run "yarn add @rails/request.js" if node?
|
129
129
|
end
|
130
130
|
|
131
131
|
def create_views
|
@@ -222,9 +222,7 @@ class AuthenticationGenerator < Rails::Generators::Base
|
|
222
222
|
def create_test_files
|
223
223
|
directory "test_unit/controllers/#{format}", "test/controllers"
|
224
224
|
directory "test_unit/mailers/", "test/mailers"
|
225
|
-
directory "test_unit/system", "test/system" unless options.api?
|
226
225
|
template "test_unit/test_helper.rb", "test/test_helper.rb", force: true
|
227
|
-
template "test_unit/application_system_test_case.rb", "test/application_system_test_case.rb", force: true unless options.api?
|
228
226
|
end
|
229
227
|
|
230
228
|
private
|
@@ -261,7 +259,7 @@ class AuthenticationGenerator < Rails::Generators::Base
|
|
261
259
|
end
|
262
260
|
|
263
261
|
def redis?
|
264
|
-
options.
|
262
|
+
options.ratelimit? || sudoable?
|
265
263
|
end
|
266
264
|
|
267
265
|
def importmaps?
|
@@ -17,14 +17,4 @@ class ApplicationController < ActionController::API
|
|
17
17
|
Current.user_agent = request.user_agent
|
18
18
|
Current.ip_address = request.ip
|
19
19
|
end
|
20
|
-
<%- if options.lockable? %>
|
21
|
-
def require_lock(wait: 1.hour, attempts: 10)
|
22
|
-
counter = Kredis.counter("require_lock:#{request.remote_ip}:#{controller_path}:#{action_name}", expires_in: wait)
|
23
|
-
counter.increment
|
24
|
-
|
25
|
-
if counter.value > attempts
|
26
|
-
render json: { error: "You've exceeded the maximum number of attempts" }, status: :too_many_requests
|
27
|
-
end
|
28
|
-
end
|
29
|
-
<%- end -%>
|
30
20
|
end
|
@@ -2,7 +2,7 @@ class Identity::PasswordResetsController < ApplicationController
|
|
2
2
|
skip_before_action :authenticate
|
3
3
|
|
4
4
|
<%- if options.lockable? -%>
|
5
|
-
|
5
|
+
rate_limit to: 10, within: 1.hour, only: :create
|
6
6
|
<%- end -%>
|
7
7
|
before_action :set_user, only: :update
|
8
8
|
|
@@ -15,16 +15,6 @@ class ApplicationController < ActionController::Base
|
|
15
15
|
Current.user_agent = request.user_agent
|
16
16
|
Current.ip_address = request.ip
|
17
17
|
end
|
18
|
-
<%- if options.lockable? %>
|
19
|
-
def require_lock(wait: 1.hour, attempts: 10)
|
20
|
-
counter = Kredis.counter("require_lock:#{request.remote_ip}:#{controller_path}:#{action_name}", expires_in: wait)
|
21
|
-
counter.increment
|
22
|
-
|
23
|
-
if counter.value > attempts
|
24
|
-
redirect_to root_path, alert: "You've exceeded the maximum number of attempts"
|
25
|
-
end
|
26
|
-
end
|
27
|
-
<%- end -%>
|
28
18
|
<%- if sudoable? %>
|
29
19
|
def require_sudo
|
30
20
|
unless Current.session.sudo?
|
@@ -2,7 +2,7 @@ class Identity::PasswordResetsController < ApplicationController
|
|
2
2
|
skip_before_action :authenticate
|
3
3
|
|
4
4
|
<%- if options.lockable? -%>
|
5
|
-
|
5
|
+
rate_limit to: 10, within: 1.hour, only: :create, with: -> { redirect_to root_path, alert: "Try again later" }
|
6
6
|
<%- end -%>
|
7
7
|
before_action :set_user, only: %i[ edit update ]
|
8
8
|
|
@@ -2,7 +2,7 @@ class Sessions::PasswordlessesController < ApplicationController
|
|
2
2
|
skip_before_action :authenticate
|
3
3
|
|
4
4
|
<%- if options.lockable? -%>
|
5
|
-
|
5
|
+
rate_limit to: 10, within: 1.hour, only: :create, with: -> { redirect_to root_path, alert: "Try again later" }
|
6
6
|
<%- end -%>
|
7
7
|
before_action :set_user, only: :edit
|
8
8
|
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { create, get, supported } from "@github/webauthn-json"
|
3
|
+
import { FetchRequest } from "@rails/request.js"
|
4
|
+
|
5
|
+
export default class WebAuthnController extends Controller {
|
6
|
+
static targets = [ "error", "button", "supportText" ]
|
7
|
+
static classes = [ "loading" ]
|
8
|
+
static values = {
|
9
|
+
challengeUrl: String,
|
10
|
+
verificationUrl: String,
|
11
|
+
fallbackUrl: String,
|
12
|
+
retryText: { type: String, default: "Retry" },
|
13
|
+
notAllowedText: { type: String, default: "That didn't work. Either it was cancelled or took too long. Please try again." },
|
14
|
+
invalidStateText: { type: String, default: "We couldn't add that security key. Please confirm you haven't already registered it, then try again." }
|
15
|
+
}
|
16
|
+
|
17
|
+
connect() {
|
18
|
+
if (!supported()) {
|
19
|
+
this.handleUnsupportedBrowser()
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
getCredential() {
|
24
|
+
this.hideError()
|
25
|
+
this.disableForm()
|
26
|
+
this.requestChallengeAndVerify(get)
|
27
|
+
}
|
28
|
+
|
29
|
+
createCredential() {
|
30
|
+
this.hideError()
|
31
|
+
this.disableForm()
|
32
|
+
this.requestChallengeAndVerify(create)
|
33
|
+
}
|
34
|
+
|
35
|
+
// Private
|
36
|
+
|
37
|
+
handleUnsupportedBrowser() {
|
38
|
+
this.buttonTarget.parentNode.removeChild(this.buttonTarget)
|
39
|
+
|
40
|
+
if (this.fallbackUrlValue) {
|
41
|
+
window.location.replace(this.fallbackUrlValue)
|
42
|
+
} else {
|
43
|
+
this.supportTextTargets.forEach(target => target.hidden = !target.hidden)
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
async requestChallengeAndVerify(fn) {
|
48
|
+
try {
|
49
|
+
const challengeResponse = await this.requestPublicKeyChallenge()
|
50
|
+
const credentialResponse = await fn({ publicKey: challengeResponse })
|
51
|
+
this.onCompletion(await this.verify(credentialResponse))
|
52
|
+
} catch (error) {
|
53
|
+
this.onError(error)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
async requestPublicKeyChallenge() {
|
58
|
+
return await this.request("get", this.challengeUrlValue)
|
59
|
+
}
|
60
|
+
|
61
|
+
async verify(credentialResponse) {
|
62
|
+
return await this.request("post", this.verificationUrlValue, {
|
63
|
+
body: JSON.stringify({ credential: credentialResponse }),
|
64
|
+
contentType: "application/json",
|
65
|
+
responseKind: "json"
|
66
|
+
})
|
67
|
+
}
|
68
|
+
|
69
|
+
onCompletion(response) {
|
70
|
+
window.location.replace(response.location)
|
71
|
+
}
|
72
|
+
|
73
|
+
onError(error) {
|
74
|
+
if (error.code === 0 && error.name === "NotAllowedError") {
|
75
|
+
this.errorTarget.textContent = this.notAllowedTextValue
|
76
|
+
} else if (error.code === 11 && error.name === "InvalidStateError") {
|
77
|
+
this.errorTarget.textContent = this.invalidStateTextValue
|
78
|
+
} else {
|
79
|
+
this.errorTarget.textContent = error.message
|
80
|
+
}
|
81
|
+
this.showError()
|
82
|
+
}
|
83
|
+
|
84
|
+
hideError() {
|
85
|
+
if (this.hasErrorTarget) this.errorTarget.hidden = true
|
86
|
+
}
|
87
|
+
|
88
|
+
showError() {
|
89
|
+
if (this.hasErrorTarget) {
|
90
|
+
this.errorTarget.hidden = false
|
91
|
+
this.buttonTarget.textContent = this.retryTextValue
|
92
|
+
this.enableForm()
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
enableForm() {
|
97
|
+
this.element.classList.remove(this.loadingClass)
|
98
|
+
this.buttonTarget.disabled = false
|
99
|
+
}
|
100
|
+
|
101
|
+
disableForm() {
|
102
|
+
this.element.classList.add(this.loadingClass)
|
103
|
+
this.buttonTarget.disabled = true
|
104
|
+
}
|
105
|
+
|
106
|
+
async request(method, url, options) {
|
107
|
+
const request = new FetchRequest(method, url, { responseKind: "json", ...options })
|
108
|
+
const response = await request.perform()
|
109
|
+
return response.json
|
110
|
+
}
|
111
|
+
}
|
@@ -1,8 +1,14 @@
|
|
1
1
|
class User < ApplicationRecord
|
2
2
|
has_secure_password
|
3
3
|
|
4
|
-
generates_token_for :email_verification, expires_in: 2.days
|
5
|
-
|
4
|
+
generates_token_for :email_verification, expires_in: 2.days do
|
5
|
+
email
|
6
|
+
end
|
7
|
+
|
8
|
+
generates_token_for :password_reset, expires_in: 20.minutes do
|
9
|
+
password_salt.last(10)
|
10
|
+
end
|
11
|
+
|
6
12
|
<%- if options.tenantable? %>
|
7
13
|
belongs_to :account
|
8
14
|
<%- end -%>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: authentication-zero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nixon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -94,7 +94,7 @@ files:
|
|
94
94
|
- lib/generators/authentication/templates/erb/user_mailer/invitation_instructions.html.erb.tt
|
95
95
|
- lib/generators/authentication/templates/erb/user_mailer/password_reset.html.erb.tt
|
96
96
|
- lib/generators/authentication/templates/erb/user_mailer/passwordless.html.erb.tt
|
97
|
-
- lib/generators/authentication/templates/javascript/controllers/
|
97
|
+
- lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js
|
98
98
|
- lib/generators/authentication/templates/lib/account_middleware.rb
|
99
99
|
- lib/generators/authentication/templates/mailers/user_mailer.rb.tt
|
100
100
|
- lib/generators/authentication/templates/migrations/create_accounts_migration.rb.tt
|
@@ -113,7 +113,6 @@ files:
|
|
113
113
|
- lib/generators/authentication/templates/models/session.rb.tt
|
114
114
|
- lib/generators/authentication/templates/models/sign_in_token.rb.tt
|
115
115
|
- lib/generators/authentication/templates/models/user.rb.tt
|
116
|
-
- lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt
|
117
116
|
- lib/generators/authentication/templates/test_unit/controllers/api/identity/email_verifications_controller_test.rb.tt
|
118
117
|
- lib/generators/authentication/templates/test_unit/controllers/api/identity/emails_controller_test.rb.tt
|
119
118
|
- lib/generators/authentication/templates/test_unit/controllers/api/identity/password_resets_controller_test.rb.tt
|
@@ -127,11 +126,6 @@ files:
|
|
127
126
|
- lib/generators/authentication/templates/test_unit/controllers/html/registrations_controller_test.rb.tt
|
128
127
|
- lib/generators/authentication/templates/test_unit/controllers/html/sessions_controller_test.rb.tt
|
129
128
|
- lib/generators/authentication/templates/test_unit/mailers/user_mailer_test.rb.tt
|
130
|
-
- lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt
|
131
|
-
- lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt
|
132
|
-
- lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt
|
133
|
-
- lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt
|
134
|
-
- lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt
|
135
129
|
- lib/generators/authentication/templates/test_unit/test_helper.rb.tt
|
136
130
|
- lib/generators/authentication/templates/test_unit/users.yml
|
137
131
|
homepage: https://github.com/lazaronixon/authentication-zero
|
@@ -156,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
150
|
- !ruby/object:Gem::Version
|
157
151
|
version: '0'
|
158
152
|
requirements: []
|
159
|
-
rubygems_version: 3.
|
153
|
+
rubygems_version: 3.5.5
|
160
154
|
signing_key:
|
161
155
|
specification_version: 4
|
162
156
|
summary: An authentication system generator for Rails applications
|
@@ -1,11 +0,0 @@
|
|
1
|
-
import { Application } from "@hotwired/stimulus"
|
2
|
-
import WebAuthnController from "stimulus-web-authn"
|
3
|
-
|
4
|
-
const application = Application.start()
|
5
|
-
application.register("web-authn", WebAuthnController)
|
6
|
-
|
7
|
-
// Configure Stimulus development experience
|
8
|
-
application.debug = false
|
9
|
-
window.Stimulus = application
|
10
|
-
|
11
|
-
export { application }
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require "test_helper"
|
2
|
-
|
3
|
-
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
4
|
-
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
|
5
|
-
|
6
|
-
def sign_in_as(user)
|
7
|
-
visit sign_in_url
|
8
|
-
fill_in :email, with: user.email
|
9
|
-
fill_in :password, with: "Secret1*3*5*"
|
10
|
-
click_on "Sign in"
|
11
|
-
|
12
|
-
assert_current_path root_url
|
13
|
-
user
|
14
|
-
end
|
15
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require "application_system_test_case"
|
2
|
-
|
3
|
-
class Identity::EmailsTest < ApplicationSystemTestCase
|
4
|
-
setup do
|
5
|
-
@user = sign_in_as(users(:lazaro_nixon))
|
6
|
-
end
|
7
|
-
|
8
|
-
test "updating the email" do
|
9
|
-
click_on "Change email address"
|
10
|
-
|
11
|
-
fill_in "New email", with: "new_email@hey.com"
|
12
|
-
fill_in "Password challenge", with: "Secret1*3*5*"
|
13
|
-
click_on "Save changes"
|
14
|
-
|
15
|
-
assert_text "Your email has been changed"
|
16
|
-
end
|
17
|
-
|
18
|
-
test "sending a verification email" do
|
19
|
-
@user.update! verified: false
|
20
|
-
|
21
|
-
click_on "Change email address"
|
22
|
-
click_on "Re-send verification email"
|
23
|
-
|
24
|
-
assert_text "We sent a verification email to your email address"
|
25
|
-
end
|
26
|
-
end
|
data/lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
require "application_system_test_case"
|
2
|
-
|
3
|
-
class Identity::PasswordResetsTest < ApplicationSystemTestCase
|
4
|
-
setup do
|
5
|
-
@user = users(:lazaro_nixon)
|
6
|
-
@sid = @user.generate_token_for(:password_reset)
|
7
|
-
end
|
8
|
-
|
9
|
-
test "sending a password reset email" do
|
10
|
-
visit sign_in_url
|
11
|
-
click_on "Forgot your password?"
|
12
|
-
|
13
|
-
fill_in "Email", with: @user.email
|
14
|
-
click_on "Send password reset email"
|
15
|
-
|
16
|
-
assert_text "Check your email for reset instructions"
|
17
|
-
end
|
18
|
-
|
19
|
-
test "updating password" do
|
20
|
-
visit edit_identity_password_reset_url(sid: @sid)
|
21
|
-
|
22
|
-
fill_in "New password", with: "Secret6*4*2*"
|
23
|
-
fill_in "Confirm new password", with: "Secret6*4*2*"
|
24
|
-
click_on "Save changes"
|
25
|
-
|
26
|
-
assert_text "Your password was reset successfully. Please sign in"
|
27
|
-
end
|
28
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require "application_system_test_case"
|
2
|
-
|
3
|
-
class PasswordsTest < ApplicationSystemTestCase
|
4
|
-
setup do
|
5
|
-
@user = sign_in_as(users(:lazaro_nixon))
|
6
|
-
end
|
7
|
-
|
8
|
-
test "updating the password" do
|
9
|
-
click_on "Change password"
|
10
|
-
|
11
|
-
fill_in "Password challenge", with: "Secret1*3*5*"
|
12
|
-
fill_in "New password", with: "Secret6*4*2*"
|
13
|
-
fill_in "Confirm new password", with: "Secret6*4*2*"
|
14
|
-
click_on "Save changes"
|
15
|
-
|
16
|
-
assert_text "Your password has been changed"
|
17
|
-
end
|
18
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require "application_system_test_case"
|
2
|
-
|
3
|
-
class RegistrationsTest < ApplicationSystemTestCase
|
4
|
-
test "signing up" do
|
5
|
-
visit sign_up_url
|
6
|
-
|
7
|
-
fill_in "Email", with: "lazaronixon@hey.com"
|
8
|
-
fill_in "Password", with: "Secret6*4*2*"
|
9
|
-
fill_in "Password confirmation", with: "Secret6*4*2*"
|
10
|
-
click_on "Sign up"
|
11
|
-
|
12
|
-
assert_text "Welcome! You have signed up successfully"
|
13
|
-
end
|
14
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
require "application_system_test_case"
|
2
|
-
|
3
|
-
class SessionsTest < ApplicationSystemTestCase
|
4
|
-
setup do
|
5
|
-
@user = users(:lazaro_nixon)
|
6
|
-
end
|
7
|
-
|
8
|
-
test "visiting the index" do
|
9
|
-
sign_in_as @user
|
10
|
-
|
11
|
-
click_on "Devices & Sessions"
|
12
|
-
assert_selector "h1", text: "Sessions"
|
13
|
-
end
|
14
|
-
|
15
|
-
test "signing in" do
|
16
|
-
visit sign_in_url
|
17
|
-
fill_in "Email", with: @user.email
|
18
|
-
fill_in "Password", with: "Secret1*3*5*"
|
19
|
-
click_on "Sign in"
|
20
|
-
|
21
|
-
assert_text "Signed in successfully"
|
22
|
-
end
|
23
|
-
|
24
|
-
test "signing out" do
|
25
|
-
sign_in_as @user
|
26
|
-
|
27
|
-
click_on "Log out"
|
28
|
-
assert_text "That session has been logged out"
|
29
|
-
end
|
30
|
-
end
|