上一章結(jié)尾,我們運行 mix test
檢查出 recipe_controller_test.exs
文件中的兩個錯誤。
很顯然,它們是因為 @valid_attrs
中缺少 user_id
導致的。
怎么辦,在 @valid_attrs
中隨意添加個 user_id
可不能解決問題 - 用戶必須存在。
一個粗暴的解決辦法,是在每個測試中新建一個用戶,然后把用戶 id 傳給 @valid_attrs
,但那樣又要重復一堆代碼,我們可以把新建用戶部分抽取到 setup
中:
diff --git a/test/tv_recipe_web/controllers/recipe_controller_test.exs b/test/tv_recipe_web/controllers/recipe_controller_test.exs
index 923a4a9..0548c85 100644
--- a/test/tv_recipe_web/controllers/recipe_controller_test.exs
+++ b/test/tv_recipe_web/controllers/recipe_controller_test.exs
@@ -2,17 +2,29 @@ defmodule TvRecipeWeb.RecipeControllerTest do
use TvRecipeWeb.ConnCase
alias TvRecipe.Recipes
+ alias TvRecipe.Repo
+ alias TvRecipe.Users.User
+ alias TvRecipe.Recipes.Recipe
@create_attrs %{content: "some content", episode: 42, name: "some name", season: 42, title: "some title"}
@update_attrs %{content: "some updated content", episode: 43, name: "some updated name", season: 43, title: "some updated title"}
@invalid_attrs %{content: nil, episode: nil, name: nil, season: nil, title: nil}
+ defp init_attrs (%{conn: conn} = context) do
+ user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)})
+ attrs = Map.put(@create_attrs, :user_id, user.id)
+
+ context
+ |> Map.put(:attrs, attrs)
+ end
+
def fixture(attrs) do
{:ok, recipe} = Recipes.create_recipe(attrs)
recipe
end
describe "index" do
+ setup [:init_attrs]
test "lists all recipes", %{conn: conn} do
conn = get(conn, Routes.recipe_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Recipes"
@@ -27,8 +39,9 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "create recipe" do
+ setup [:init_attrs]
- test "redirects to show when data is valid", %{conn: conn} do
+ test "redirects to show when data is valid", %{conn: conn, attrs: attrs} do
- conn = post(conn, Routes.recipe_path(conn, :create), recipe: @create_attrs)
+ conn = post(conn, Routes.recipe_path(conn, :create), recipe: attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.recipe_path(conn, :show, id)
@@ -44,7 +57,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "edit recipe" do
- setup [:create_recipe]
+ setup [:init_attrs, :create_recipe]
test "renders form for editing chosen recipe", %{conn: conn, recipe: recipe} do
conn = get(conn, Routes.recipe_path(conn, :edit, recipe))
@@ -53,7 +66,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "update recipe" do
- setup [:create_recipe]
+ setup [:init_attrs, :create_recipe]
test "redirects when data is valid", %{conn: conn, recipe: recipe} do
conn = put(conn, Routes.recipe_path(conn, :update, recipe), recipe: @update_attrs)
@@ -70,7 +83,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "delete recipe" do
- setup [:create_recipe]
+ setup [:init_attrs, :create_recipe]
test "deletes chosen recipe", %{conn: conn, recipe: recipe} do
conn = delete(conn, Routes.recipe_path(conn, :delete, recipe))
@@ -81,8 +94,10 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
end
- defp create_recipe(_) do
+ defp create_recipe(%{attrs: attrs} = context) do
- recipe = fixture(:recipe)
+ recipe = fixture(attrs)
+
- %{recipe: recipe}
+ context
+ |> Map.put(:recipe, recipe)
end
end
在 setup
塊中,我們新建了一個用戶,并且重新組合出真正有效的 recipe 屬性 attrs
,然后返回。
現(xiàn)在運行測試:
$ mix test
......................................................
Finished in 0.8 seconds
56 tests, 0 failures
非常好,全部通過了。
接下來,我們處理動作的權(quán)限問題。
Recipe 動作的權(quán)限
我們先確認 RecipeController
模塊中各個動作的權(quán)限要求:
動作名 | 是否需要登錄 |
---|---|
index | 需要 |
new | 需要 |
create | 需要 |
show | 需要 |
edit | 需要 |
update | 需要 |
delete | 需要 |
都要登錄?難道未登錄用戶不能查看其它用戶創(chuàng)建的菜譜?當然可以,但我們將新建路由來滿足這些需求。這一節(jié),我們開發(fā)的是 Recipe 相關(guān)的管理動作。
前面章節(jié)中我們已經(jīng)嘗試過使用 setup [:login_user]
來標注用戶登錄狀態(tài)下的測試,現(xiàn)在根據(jù)上面羅列的需求來修改 recipe_controller_test.exs
文件中的測試:
diff --git a/test/tv_recipe_web/controllers/recipe_controller_test.exs b/test/tv_recipe_web/controllers/recipe_controller_test.exs
index 0548c85..5eb8866 100644
--- a/test/tv_recipe_web/controllers/recipe_controller_test.exs
+++ b/test/tv_recipe_web/controllers/recipe_controller_test.exs
@@ -10,8 +10,18 @@ defmodule TvRecipeWeb.RecipeControllerTest do
@update_attrs %{content: "some updated content", episode: 43, name: "some updated name", season: 43, title: "some updated title"}
@invalid_attrs %{content: nil, episode: nil, name: nil, season: nil, title: nil}
+ defp login_user(%{conn: conn} = context) do
+ user_attrs = %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)}
+ user = Repo.insert! User.changeset(%User{}, user_attrs)
+ attrs = Map.put(@create_attrs, :user_id, user.id)
+ conn = post conn, Routes.session_path(conn, :create), session: user_attrs
+
+ context
+ |> Map.put(:conn, conn)
+ |> Map.put(:user, user)
+ end
+
- defp init_attrs (%{conn: conn} = context) do
+ defp init_attrs (%{user: user} = context) do
- user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)})
attrs = Map.put(@create_attrs, :user_id, user.id)
context
@@ -24,7 +34,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "index" do
- setup [:init_attrs]
+ setup [:login_user, :init_attrs]
test "lists all recipes", %{conn: conn} do
conn = get(conn, Routes.recipe_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Recipes"
@@ -39,7 +49,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "create recipe" do
- setup [:init_attrs]
+ setup [:login_user, :init_attrs]
test "redirects to show when data is valid", %{conn: conn, attrs: attrs} do
conn = post(conn, Routes.recipe_path(conn, :create), recipe: attrs)
@@ -57,7 +67,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "edit recipe" do
- setup [:init_attrs, :create_recipe]
+ setup [:login_user, :init_attrs, :create_recipe]
test "renders form for editing chosen recipe", %{conn: conn, recipe: recipe} do
conn = get(conn, Routes.recipe_path(conn, :edit, recipe))
@@ -66,7 +76,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "update recipe" do
- setup [:init_attrs, :create_recipe]
+ setup [:login_user, :init_attrs, :create_recipe]
test "redirects when data is valid", %{conn: conn, recipe: recipe} do
conn = put(conn, Routes.recipe_path(conn, :update, recipe), recipe: @update_attrs)
@@ -83,7 +93,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
describe "delete recipe" do
- setup [:init_attrs, :create_recipe]
+ setup [:login_user, :init_attrs, :create_recipe]
test "deletes chosen recipe", %{conn: conn, recipe: recipe} do
conn = delete(conn, Routes.recipe_path(conn, :delete, recipe))
我們給所有測試代碼都加上了 setup [:login_user, :init_attrs]
的設置。
接下來我們需要一個驗證用戶登錄狀態(tài)的 plug,不巧我們在 user_controller.ex
文件中已經(jīng)定義了一個 login_require
的 plug,現(xiàn)在是其它地方也要用到它 - 再放在 user_controller.ex
中并不合適,我們將它移到 auth.ex
文件中:
diff --git a/web/controllers/auth.ex b/web/controllers/auth.ex
index e298b68..3dd3e7f 100644
--- a/web/controllers/auth.ex
+++ b/web/controllers/auth.ex
@@ -1,5 +1,7 @@
defmodule TvRecipeWeb.Auth do
import Plug.Conn
+ import Phoenix.Controller
+ use TvRecipeWeb, :controller
@doc """
初始化選項
@@ -21,4 +23,37 @@ defmodule TvRecipeWeb.Auth do
|> configure_session(renew: true)
end
+ @doc """
+ 檢查用戶登錄狀態(tài)
+
+ Returns `conn`
+ """
+ def login_require(conn, _opts) do
+ if conn.assigns.current_user do
+ conn
+ else
+ conn
+ |> put_flash(:info, "請先登錄")
+ |> redirect(to: Routes.session_path(conn, :new))
+ |> halt()
+ end
+ end
+
+ @doc """
+ 檢查用戶是否授權(quán)訪問動作
+
+ Returns `conn`
+ """
+ def self_require(conn, _opts) do
+ %{"id" => id} = conn.params
+ if String.to_integer(id) == conn.assigns.current_user.id do
+ conn
+ else
+ conn
+ |> put_flash(:error, "禁止訪問未授權(quán)頁面")
+ |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user))
+ |> halt()
+ end
+ end
+
end
\ No newline at end of file
diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex
index 520d986..0f023d3 100644
--- a/web/controllers/user_controller.ex
+++ b/web/controllers/user_controller.ex
@@ -49,36 +49,4 @@ defmodule TvRecipe.UserController do
end
end
- @doc """
- 檢查用戶登錄狀態(tài)
-
- Returns `conn`
- """
- def login_require(conn, _opts) do
- if conn.assigns.current_user do
- conn
- else
- conn
- |> put_flash(:info, "請先登錄")
- |> redirect(to: Routes.session_path(conn, :new))
- |> halt()
- end
- end
-
- @doc """
end
- @doc """
- 檢查用戶登錄狀態(tài)
-
- Returns `conn`
- """
- def login_require(conn, _opts) do
- if conn.assigns.current_user do
- conn
- else
- conn
- |> put_flash(:info, "請先登錄")
- |> redirect(to: Routes.session_path(conn, :new))
- |> halt()
- end
- end
-
- @doc """
- 檢查用戶是否授權(quán)訪問動作
-
- Returns `conn`
- """
- def self_require(conn, _opts) do
- %{"id" => id} = conn.params
- if String.to_integer(id) == conn.assigns.current_user.id do
- conn
- else
- conn
- |> put_flash(:error, "禁止訪問未授權(quán)頁面")
- |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user))
- |> halt()
- end
- end
end
注意,我們并非只是簡單的移動文本到 auth.ex
文件中。在 auth.ex
頭部,我們還引入了兩行代碼,并調(diào)整了兩個 plug:
import Phoenix.Controller
use TvRecipeWeb, :controller
import Phoenix.Controller
導入 put_flash
等方法,而 use TvRecipeWeb, :controller
讓我們在 auth.ex
中可以快速書寫各種路徑。
接著在 user_controller.ex
文件中 import
它:
diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex
index 50fd62e..9990080 100644
--- a/web/web.ex
+++ b/web/web.ex
@@ -36,6 +36,7 @@ defmodule TvRecipe.Web do
+ import TvRecipeWeb.Auth, only: [login_require: 2, self_require: 2]
end
end
注意,目前我們只在 controller
中 import
,后面可能會需要在 router
中 import
。
最后,將 plug 應用到 recipe_controller.ex
文件中:
diff --git a/web/controllers/recipe_controller.ex b/web/controllers/recipe_controller.ex
index 96a0276..c74b492 100644
--- a/web/controllers/recipe_controller.ex
+++ b/web/controllers/recipe_controller.ex
@@ -1,6 +1,6 @@
defmodule TvRecipeWeb.RecipeController do
use TvRecipeWeb, :controller
import TvRecipeWeb.Auth, only: [login_require: 2]
-
+ plug :login_require
alias TvRecipe.Recipe
def index(conn, _params) do
我們這次并沒有使用 when action in
,plug 將應用到該文件中所有的動作。
運行測試:
$ mix test
......................................................
Finished in 0.7 seconds
56 tests, 0 failures
一切順利。
但因為我們現(xiàn)在所有測試都針對登錄狀態(tài),我們還需要補充下未登錄狀態(tài)的測試:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index 5632f8c..faf67ca 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -16,7 +16,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do
{:ok, [attrs: attrs]}
end
end
-
+
@tag logged_in: true
test "lists all entries on index", %{conn: conn} do
conn = get conn, Routes.recipe_path(conn, :index)
@@ -85,4 +85,20 @@ defmodule TvRecipeWeb.RecipeControllerTest do
assert redirected_to(conn) == Routes.recipe_path(conn, :index)
refute Repo.get(Recipe, recipe.id)
end
+
+ test "guest access user action redirected to login page", %{conn: conn} do
+ recipe = Repo.insert! %Recipe{}
+ Enum.each([
+ get(conn, Routes.recipe_path(conn, :index)),
+ get(conn, Routes.recipe_path(conn, :new)),
+ get(conn, Routes.recipe_path(conn, :create), recipe: %{}),
+ get(conn, Routes.recipe_path(conn, :show, recipe)),
+ get(conn, Routes.recipe_path(conn, :edit, recipe)),
+ put(conn, Routes.recipe_path(conn, :update, recipe), recipe: %{}),
+ put(conn, Routes.recipe_path(conn, :delete, recipe)),
+ ], fn conn ->
+ assert redirected_to(conn) == Routes.session_path(conn, :new)
+ assert conn.halted
+ end)
+ end
end
user_id 與 :current_user
在前面的測試里,我們先創(chuàng)建一個新用戶,然后登錄新用戶,新建 recipe,并將新用戶的 id 與新建的 recipe 關(guān)聯(lián)起來。
考慮另一種情況:
- 新建用戶 A
- 新建用戶 B
- 登錄用戶 B
- 關(guān)聯(lián)用戶 A 的 id 與 Recipe
這一種情況,我們的測試還沒有覆蓋到。
我們來新增一個測試,驗證一下:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index faf67ca..d8157a2 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -101,4 +101,17 @@ defmodule TvRecipeWeb.RecipeControllerTest do
assert conn.halted
end)
end
+
+ test "creates resource and redirects when data is valid but with other user_id", %{conn: conn, attrs: attrs} do
+ # 新建一個用戶
+ user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan+1@gmail.com", username: "samchen", password: String.duplicate("1", 6)})
+ # 將新用戶的 id 更新入 attrs,嘗試替 samchen 創(chuàng)建一個菜譜
+ new_attrs = %{attrs | user_id: user.id}
+ post conn, Routes.recipe_path(conn, :create), recipe: new_attrs
+ # 用戶 chenxsan 只能創(chuàng)建自己的菜譜,無法替 samchen 創(chuàng)建菜譜
+ assert Repo.get_by(Recipe, attrs)
+ # samchen 不應該有菜譜
+ refute Repo.get_by(Recipe, new_attrs)
+ end
end
運行測試:
$ mix test
..........................
1) test creates resource and redirects when data is valid but with other user_id (TvRecipeWeb.RecipeControllerTest)
test/controllers/recipe_controller_test.exs:106
Expected truthy, got nil
code: Repo.get_by(Recipe, attrs)
stacktrace:
test/controllers/recipe_controller_test.exs:113: (test)
.............................
Finished in 0.7 seconds
58 tests, 1 failure
測試失?。旱卿洜顟B(tài)下的用戶 A 給用戶 B 創(chuàng)建了一個菜譜。
那么要如何修改我們的控制器代碼?
我們可以像測試中一樣,把登錄用戶的 id 傳遞進去,比如:
def create(conn, %{"recipe" => recipe_params}) do
changeset = Recipe.changeset(%Recipe{}, Map.put(recipe_params, "user_id", conn.assigns.current_user.id))
可是,我們?yōu)槭裁匆峁┙o用戶傳遞 user_id
的機會呢?
讓我們從 recipe.ex
文件中把 user_id
相關(guān)的代碼去掉:
diff --git a/web/models/recipe.ex b/web/models/recipe.ex
index a0b42fd..8d34ed2 100644
--- a/web/models/recipe.ex
+++ b/web/models/recipe.ex
@@ -17,8 +17,7 @@ defmodule TvRecipe.Recipe do
"""
def changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:name, :title, :season, :episode, :content, :user_id])
- |> validate_required([:name, :title, :season, :episode, :content, :user_id], message: "請?zhí)顚?)
- |> foreign_key_constraint(:user_id, message: "用戶不存在")
+ |> cast(params, [:name, :title, :season, :episode, :content])
+ |> validate_required([:name, :title, :season, :episode, :content], message: "請?zhí)顚?)
end
end
recipe_test.exs
中 user_id
相關(guān)的代碼也要去掉:
diff --git a/test/models/recipe_test.exs b/test/models/recipe_test.exs
index 2e1191c..4e59fb9 100644
--- a/test/models/recipe_test.exs
+++ b/test/models/recipe_test.exs
@@ -3,7 +3,7 @@ defmodule TvRecipe.RecipeTest do
alias TvRecipe.{Recipe}
- @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content", user_id: 1}
+ @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content"}
@invalid_attrs %{}
test "changeset with valid attributes" do
@@ -40,14 +40,5 @@ defmodule TvRecipe.RecipeTest do
attrs = %{@valid_attrs | content: ""}
assert {:content, "請?zhí)顚?} in errors_on(%Recipe{}, attrs)
end
-
- test "user_id is required" do
- attrs = %{@valid_attrs | user_id: nil}
- assert {:user_id, "請?zhí)顚?} in errors_on(%Recipe{}, attrs)
- end
- test "user_id should exist in users table" do
- {:error, changeset} = Repo.insert Recipe.changeset(%Recipe{}, @valid_attrs)
- assert {:user_id, "用戶不存在"} in errors_on(changeset)
- end
end
不要忘了還有 recipe_controller_test.exs
文件中的代碼:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index d8157a2..d953315 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -7,13 +7,12 @@ defmodule TvRecipeWeb.RecipeControllerTest do
- defp init_attrs(%{user: user} = context) do
+ defp init_attrs(%{conn: conn} = context) do
- attrs = Map.put(@valid_attrs, :user_id, user.id)
-
context
|> Map.put :attrs, @@valid_attrs
end
@@ -30,10 +29,10 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
- test "creates resource and redirects when data is valid", %{conn: conn, attrs: attrs} do
- conn = post conn, Routes.recipe_path(conn, :create), recipe: attrs
+ test "creates resource and redirects when data is valid", %{conn: conn} do
+ conn = post conn, Routes.recipe_path(conn, :create), recipe: @valid_attrs
assert redirected_to(conn) == Routes.recipe_path(conn, :index)
- assert Repo.get_by(Recipe, attrs)
+ assert Repo.get_by(Recipe, @valid_attrs)
end
@@ -64,11 +63,11 @@ defmodule TvRecipeWeb.RecipeControllerTest do
@@ -64,11 +63,11 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
- test "updates chosen resource and redirects when data is valid", %{conn: conn, attrs: attrs} do
+ test "updates chosen resource and redirects when data is valid", %{conn: conn} do
recipe = Repo.insert! %Recipe{}
- conn = put conn, Routes.recipe_path(conn, :update, recipe), recipe: attrs
+ conn = put conn, Routes.recipe_path(conn, :update, recipe), recipe: @valid_attrs
assert redirected_to(conn) == Routes.recipe_path(conn, :show, recipe)
- assert Repo.get_by(Recipe, attrs)
+ assert Repo.get_by(Recipe, @valid_attrs)
end
@@ -102,16 +101,4 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end)
end
- @tag logged_in: true
- test "creates resource and redirects when data is valid but with other user_id", %{conn: conn, attrs: attrs} do
- # 新建一個用戶
- user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan+1@gmail.com", username: "samchen", password: String.duplicate("1", 6)})
- # 將新用戶的 id 更新入 attrs,嘗試替 samchen 創(chuàng)建一個菜譜
- new_attrs = %{attrs | user_id: user.id}
- post conn, Routes.recipe_path(conn, :create), recipe: new_attrs
- # 用戶 chenxsan 只能創(chuàng)建自己的菜譜,無法替 samchen 創(chuàng)建菜譜
- assert Repo.get_by(Recipe, attrs)
- # samchen 不應該有菜譜
- refute Repo.get_by(Recipe, new_attrs)
- end
end
是的,繞了一圈,我們把前面新增的那個測試給刪除了,因為 Recipe.changeset
已經(jīng)不再接收 user_id
,那個測試已經(jīng)失去意義。
那么,我們要怎樣將當前登錄的用戶 id 置入 recipe 中?
Ecto 提供了 build_assoc 方法,用于處理這類“關(guān)聯(lián)”,我們來改造下 recipe_controller.ex
文件:
diff --git a/web/controllers/recipe_controller.ex b/web/controllers/recipe_controller.ex
index c74b492..967b7bc 100644
--- a/web/controllers/recipe_controller.ex
+++ b/web/controllers/recipe_controller.ex
@@ -9,12 +9,18 @@ defmodule TvRecipe.RecipeController do
end
def index(conn, _params) do
- recipes = Recipes.list_recipes()
+ recipes =
+ conn.assigns.current_user
+ |> assoc(:recipes)
+ |> TvRecipe.Repo.all()
render(conn, "index.html", recipes: recipes)
end
def new(conn, _params) do
- changeset = Recipe.changeset(%Recipe{})
+ changeset =
+ conn.assigns.current_user
+ |> build_assoc(:recipes)
+ |> Recipe.changeset()
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"recipe" => recipe_params}) do
- changeset = Recipe.changeset(%Recipe{}, recipe_params)
+ changeset =
+ conn.assigns.current_user
+ |> build_assoc(:recipes)
+ |> Recipe.changeset(recipe_params)
case Repo.insert(changeset) do
{:ok, _recipe} ->
你可能會好奇此時的 changeset
,我們可以使用 IO.inspect(changeset)
在終端窗口中打印出來,它大致是這個樣子:
#Ecto.Changeset<action: nil,
changes: %{content: "Phoenix Framework is awesome", episode: 2, name: "2", season: 2, title: "2"},
errors: [], data: #TvRecipe.Recipe<>, valid?: true>
可是其中并沒有看到 user_id
- 那么 build_assoc
構(gòu)建的數(shù)據(jù)存放在哪?在 changeset.data
下,大致是這樣:
%TvRecipe.Recipe{__meta__: #Ecto.Schema.Metadata<:built, "recipes">,
content: nil, episode: nil, id: nil, inserted_at: nil, name: nil, season: nil,
title: nil, updated_at: nil,
user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 1}
咦,上面看起來有點像魔法。
但我們仔細閱讀 Repo.insert
的說明,可以看到如下一段:
In case a changeset is given, the changes in the changeset are merged with the struct fields, and all of them are sent to the database.
其中有個關(guān)鍵詞 merged,是的,存放在 changeset.data
下的數(shù)據(jù),會被合并進去,這解釋了我們 user_id
不在 changeset.changes
下,卻最終在數(shù)據(jù)庫中出現(xiàn)的魔法。
最后,我們還要調(diào)整些代碼,來檢驗新建的 recipe 中是否包含了當前登錄用戶的 id:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index d953315..b901b61 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -29,10 +29,10 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
- test "creates resource and redirects when data is valid", %{conn: conn} do
+ test "creates resource and redirects when data is valid", %{conn: conn, user: user} do
conn = post conn, Routes.recipe_path(conn, :create), recipe: @valid_attrs
assert redirected_to(conn) == Routes.recipe_path(conn, :index)
- assert Repo.get_by(Recipe, @valid_attrs)
+ assert Repo.get_by(Recipe, Map.put(@valid_attrs, :user_id, user.id))
end
運行測試:
mix test
.....................................................
Finished in 0.7 seconds
55 tests, 0 failures
全部通過。
我的 recipes
我們還有一個情況未處理,用戶 A 登錄后現(xiàn)在可以查看、編輯、更新用戶 B 的菜譜的,我們要禁止這些動作。
寫個測試驗證一下:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index b901b61..cdbc420 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -101,4 +101,19 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end)
end
+ test "user should not allowed to show recipe of other people", %{conn: conn, user: user} do
+ # 當前登錄用戶創(chuàng)建了一個菜譜
+ conn = post conn, Routes.recipe_path(conn, :create), recipe: @valid_attrs
+ recipe = Repo.get_by(Recipe, Map.put(@valid_attrs, :user_id, user.id))
+ # 新建一個用戶
+ new_user_attrs = %{email: "chenxsan+1@gmail.com", "username": "samchen", password: String.duplicate("1", 6)}
+ Repo.insert! User.changeset(%User{}, new_user_attrs)
+ # 登錄新建的用戶
+ conn = post conn, Routes.session_path(conn, :create), session: new_user_attrs
+ # 讀取前頭的 recipe 失敗,因為它不屬于新用戶所有
+ assert_error_sent 404, fn ->
+ get conn, Routes.recipe_path(conn, :show, recipe)
+ end
+ end
end
運行測試:
mix test
Compiling 1 file (.ex)
................................
1) test user should not allowed to show recipe of other people (TvRecipeWeb.RecipeControllerTest)
test/controllers/recipe_controller_test.exs:105
expected error to be sent as 404 status, but response sent 200 without error
stacktrace:
(phoenix) lib/phoenix/test/conn_test.ex:570: Phoenix.ConnTest.assert_error_sent/2
test/controllers/recipe_controller_test.exs:115: (test)
.....................
Finished in 0.8 seconds
56 tests, 1 failure
Oops,報錯了,我們期望響應是 404,卻得到 200。
那么該如何取得當前登錄用戶自有的菜譜?
既然我們前面已經(jīng)定義過用戶與菜譜的關(guān)聯(lián)關(guān)系,那么一切應該很容易才是。
是的,Ecto 提供了一個 assoc
方法,它能幫我們?nèi)〉糜脩絷P(guān)聯(lián)的所有菜譜。
我們調(diào)整下 recipe_controller.ex
文件:
diff --git a/web/controllers/recipe_controller.ex b/web/controllers/recipe_controller.ex
index 967b7bc..22554ea 100644
--- a/web/controllers/recipe_controller.ex
+++ b/web/controllers/recipe_controller.ex
@@ -33,7 +33,7 @@ defmodule TvRecipe.RecipeController do
end
def show(conn, %{"id" => id}) do
- recipe = Repo.get!(Recipe, id)
+ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id)
render(conn, "show.html", recipe: recipe)
end
再運行測試:
$ mix test
..........................
1) test shows chosen resource (TvRecipeWeb.RecipeControllerTest)
test/controllers/recipe_controller_test.exs:45
** (Ecto.NoResultsError) expected at least one result but got none in query:
from r in TvRecipe.Recipe,
where: r.user_id == ^2469,
where: r.id == ^"645"
stacktrace:
(ecto) lib/ecto/repo/queryable.ex:78: Ecto.Repo.Queryable.one!/4
(tv_recipe) web/controllers/recipe_controller.ex:36: TvRecipe.RecipeController.show/2
(tv_recipe) web/controllers/recipe_controller.ex:1: TvRecipe.RecipeController.action/2
(tv_recipe) web/controllers/recipe_controller.ex:1: TvRecipe.RecipeController.phoenix_controller_pipeline/2
(tv_recipe) lib/tv_recipe/endpoint.ex:1: TvRecipe.Endpoint.instrument/4
(tv_recipe) lib/phoenix/router.ex:261: TvRecipe.Router.dispatch/2
(tv_recipe) web/router.ex:1: TvRecipe.Router.do_call/2
(tv_recipe) lib/tv_recipe/endpoint.ex:1: TvRecipe.Endpoint.phoenix_pipeline/1
(tv_recipe) lib/tv_recipe/endpoint.ex:1: TvRecipe.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
test/controllers/recipe_controller_test.exs:47: (test)
...........................
Finished in 0.8 seconds
56 tests, 1 failure
我們修復了前面一個錯誤,但因為我們的修復代碼導致了另一個新錯誤。
檢查測試代碼,可以發(fā)現(xiàn),舊的測試代碼已經(jīng)不適用了,因為它們新建的 recipe 不包含 user_id
。我們調(diào)整一下:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index cdbc420..d93bbd1 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -42,8 +42,8 @@ defmodule TvRecipeWeb.RecipeControllerTest do
end
@tag logged_in: true
- test "shows chosen resource", %{conn: conn} do
- recipe = Repo.insert! %Recipe{}
+ test "shows chosen resource", %{conn: conn, user: user} do
+ recipe = Repo.insert! %Recipe{user_id: user.id}
conn = get conn, Routes.recipe_path(conn, :show, recipe)
assert html_response(conn, 200) =~ "Show recipe"
end
再運行測試:
$ mix test
......................................................
Finished in 0.8 seconds
56 tests, 0 failures
通過了。
但上面我們只修改了 show
這個動作,其它幾個動作同樣需要修改:
diff --git a/web/controllers/recipe_controller.ex b/web/controllers/recipe_controller.ex
index 22554ea..f317b59 100644
--- a/web/controllers/recipe_controller.ex
+++ b/web/controllers/recipe_controller.ex
@@ -4,7 +4,7 @@ defmodule TvRecipe.RecipeController do
alias TvRecipe.Recipe
def index(conn, _params) do
- recipes = Repo.all(Recipe)
+ recipes = assoc(conn.assigns.current_user, :recipes) |> Repo.all()
render(conn, "index.html", recipes: recipes)
end
@@ -38,13 +38,13 @@ defmodule TvRecipe.RecipeController do
end
def edit(conn, %{"id" => id}) do
- recipe = Repo.get!(Recipe, id)
+ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id)
changeset = Recipe.changeset(recipe)
render(conn, "edit.html", recipe: recipe, changeset: changeset)
end
def update(conn, %{"id" => id, "recipe" => recipe_params}) do
- recipe = Repo.get!(Recipe, id)
+ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id)
changeset = Recipe.changeset(recipe, recipe_params)
case Repo.update(changeset) do
@@ -58,7 +58,7 @@ defmodule TvRecipe.RecipeController do
end
def delete(conn, %{"id" => id}) do
- recipe = Repo.get!(Recipe, id)
+ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id)
# Here we use delete! (with a bang) because we expect
# it to always work (and if it does not, it will raise).
別忘了修復測試:
diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs
index d93bbd1..190ede9 100644
--- a/test/controllers/recipe_controller_test.exs
+++ b/test/controllers/recipe_controller_test.exs
@@ -56,30 +56,30 @@ defmodule TvRecipeWeb.RecipeControllerTest do
-defp create_recipe(%{attrs: attrs} = context) do
+defp create_recipe(%{conn: conn, attrs: attrs} = context) do
- recipe = fixture(attrs)
+ conn = post conn, Routes.recipe_path(conn, :create), recipe: attrs
+ assert %{id: id} = redirected_params(conn)
+ recipe = Recipes.get_recipe!(id)
context
|> Map.put(:recipe, recipe)
end
前面我們在處理 user_id
時,順手刪除了 foreign_key_constraint
,那么,我們要如何處理這樣一種情況:用戶提交創(chuàng)建菜譜的請求,但用戶突然被管理員刪除。這時我們的數(shù)據(jù)庫里就會出現(xiàn)無主的菜譜。我們希望避免這種情況。
我們在 recipe_test.exs
文件中重新增加一個測試:
diff --git a/test/models/recipe_test.exs b/test/models/recipe_test.exs
index 4dbc961..2e2b518 100644
--- a/test/models/recipe_test.exs
+++ b/test/models/recipe_test.exs
@@ -1,7 +1,7 @@
defmodule TvRecipe.RecipeTest do
use TvRecipe.ModelCase
- alias TvRecipe.{Recipe}
+ alias TvRecipe.Repo
+ alias TvRecipe.Users.User
+ alias TvRecipe.Recipes.Recipe
@valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content"}
@invalid_attrs %{}
@@ -41,4 +41,13 @@ defmodule TvRecipe.RecipeTest do
assert {:content, "請?zhí)顚?} in errors_on(%Recipe{}, attrs)
end
+ test "user must exist" do
+ changeset =
+ %User{id: -1}
+ |> Ecto.build_assoc(:recipes)
+ |> Recipe.changeset(@valid_attrs)
+ {:error, changeset} = Repo.insert changeset
+ assert %{user_id: ["does not exist"]} = errors_on(changeset)
+ end
+
end
運行測試:
mix test
Compiling 13 files (.ex)
.............................................
1) test user must exist (TvRecipe.RecipeTest)
test/models/recipe_test.exs:44
** (Ecto.ConstraintError) constraint error when attempting to insert struct:
* foreign_key: recipes_user_id_fkey
If you would like to convert this constraint into an error, please
call foreign_key_constraint/3 in your changeset and define the proper
constraint name. The changeset has not defined any constraint.
stacktrace:
(ecto) lib/ecto/repo/schema.ex:493: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(ecto) lib/ecto/repo/schema.ex:479: Ecto.Repo.Schema.constraints_to_errors/3
(ecto) lib/ecto/repo/schema.ex:213: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
(ecto) lib/ecto/repo/schema.ex:684: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
(ecto) lib/ecto/adapters/sql.ex:615: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
(db_connection) lib/db_connection.ex:1274: DBConnection.transaction_run/4
(db_connection) lib/db_connection.ex:1198: DBConnection.run_begin/3
(db_connection) lib/db_connection.ex:789: DBConnection.transaction/3
test/models/recipe_test.exs:49: (test)
.........
Finished in 0.7 seconds
57 tests, 1 failure
很好,foreign_key_constraint
的提示又出來了。修改 recipe.ex
文件:
diff --git a/web/models/recipe.ex b/web/models/recipe.ex
index 8d34ed2..fcc97ad 100644
--- a/web/models/recipe.ex
+++ b/web/models/recipe.ex
@@ -19,5 +19,6 @@ defmodule TvRecipe.Recipe do
struct
|> cast(params, [:name, :title, :season, :episode, :content])
|> validate_required([:name, :title, :season, :episode, :content], message: "請?zhí)顚?)
+ |> foreign_key_constraint(:user_id)
end
end
再次運行測試:
mix test
Compiling 13 files (.ex)
.......................................................
Finished in 0.7 seconds
57 tests, 0 failures
悉數(shù)通過。至此,我們完成了 RecipeController
的測試。
更多建議: