# 8.2 登錄
登錄表單已經可以處理無效提交,下一步要正確處理有效提交,登入用戶。本節通過臨時會話讓用戶登錄,瀏覽器關閉后會話自動失效。[8.4 節](#remember-me)會實現持久會話,即便瀏覽器關閉,依然處于登錄狀態。
實現會話的過程中要定義很多相關的函數,而且要在多個控制器和視圖中使用。[4.2.5 節](chapter4.html#back-to-the-title-helper)說過,Ruby 支持使用“模塊”把這些函數集中放在一處。Rails 生成器很人性化,生成會話控制器時([8.1.1 節](#sessions-controller))自動生成了一個會話輔助方法模塊。而且,其中的輔助方法會自動引入 Rails 視圖。如果在控制器的基類(`ApplicationController`)中引入輔助方法模塊,還可以在控制器中使用,如[代碼清單 8.11](#listing-sessions-helper-include) 所示。
##### 代碼清單 8.11:在 `ApplicationController` 中引入會話輔助方法模塊
app/controllers/application_controller.rb
```
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper end
```
做好這些基礎工作后,現在可以開始編寫代碼登入用戶了。
## 8.2.1 `log_in` 方法
有 Rails 提供的 `session` 方法協助,登入用戶很簡單。(`session` 方法和 [8.1.1 節](#sessions-controller)生成的會話控制器沒有關系。)我們可以把 `session` 視作一個哈希,可以按照下面的方式賦值:
```
session[:user_id] = user.id
```
這么做會在用戶的瀏覽器中創建一個臨時 cookie,內容是加密后的用戶 ID。在后續的請求中,可以使用 `session[:user_id]` 取回這個 ID。[8.4 節](#remember-me)使用的 `cookies` 方法創建的是持久 cookie,而 `session` 方法創建的是臨時會話,瀏覽器關閉后立即失效。
我們想在多個不同的地方使用這個登錄方式,所以在會話輔助方法模塊中定義一個名為 `log_in` 的方法,如[代碼清單 8.12](#listing-log-in-function) 所示。
##### 代碼清單 8.12:`log_in` 方法
app/helpers/sessions_helper.rb
```
module SessionsHelper
# 登入指定的用戶
def log_in(user)
session[:user_id] = user.id end
end
```
`session` 方法創建的臨時 cookie 會自動加密,所以[代碼清單 8.12](#listing-log-in-function) 中的代碼是安全的,攻擊者無法使用會話中的信息以該用戶的身份登錄。不過,只有 `session` 方法創建的臨時 cookie 是這樣,`cookies` 方法創建的持久 cookie 則有可能會受到“會話劫持”(session hijacking)攻擊。所以在 [8.4 節](#remember-me)我們會小心處理存入用戶瀏覽器中的信息。
定義好 `log_in` 方法后,我們可以完成會話控制器中的 `create` 動作了——登入用戶,然后重定向到用戶的資料頁面,如[代碼清單 8.13](#listing-log-in-success) 所示。[[4](#fn-4)]
##### 代碼清單 8.13:登入用戶
app/controllers/sessions_controller.rb
```
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user redirect_to user else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
```
注意簡潔的重定向代碼
```
redirect_to user
```
我們在 [7.4.1 節](chapter7.html#the-finished-signup-form)見過。Rails 會自動把地址轉換成用戶資料頁的地址:
```
user_url(user)
```
定義好 `create` 動作后,[代碼清單 8.2](#listing-login-form) 中的登錄表單就可以使用了。不過從應用的外觀上看不出什么區別,除非直接查看瀏覽器中的會話,否則沒有方法判斷用戶是否已經登錄。[8.2.2 節](#current-user)會使用會話中的用戶 ID 從數據庫中取回當前用戶,做些視覺上的變化。[8.2.3 節](#changing-the-layout-links)會修改網站布局中的鏈接,還會添加一個指向當前用戶資料頁面的鏈接。
## 8.2.2 當前用戶
把用戶 ID 安全地存儲在臨時會話中之后,在后續的請求中可以將其讀取出來。我們要定義一個名為 `current_user` 的方法,從數據庫中取出用戶 ID 對應的用戶。`current_user` 方法的作用是編寫類似下面的代碼:
```
<%= current_user.name %>
```
或是:
```
redirect_to current_user
```
查找用戶的方法之一是使用 `find` 方法,在用戶資料頁面就是這么做的([代碼清單 7.5](chapter7.html#listing-user-show-action)):
```
User.find(session[:user_id])
```
[6.1.4 節](chapter6.html#finding-user-objects)說過,如果用戶 ID 不存在,`find` 方法會拋出異常。在用戶的資料頁面可以使用這種表現,因為必須有相應的用戶才能顯示他的信息。但 `session[:user_id]` 的值經常是 `nil`(表示用戶未登錄),所以我們要使用 `create` 動作中通過電子郵件地址查找用戶的 `find_by` 方法,通過 `id` 查找用戶:
```
User.find_by(id: session[:user_id])
```
如果 ID 無效,`find_by` 方法返回 `nil`,而不會拋出異常。
因此,我們可以按照下面的方式定義 `current_user` 方法:
```
def current_user
User.find_by(id: session[:user_id])
end
```
這樣定義應該可以,不過如果頁面中多次調用 `current_user`,就會多次查詢數據庫。所以,我們要使用一種 Ruby 習慣寫法,把 `User.find_by` 的結果存儲在實例變量中,只在第一次調用時查詢數據庫,后續再調用直接返回實例變量中存儲的值:[[5](#fn-5)]
```
if @current_user.nil?
@current_user = User.find_by(id: session[:user_id])
else
@current_user
end
```
使用 [4.2.3 節](chapter4.html#objects-and-message-passing)中介紹的“或”操作符 `||`,可以把這段代碼改寫成:
```
@current_user = @current_user || User.find_by(id: session[:user_id])
```
`User` 對象是真值,所以僅當 `@current_user` 沒有賦值時才會執行 `find_by` 方法。
上述代碼雖然可以使用,但并不符合 Ruby 的習慣。`@current_user` 賦值語句的正確寫法是這樣:
```
@current_user ||= User.find_by(id: session[:user_id])
```
這種寫法用到了容易讓人困惑的 `||=`(或等)操作符,參見[旁注 8.1](#aside-or-equals) 中的說明。
##### 旁注 8.1:`||=` 操作符簡介
`||=`(或等)賦值操作符在 Ruby 中常用,因此有追求的 Rails 開發者要學會使用。初學時可能會覺得 `||=` 很神秘,不過和其他操作符對比之后,你會發現也不難理解。
我們先來看一下常見的變量自增一賦值:
```
x = x + 1
```
很多編程語言都為這種操作提供了簡化的操作符,在 Ruby 中(C、C++、Perl、Python、Java 等也可以),可以寫成下面這樣:
```
x += 1
```
其他操作符也有類似的簡化形式:
```
$ rails console
>> x = 1
=> 1
>> x += 1
=> 2
>> x *= 3
=> 6
>> x -= 8
=> -2
>> x /= 2
=> -1
```
通過上面的例子可以得知,`x = x O y` 和 `x O=y` 是等效的,其中 `O` 表示操作符。
在 Ruby 中還經常會遇到這種情況,如果變量的值為 `nil` 則給它賦值,否則就不改變這個變量的值。我們可以使用 [4.2.3 節](chapter4.html#objects-and-message-passing)介紹的或操作符(`||`)編寫下面的代碼:
```
>> @foo
=> nil
>> @foo = @foo || "bar"
=> "bar"
>> @foo = @foo || "baz"
=> "bar"
```
因為 `nil` 是“假值”,所以第一個賦值語句等同于 `nil || "bar"`,得到的結果是 `"bar"`。同樣,第二個賦值操作等同于 `"bar" || "baz"`,得到的結果還是 `"bar"`。這是因為除了 `nil` 和 `false` 之外,其他值都是“真值”,而如果第一個表達式的值是真值,`||` 會終止執行。(或操作的執行順序從左至右,只要出現真值就會終止語句的執行,這種方式叫“短路計算”(short-circuit evaluation)。)
和前面的控制臺會話對比之后,我們發現 `@foo = @foo || "bar"` 符合 `x = x O y` 形式,其中 `||` 就是 `O`:
```
x = x + 1 -> x += 1
x = x * 3 -> x *= 3
x = x - 8 -> x -= 8
x = x / 2 -> x /= 2
@foo = @foo || "bar" -> @foo ||= "bar"
```
因此,`@foo = @foo || "bar"` 和 `@foo ||= "bar"` 兩種寫法是等效的。在獲取當前用戶時,建議使用下面的寫法:
```
@current_user ||= User.find_by(id: session[:user_id])
```
不難理解吧]
綜上所述,`current_user` 方法更簡潔的定義方式如[代碼清單 8.14](#listing-current-user) 所示。
##### 代碼清單 8.14:在會話中查找當前用戶
app/helpers/sessions_helper.rb
```
module SessionsHelper
# 登入指定的用戶
def log_in(user)
session[:user_id] = user.id
end
# 返回當前登錄的用戶(如果有的話)
def current_user
@current_user ||= User.find_by(id: session[:user_id]) end
end
```
定義好 `current_user` 之后,現在可以根據用戶的登錄狀態修改應用的布局了。
## 8.2.3 修改布局中的鏈接
實現登錄功能后,我們要根據登錄狀態修改布局中的鏈接。具體而言,我們要添加退出鏈接、用戶設置頁面的鏈接、用戶列表頁面的鏈接和當前用戶的資料頁面鏈接,構思圖如[圖 8.7](#fig-login-success-mockup) 所示。[[7](#fn-7)]注意,退出鏈接和資料頁面的鏈接在“Account”(賬戶)下拉菜單中。使用 Bootstrap 實現下拉菜單的方法參見[代碼清單 8.16](#listing-layout-login-logout-links)。
圖 8.7:成功登錄后顯示的資料頁面構思圖
此時,在現實開發中,我會考慮編寫集成測試檢測上面規劃的行為。我在[旁注 3.3](chapter3.html#aside-when-to-test) 中說過,當你熟練掌握 Rails 的測試工具后,會傾向于先寫測試。但這個測試涉及到一些新知識,所以最好在專門的一節中編寫([8.2.4 節](#testing-layout-changes))。
修改網站布局中的鏈接時要在 ERb 中使用 `if-else` 語句,用戶登錄時顯示一組鏈接,未登錄時顯示另一組鏈接:
```
<% if logged_in? %>
# 登錄用戶看到的鏈接
<% else %>
# 未登錄用戶看到的鏈接
<% end %>
```
為了編寫這種代碼,我們需要定義 `logged_in?` 方法,返回布爾值。
用戶登錄后,當前用戶存儲在會話中,即 `current_user` 不是 `nil`。檢測會話中有沒有當前用戶要使用“非”操作符([4.2.3 節](chapter4.html#objects-and-message-passing))。“非”操作符寫做 `!`,經常讀作“bang”。`logged_in?` 方法的定義如[代碼清單 8.15](#listing-logged-in-p) 所示。
##### 代碼清單 8.15:`logged_in?` 輔助方法
app/helpers/sessions_helper.rb
```
module SessionsHelper
# 登入指定的用戶
def log_in(user)
session[:user_id] = user.id
end
# 返回當前登錄的用戶(如果有的話)
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
# 如果用戶已登錄,返回 true,否則返回 false
def logged_in?
!current_user.nil? end
end
```
定義好 `logged_in?` 方法之后,可以修改用戶登錄后顯示的鏈接了。我們要添加四個新鏈接,其中兩個鏈接的地址先使用占位符,[第 9 章](chapter9.html#updating-showing-and-deleting-users)會換成真正的地址:
```
<%= link_to "Users", '#' %>
<%= link_to "Settings", '#' %>
```
退出鏈接使用[代碼清單 8.1](#listing-sessions-resource) 中定義的退出頁面地址:
```
<%= link_to "Log out", logout_path, method: "delete" %>
```
注意,退出鏈接中指定了哈希參數,指明這個鏈接發送的是 HTTP `DELETE` 請求。[[8](#fn-8)]我們還要添加資料頁面的鏈接:
```
<%= link_to "Profile", current_user %>
```
這個鏈接可以寫成:
```
<%= link_to "Profile", user_path(current_user) %>
```
和之前一樣,我們可以直接鏈接到用戶對象,Rails 會自動把 `current_user` 轉換成 `user_path(current_user)`。最后,如果用戶未登錄,我們要添加一個鏈接,使用[代碼清單 8.1](#listing-sessions-resource) 中定義的登錄地址,鏈接到登錄頁面:
```
<%= link_to "Log in", login_path %>
```
把這些鏈接都放到頭部局部視圖中,得到的視圖如[代碼清單 8.16](#listing-layout-login-logout-links) 所示。
##### 代碼清單 8.16:修改布局中的鏈接
app/views/layouts/_header.html.erb
```
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
```
除了在布局中添加新鏈接之外,[代碼清單 8.16](#listing-layout-login-logout-links) 還借助 Bootstrap 實現了下拉菜單。[[9](#fn-9)]注意這段代碼中使用的幾個 Bootstrap CSS 類:`dropdown`,`dropdown-menu` 等。為了讓下拉菜單生效,我們要在 `application.js`(Asset Pipeline 的一部分)中引入 Bootstrap 提供的 JavaScript 庫,如[代碼清單 8.17](#listing-bootstrap-js) 所示。
##### 代碼清單 8.17:在 `application.js` 中引入 Bootstrap JavaScript 庫
app/assets/javascripts/application.js
```
//= require jquery
//= require jquery_ujs
//= require bootstrap //= require turbolinks
//= require_tree .
```
現在,你應該訪問登錄頁面,然后使用有效賬戶登錄——這樣足以測試前三節編寫的代碼表現是否正常。[[10](#fn-10)]添加[代碼清單 8.16](#listing-layout-login-logout-links) 和[代碼清單 8.17](#listing-bootstrap-js) 中的代碼后,應該能看到下拉菜單和只有已登錄用戶才能看到的鏈接,如[圖 8.8](#fig-profile-with-logout-link) 所示。如果關閉瀏覽器,還能確認應用確實忘了登錄狀態,必須再次登錄才能看到上述改動。
圖 8.8:用戶登錄后看到了新添加的鏈接和下拉菜單
## 8.2.4 測試布局中的變化
我們自己動手驗證了成功登錄后應用的表現正常,在繼續之前,還要編寫集成測試檢查這些行為,以及捕獲回歸。我們要在[代碼清單 8.7](#listing-flash-persistence-test)的基礎上,再添加一些測試,檢查下面的操作步驟:
1. 訪問登錄頁面;
2. 通過 `post` 請求發送有效的登錄信息;
3. 確認登錄鏈接消失了;
4. 確認出現了退出鏈接;
5. 確認出現了資料頁面鏈接。
為了檢查這些變化,在測試中要登入已經注冊的用戶,也就是說數據庫中必須有一個用戶。Rails 默認使用“固件”實現這種需求。固件是一種組織數據的方式,這些數據會載入測試數據庫。[6.2.5 節](chapter6.html#uniqueness-validation)刪除了默認生成的固件([代碼清單 6.30](chapter6.html#listing-empty-fixtures)),目的是讓檢查電子郵件地址的測試通過。現在,我們要在這個空文件中加入自定義的固件。
目前,我們只需要一個用戶,它的名字和電子郵件地址應該是有效的。因為我們要登入這個用戶,所以還要提供正確的密碼,和提交給會話控制器中 `create` 動作的密碼比較。參照[圖 6.7](chapter6.html#fig-user-model-password-digest) 中的數據模型,可以看出,我們要在用戶固件中定義 `password_digest` 屬性。我們會定義 `digest` 方法計算這個屬性的值。
[6.3.1 節](chapter6.html#a-hashed-password)說過,密碼摘要使用 bcrypt 生成(通過 `has_secure_password` 方法),所以固件中的密碼摘要也要使用這種方法生成。查看[安全密碼的源碼](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb)后,我們發現生成摘要的方法是:
```
BCrypt::Password.create(string, cost: cost)
```
其中,`string` 是要計算哈希值的字符串;`cost` 是“耗時因子”,決定計算哈希值時消耗的資源。耗時因子的值越大,由哈希值破解出原密碼的難度越大。這個值對生產環境的安全防護很重要,但在測試中我們希望 `digest` 方法的執行速度越快越好。安全密碼的源碼中還有這么一行代碼:
```
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
```
這行代碼相當難懂,你無須完全理解,它的作用是嚴格實現前面的分析:在測試中耗時因子使用最小值,在生產環境則使用普通(最大)值。([8.4.5 節](chapter8.html#remember-me-checkbox)會深入介紹奇怪的 `?-:` 寫法。)
`digest` 方法可以放在幾個不同的地方,但 [8.4.1 節](#remember-token-and-digest)會在用戶模型中使用,所以建議放在 `user.rb` 中。因為計算摘要時不用獲取用戶對象,所以我們要把 `digest` 方法附在 `User` 類上,也就是定義為類方法([4.4.1 節](chapter4.html#constructors)簡要介紹過)。結果如[代碼清單 8.18](#listing-digest-method) 所示。
##### 代碼清單 8.18:定義固件中要使用的 `digest` 方法
app/models/user.rb
```
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# 返回指定字符串的哈希摘要
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end
end
```
定義好 `digest` 方法后,我們可以創建一個有效的用戶固件了,如[代碼清單 8.19](#listing-real-user-fixture) 所示。
##### 代碼清單 8.19:測試用戶登錄所需的固件
test/fixtures/users.yml
```
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
```
特別注意一下,固件中可以使用嵌入式 Ruby。因此,我們可以使用
```
<%= User.digest('password') %>
```
生成測試用戶正確的密碼摘要。
我們雖然定義了 `has_secure_password` 所需的 `password_digest` 屬性,但有時也需要使用密碼的原始值。可是,在固件中無法實現,如果在[代碼清單 8.19](#listing-real-user-fixture) 中添加 `password` 屬性,Rails 會提示數據庫中沒有這個列(確實沒有)。所以,我們約定固件中所有用戶的密碼都一樣,即 `'password'`。
創建了一個有效用戶固件后,在測試中可以使用下面的方式獲取這個用戶:
```
user = users(:michael)
```
其中,`users` 對應固件文件 `users.yml` 的文件名,`:michael` 是[代碼清單 8.19](#listing-real-user-fixture) 中定義的用戶。
定義好用戶固件之后,現在可以把本節開頭列出的操作步驟轉換成代碼了,如[代碼清單 8.20](#listing-user-login-test-valid-information) 所示。(注意,這段代碼中的 `get` 和 `post` 兩步嚴格來說沒有關系,其實向控制器發起 `POST` 請求之前沒必要向登錄頁面發起 `GET` 請求。我之所以加入這一步是為了明確表明操作步驟,以及確認渲染登錄表單時沒有錯誤。)
##### 代碼清單 8.20:測試使用有效信息登錄的情況 GREEN
test/integration/users_login_test.rb
```
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup @user = users(:michael) end .
.
.
test "login with valid information" do
get login_path
post login_path, session: { email: @user.email, password: 'password' }
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)
end
end
```
在這段代碼中,我們使用 `assert_redirected_to @user` 檢查重定向的地址是否正確;使用 `follow_redirect!` 訪問重定向的目標地址。還確認頁面中有零個登錄鏈接,從而確認登錄鏈接消失了:
```
assert_select "a[href=?]", login_path, count: 0
```
`count: 0` 參數的目的是,告訴 `assert_select`,我們期望頁面中有零個匹配指定模式的鏈接。([代碼清單 5.25](chapter5.html#listing-layout-links-test)中使用的是 `count: 2`,指定必須有兩個匹配模式的鏈接。)
因為應用代碼已經能正常運行,所以這個測試應該可以通過:
##### 代碼清單 8.21:**GREEN**
```
$ bundle exec rake test TEST=test/integration/users_login_test.rb \
> TESTOPTS="--name test_login_with_valid_information"
```
上述命令說明了如何運行一個測試文件中的某個測試——使用如下參數,并指定測試的名字:
```
TESTOPTS="--name test_login_with_valid_information"
```
(測試的名字是使用下劃線把“test”和測試說明連接在一起。)
## 8.2.5 注冊后直接登錄
雖然現在基本完成了認證功能,但是新注冊的用戶可能還是會困惑,為什么注冊后沒有登錄呢。注冊后立即要求用戶登錄是很奇怪的,所以我們要在注冊的過程中自動登入用戶。為了實現這一功能,我們只需在用戶控制器的 `create` 動作中調用 `log_in` 方法,如[代碼清單 8.22](#listing-login-upon-signup) 所示。[[11](#fn-11)]
##### 代碼清單 8.22:注冊后登入用戶
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
```
為了測試這個功能,我們可以在[代碼清單 7.26](chapter7.html#listing-a-test-for-valid-submission) 中添加一行代碼,檢查用戶是否已經登錄。我們可以定義一個 `is_logged_in?` 輔助方法,功能和[代碼清單 8.15](#listing-logged-in-p) 中的 `logged_in?` 方法一樣,如果(測試環境的)會話中有用戶的 ID 就返回 `true`,否則返回 `false`,如[代碼清單 8.23](#listing-test-helper-sessions) 所示。(我們不能像[代碼清單 8.15](#listing-logged-in-p) 那樣使用 `current_user`,因為在測試中不能使用 `current_user` 方法,但是可以使用 `session` 方法。)我們定義的方法不是 `logged_in?`,而是 `is_logged_in?`——測試輔助方法和會話輔助方法名字不一樣,以免混淆。[[12](#fn-12)]
##### 代碼清單 8.23:在測試中定義檢查登錄狀態的方法,返回布爾值
test/test_helper.rb
```
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
# 如果用戶已登錄,返回 true
def is_logged_in? !session[:user_id].nil? end end
```
然后,我們可以使用[代碼清單 8.24](#listing-login-after-signup-test) 中的測試檢查注冊后用戶有沒有登錄。
##### 代碼清單 8.24:測試注冊后有沒有登入用戶 GREEN
test/integration/users_signup_test.rb
```
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" }
end
assert_template 'users/show'
assert is_logged_in? end
end
```
現在,測試組件應該可以通過:
##### 代碼清單 8.25:**GREEN**
```
$ bundle exec rake test
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習