Currently self-taught, the first lap of the Rails tutorial If you want to challenge on the second lap I want to try Test Driven Development (TDD) I decided to list the requirements that should be implemented in the theme of each chapter
As a task flow that I imagined in the second week Requirement definition (leave here in the first week)> Write a test (connect here in the second week)> Implementation (If it is far from reality, please point it out early)
--Cookies stored in the browser are used to maintain login information.
--I was able to experience the scope of Rails variables (local variables and instance variables)
--ʻAssigns (: user) makes it possible to access the previous instance variable @user. --Dare to include
raise in your code to raise an exception so you can see if it's included in your test --ʻAssert_equal
is written as <expected>, <actual>
--There is a way to display the maintenance page if Heroku is temporarily inaccessible heroku maintenance: on (off)
Not related to the content of this chapter Template out the output to improve the quality of learning Overwhelmingly increased the amount (iuput: output = about 2: 8) The speed of progress has dropped significantly, but I feel that the quality of learning has improved.
Difficult to balance time efficiency and learning effect
Introduced markdown editor for mac to improve time efficiency as much as possible
--Save user ID and memory token in cookie --Make it a persistent cookie --User ID is encrypted and saved (signature) --Use a random character string as a memory token --The token is converted to a hash value and then saved in the database (digest memory). --Allows you to choose to keep your login using checkboxes
current_user = nil
)--If not current_user = nil
, the following processing is unnecessary.
--Search the DB with the user ID saved in the cookie and extract the matching user (user)
--Confirm that the hashed cookie memory token matches the user's digest memory (DB)
--Log in as user (no longer current_user = nil
)
--When you log out, session is nil
and storage token (cookie) and digest storage (DB) are nil
.
--When you log out, current_user
becomes nil
--Redirect to root_url after logout (implemented)
Risk of cookie exposure and what to do
Extract cookies directly from network packets that pass through a poorly managed network with a special software called packet sniffer > Countermeasure: SSL (supported)
Extract the storage token from the database > Workaround: Hash the token stored in the DB
Steal access by directly operating the computer or smartphone on which the user is logged in > Workaround: Change token when logged out (Rails Tutorial 6th Edition)
Added remember_digest
attribute to User model
$ rails generate migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate
I want to be able to handle the remember_token
attribute in the User model
This attribute is not saved in the DB
> Use ʻattr_accessor`
Why you need attr_accessor and why you don't (Thanks to Rails): Rails Tutorial Memorandum-Chapter 9
Define ʻUser.new_tokenin User model (return memory token)
SecureRandom.urlsafe_base64` is suitable for generating memory tokens
$ rails console
>> SecureRandom.urlsafe_base64
=> "brl_446-8bqHv87AQzUj_Q"
def User.new_token
SecureRandom.urlsafe_base64
end
Hash the storage token and save it in the DB
Define remember
in User model
You can use ʻUser.digest (string)` for hashing
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
self.
is required
I also want to have remember executed when I log in It's after this
I want to save the user ID in the browser
Signed
with signed cookies
cookies.signed[:user_id] = user.id
Decrypted with cookies.signed [: user_id]
Cookie persistence is possible with permanent
In the method chain
cookies.permanent.signed[:user_id] = user.id
Define ʻauthenticated?` in User model The behavior is the same as the password, but I can imagine it, but the partial understanding of BCrypt ... is insufficient.
#Returns true if the passed token matches the digest
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
The argument remember_token
is a local variable
Change the behavior when logging in
Added remember (user)
This is different from the remember method of the User model (taking arguments)
app/controllers/sessions_controller.rb
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
remember (user)
is defined as a helper
Call when you log in
Generates a memory token, saves it in a cookie, and saves a digest token in a DB.
(The significance of using different helpers and the priority of methods with the same name are not fully understood)
app/helpers/sessions_helper.rb
#Make a user's session persistent
def remember(user)
user.remember#User model method (generation of storage token, DB storage of digest token)
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
current_user
is not just session
Be maintained by cookies
app/helpers/sessions_helper.rb
#Returns the user corresponding to the memory token cookie
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
You can omit repetition with (user_id = session [: user_id])
=
is not a logical operation, assignment
At this point rails test
is (RED)
FAIL["test_login_with_valid_information_followed_by_logout", #<Minitest::Reporters::Suite:0x0000556848d6b040 @name="UsersLoginTest">, 1.7997455329999923]
test_login_with_valid_information_followed_by_logout#UsersLoginTest (1.80s)
Expected at least 1 element matching "a[href="/login"]", found 0..
Expected 0 to be >= 1.
test/integration/users_login_test.rb:36:in `block in <class:UsersLoginTest>'
I don't see the link to login In other words, it seems that you have not logged out (Expected to maintain current_user by cookie)
DB remember_digest
when logging out
Delete browser cookies (update with nil)
First, define forget
to operate the DB
app/models/user.rb
#Discard user login information
def forget
update_attribute(:remember_digest, nil)
end
Next, define the helper method forget (user)
Nil : remember_digest
on the DB with ʻuser.forgetearlier Browser cookies nil with
cookies.delete`
/sample_app/app/helpers/sessions_helper.rb
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
Call this helper method forget (user)
with the same helper method log_out
/sample_app/app/helpers/sessions_helper.rb
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
Now log_out
can empty both session and cookie
Full logout possible
rails test
is (GREEN)
Resolve bugs that occur when using multiple tabs and browsers
First, if you open multiple tabs while logged in at the same time,
Problems caused by being able to step on Logout (delete logout_path
) multiple times
Since current_user = nil
is set in the first delete logout_path
If you request delete logout_path
again, the controller will call the log_out
method again.
When I try to execute forget (current_user)
, I get an error because current_user = nil
NoMethodError: undefined method `forget' for nil:NilClass
app/helpers/sessions_helper.rb:36:in `forget'
app/helpers/sessions_helper.rb:24:in `log_out'
app/controllers/sessions_controller.rb:20:in `destroy'
To be able to detect this in a test
Very simple considering that you should request delete logout_path
again
test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
.
.
.
test "login with valid information followed by logout" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
#↓ Here again delete log out_Request path
delete logout_path
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
When you run the test (RED) Confirm that the bug can be reproduced successfully
When you are not logged in
For the delete logout_path
request
The log_out
method should not be called
You can use the logged_in
method to confirm your login
def logged_in?
!current_user.nil?
end
app/controllers/sessions_controller.rb
def destroy
log_out if logged_in?
redirect_to root_url
end
Becomes
log_out if logged_in?
Is a fashionable way of writing
Can be replaced with ʻif ... end`
[Ruby] Abuse is strictly prohibited! ?? Cases where writing with a postfix if makes it harder to read This person has been pointed out like this from the viewpoint of readability and would like to refer to it.
In this state, rails test
is (GREEN)
Next,
A, B, two browsers are open, log out with one browser B (DB remember_digest
is nil
),
Then close browser A (browser A's session [: user_id] is nil
)
Problems caused by leaving only cookies stored in browser A
If you reopen Browser A in this state, cookies will remain.
python
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id]) #Becomes true here
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])#error
log_in user
@current_user = user
end
end
end
ʻIf user && user.authenticated? (cookies [: remember_token])is executed Because
remember_digest is set to
nil` due to the behavior of another browser
python
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)#remember_digest = nil
end
Returns an exception due to inconsistency with the contents of the cookie
BCrypt::Errors::InvalidHash: invalid hash
To be able to detect this in the test
The situation of browser A should be reproduced with the object of User model.
In other words, prepare a model with remember_digest = nil
test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?('')
end
end
The @user
created by the setup method is just that, so you can use it
Currently remember_digest = nil
@ user.authenticated? ('')
Returns an exception and the test is (RED)
If remember_digest = nil
I want @ user.authenticated? ('')
to return false
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
Should be
With Progate, I often used it at the end of the If statement, so I don't notice it.
return
exits the method there and returns the value
So no less is executed
Now the test is (GREEN)
Checkboxes can be inserted with the helper method
html:/sample_app/app/views/sessions/new.html.erb
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
Add CSS
app/assets/stylesheets/custom.scss
.
.
.
/* forms */
.
.
.
.checkbox {
margin-top: -10px;
margin-bottom: 10px;
span {
margin-left: 20px;
font-weight: normal;
}
}
#session_remember_me {
width: auto;
margin-left: 0;
}
params [: session] [: remember_me]
If on, receive the value of '1'
, if off, receive the value of '0'
'1'
or '0'
instead of 1
or 0
Add the following to the create
action of the Sessions controller
if params[:session][:remember_me] == '1'
remember(user)
else
forget(user)
end
The above can be rewritten as follows (ternary operator)
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
Completion of a permanent login mechanism up to this point
Define a helper method that allows the user to log in within the test
Can be used for unit tests and integration tests
Define log_in_as method in each of class ActiveSupport :: TestCase
and class ActionDispatch :: IntegrationTest
Because the integration test cannot handle session
directly
Use post login_path
in the log_in_as
method for integration testing
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
#Returns true if the test user is logged in
def is_logged_in?
!session[:user_id].nil?
end
#Log in as a test user
def log_in_as(user)
session[:user_id] = user.id
end
end
class ActionDispatch::IntegrationTest
#Log in as a test user
def log_in_as(user, password: 'password', remember_me: '1')
post login_path, params: { session: { email: user.email,
password: password,
remember_me: remember_me } }
end
end
rails test
(GREEN)
Let's have two problems
Integration test does not allow access to instance variables defined in the controller * > ʻassigns (: user)` will allow you to access the previous instance variable @user
In the first place, the local variable ʻuser` is used in the create method of the Sessions controller *
Instance variable (@user
) is not defined *
> Define an instance variable (@ user
) in the create method of the Sessions controller
As a premise, the @ user
defined by the setup
method in the integration test is
Does not include the remember_token attribute
Why you need attr_accessor and why you don't (Thanks to Rails): Rails Tutorial Memorandum-Chapter 9
/sample_app/app/controllers/sessions_controller.rb
def create
@user = User.find_by(email: params[:session][:email].downcase)
if @user&.authenticate(params[:session][:password])
log_in @user
params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
redirect_to @user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
/sample_app/test/integration/users_login_test.rb
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_equal cookies[:remember_token], assigns(:user).remember_token
end
Testing the behavior of current_user
ʻAssert_equal
/sample_app/test/helpers/sessions_helper_test.rb
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
@user = users(:michael)
remember(@user)
end
test "current_user returns right user when session is nil" do
assert_equal @user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
@user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
If you do the following, it will be temporarily inaccessible The maintenance page can be displayed
$ heroku maintenance:on
$ git push heroku
$ heroku run rails db:migrate
$ heroku maintenance:off
Recommended Posts