diff --git a/00-prepare/00-prepare.md b/00-prepare/00-prepare.md index 50efe2b..3555d4c 100644 --- a/00-prepare/00-prepare.md +++ b/00-prepare/00-prepare.md @@ -47,7 +47,7 @@ $ mix local.hex --force ## 安装 Phoenix ```bash -$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez +mix archive.install hex phx_new 1.5.9 ``` ## 安装 Node.js(>=5.0.0) diff --git a/04-user-register/00-prepare.md b/04-user-register/00-prepare.md index e88689a..e581689 100644 --- a/04-user-register/00-prepare.md +++ b/04-user-register/00-prepare.md @@ -42,32 +42,35 @@ 但这样的手动添加过程太麻烦,还容易出错,应该有便捷的方法。 -是的,Phoenix 提供了一系列的 mix 工具包。我们要接触的这个是 [`mix phoenix.gen.html`](https://hexdocs.pm/phoenix/Mix.Tasks.Phoenix.Gen.Html.html)。 +是的,Phoenix 提供了一系列的 mix 工具包。我们要接触的这个是 [`mix phx.gen.html`](https://hexdocs.pm/phoenix/Mix.Tasks.Phoenix.Gen.Html.html)。 -请在命令行窗口下切换到 `tv_recipe` 目录,然后执行 `mix phoenix.gen.html` 命令: +请在命令行窗口下切换到 `tv_recipe` 目录,然后执行 `mix phx.gen.html` 命令: ``` $ cd tv_recipe -$ mix phoenix.gen.html User users username:string:unique email:string:unique password:string +$ mix phx.gen.html Users User users username:string:unique email:string:unique password:string ``` -![mix phoenix.gen.html 命令](/img/02-mix-phoenix.gen.html.png) +![mix phx.gen.html 命令](/img/02-mix-phoenix.gen.html.png) 执行命令后的输出如下: ```bash -* creating web/controllers/user_controller.ex -* creating web/templates/user/edit.html.eex -* creating web/templates/user/form.html.eex -* creating web/templates/user/index.html.eex -* creating web/templates/user/new.html.eex -* creating web/templates/user/show.html.eex -* creating web/views/user_view.ex -* creating test/controllers/user_controller_test.exs -* creating web/models/user.ex -* creating test/models/user_test.exs +* creating lib/tv_recipe_web/controllers/user_controller.ex +* creating lib/tv_recipe_web/templates/user/edit.html.eex +* creating lib/tv_recipe_web/templates/user/form.html.eex +* creating lib/tv_recipe_web/templates/user/index.html.eex +* creating lib/tv_recipe_web/templates/user/new.html.eex +* creating lib/tv_recipe_web/templates/user/show.html.eex +* creating lib/tv_recipe_web/views/user_view.ex +* creating test/lib/tv_recipe_web/controllers/user_controller_test.exs +* creating lib/tv_recipe/users/user.ex * creating priv/repo/migrations/20170123145857_create_user.exs +* creating lib/tv_recipe/users.ex +* injecting lib/tv_recipe/users.ex +* creating test/lib/tv_recipe/users_test.exs +* injecting test/tv_recipe/users_test.exs -Add the resource to your browser scope in web/router.ex: +Add the resource to your browser scope in lib/tv_recipe_web/router.ex: resources "/users", UserController @@ -122,13 +125,13 @@ Generated tv_recipe app 11:08:12.067 [info] == Migrated in 0.0s ``` -操作完上述两步后,因为某些编辑器可能导致的代码重载问题,你需要重启 Phoenix 服务器 - 按两次 Ctrl-C,然后重新执行 `mix phoenix.server`。 +操作完上述两步后,因为某些编辑器可能导致的代码重载问题,你需要重启 Phoenix 服务器 - 按两次 Ctrl-C,然后重新执行 `mix phx.server`。 之后在浏览器中打开网址 `http://localhost:4000/users/new`: ![创建用户页面截图](/img/04-users-new-page.png) -有了。是不是很惊讶?我们用 `mix phoenix.gen.html` 命令生成的样板,功能已经很完善:增删改查功能全都有了。我们需要的,只是在样板基础上做点修改。 +有了。是不是很惊讶?我们用 `mix phx.gen.html` 命令生成的样板,功能已经很完善:增删改查功能全都有了。我们需要的,只是在样板基础上做点修改。 [接下来](/04-user-register/01-username-required.md)几章,我们将一步步完成本章开头列出的限制条件。 diff --git a/04-user-register/01-username-required.md b/04-user-register/01-username-required.md index 045b1ce..15a0509 100644 --- a/04-user-register/01-username-required.md +++ b/04-user-register/01-username-required.md @@ -1,6 +1,6 @@ # username 必填 -[上一章](/04-user-register/00-prepare.md)里,我们用 `mix phoenix.gen.html` 命令创建出完整用户界面,并且具备增加、删除、更改、查询用户的功能。 +[上一章](/04-user-register/00-prepare.md)里,我们用 `mix phx.gen.html` 命令创建出完整用户界面,并且具备增加、删除、更改、查询用户的功能。 这一章,我们将实现 `username` 的第一个规则:`username` 必填,如果未填写,提示用户`请填写`。 @@ -20,7 +20,7 @@ 让我们加上试试: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/tv_recipe/users/user.ex b/tv_recipe/users/user.ex index b7713a0..87ce321 100644 --- a/web/models/user.ex +++ b/web/models/user.ex @@ -43,7 +43,7 @@ index b7713a0..87ce321 100644 又或者,我们可以用 Phoenix 生成的测试文件来验证。 -打开 `test/models/user_test.exs` 文件,默认内容如下: +打开 `test/tv_recipe/users_test.exs` 文件,默认内容如下: ```elixir defmodule TvRecipe.UserTest do @@ -68,10 +68,10 @@ end 文件中有两个变量,`@valid_attrs` 表示有效的 `User` 属性,`@invalid_attrs` 表示无效的 `User` 属性,我们按本章开头拟定的规则修改 `@valid_attrs`: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 1d5494f..7c73207 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -3,7 +3,7 @@ defmodule TvRecipe.UserTest do alias TvRecipe.User @@ -83,13 +83,13 @@ index 1d5494f..7c73207 100644 test "changeset with valid attributes" do ``` -接着,在 `user_test.exs` 文件中添加一个新测试: +接着,在 `users_test.exs` 文件中添加一个新测试: ```elixir diff --git a/test/models/user_test.exs b/test/models/user_test.exs index 7c73207..4c174ab 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -15,4 +15,9 @@ defmodule TvRecipe.UserTest do changeset = User.changeset(%User{}, @invalid_attrs) refute changeset.valid? @@ -97,28 +97,39 @@ index 7c73207..4c174ab 100644 + + test "username should not be blank" do + attrs = %{@valid_attrs | username: ""} -+ assert {:username, "请填写"} in errors_on(%User{}, attrs) ++ assert %{username: ["请填写"] } = errors_on(%User{}, attrs) + end end ``` 这里,`%{@valid_attrs | username: ""}` 是 Elixir 更新映射(Map)的一个方法。 -至于 `errors_on` 函数,它定义在 `tv_recipe/test/support/model_case.ex` 文件中: +至于 `errors_on/2` 函数,它需要新增在 `test/support/data_case.ex` 文件中: ```elixir -def errors_on(struct, data) do - struct.__struct__.changeset(struct, data) - |> Ecto.Changeset.traverse_errors(&TvRecipe.ErrorHelpers.translate_error/1) - |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end ++ ++ def errors_on(struct, attrs) do ++ changeset = struct.__struct__.changeset(struct, attrs) ++ errors_on(changeset) ++ end end ``` + +是否很吃惊?要知道,如果是在 JavaScript 里写两个同名函数,后一个函数会覆盖前一个的定义,而 Elixir 下,我们可以定义多个同名函数,它们能处理不同的状况,而又互不干扰。 + 它检查给定数据中的错误消息,并返回给我们。 现在在命令行下运行: ```bash -$ mix test test/models/user_test.exs +$ mix test test/tv_recipe/users_test.exs ``` 结果如下: diff --git a/04-user-register/02-username-unique.md b/04-user-register/02-username-unique.md index 59dae2c..8944b8c 100644 --- a/04-user-register/02-username-unique.md +++ b/04-user-register/02-username-unique.md @@ -3,13 +3,13 @@ 如果你已完成[上一章](/04-user-register/01-username-required.md),你可能已经猜到,这章的规则要怎么写,不过在那之前,还是让我们先写个测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 4c174ab..47df0c7 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -20,4 +20,13 @@ defmodule TvRecipe.UserTest do attrs = %{@valid_attrs | username: ""} - assert {:username, "请填写"} in errors_on(%User{}, attrs) + assert %{username: ["请填写"]} = errors_on(%User{}, attrs) end + + test "username should be unique" do @@ -22,7 +22,7 @@ index 4c174ab..47df0c7 100644 + end end ``` -此时运行 `mix test test/models/user_test.exs`,我们的测试会全部通过。这是因为,我们在执行 `mix phoenix.gen.html` 命令时,指定了 `unique` 给 `username` 字段,这样生成的 `User` 结构里,我们已经有了唯一性的限定规则,如下所示: +此时运行 `mix test test/tv_recipe/users_test.exs`,我们的测试会全部通过。这是因为,我们在执行 `mix phx.gen.html` 命令时,指定了 `unique` 给 `username` 字段,这样生成的 `User` 结构里,我们已经有了唯一性的限定规则,如下所示: ```elixir def changeset(struct, params \\ %{}) do @@ -35,70 +35,38 @@ end ``` 但上面的测试里,我们只知道插入同名用户时,Phoenix 会返回错误,至于错误是什么,我们还没有检查。 -还记得前一章里用于检查给定数据的错误的 `errors_on` 函数么? - -```elixir -def errors_on(struct, data) do - struct.__struct__.changeset(struct, data) - |> Ecto.Changeset.traverse_errors(&TvRecipe.ErrorHelpers.translate_error/1) - |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) -end -``` -但很可惜,它接收的是一个结构(struct)与映射。而我们现在手头上只有一个 `TvRecipe.Repo.insert(user_changeset)` 返回的 `changset` 可用。 - -我们要在 `tv_recipe/test/support/model_case.ex` 文件中再定义一个 `errors_on` 函数,这一回,它接收一个 `changeset` 参数: - -```elixir -diff --git a/test/support/model_case.ex b/test/support/model_case.ex -index 2b9cb59..85006b5 100644 ---- a/test/support/model_case.ex -+++ b/test/support/model_case.ex -@@ -62,4 +62,10 @@ defmodule TvRecipe.ModelCase do - |> Ecto.Changeset.traverse_errors(&TvRecipe.ErrorHelpers.translate_error/1) - |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) - end -+ -+ def errors_on(changeset) do -+ changeset -+ |> Ecto.Changeset.traverse_errors(&TvRecipe.ErrorHelpers.translate_error/1) -+ |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) -+ end - end -``` -是否很吃惊?要知道,如果是在 JavaScript 里写两个同名函数,后一个函数会覆盖前一个的定义,而 Elixir 下,我们可以定义多个同名函数,它们能处理不同的状况,而又互不干扰。 - 我们来完善下我们上面的测试代码: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 47df0c7..9748671 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -28,5 +28,8 @@ defmodule TvRecipe.UserTest do # 尝试插入同名用户,应报告错误 assert {:error, changeset} = TvRecipe.Repo.insert(user_changeset) + + # 错误信息为“用户名已被人占用” -+ assert {:username, "用户名已被人占用"} in errors_on(changeset) ++ assert %{username: ["用户名已被人占用"]} = errors_on(changeset) end end ``` -再次运行 `mix test test/models/user_test.exs` 的结果是: +再次运行 `mix test test/tv_recipe/users_test.exs` 的结果是: ```bash -$ mix test test/models/user_test.exs +$ mix test test/tv_recipe/users_test.exs . 1) test username should be unique (TvRecipe.UserTest) - test/models/user_test.exs:24 + test/tv_recipe/users_test.exs:24 Assertion with in failed - code: {:username, "用户名已被人占用"} in errors_on(changeset) - left: {:username, "用户名已被人占用"} + code: %{username: ["用户名已被人占用"]} = errors_on(changeset) + left: %{username: ["用户名已被人占用"]} right: [username: "has already been taken"] stacktrace: - test/models/user_test.exs:33: (test) + test/tv_recipe/users_test.exs:33: (test) .. @@ -109,13 +77,13 @@ Finished in 0.1 seconds 这是当然,我们还未自定义用户名重复时的提示消息。 -打开 `web/models/user.ex` 文件,做如下修改: +打开 `lib/tv_recipe/users/user.ex` 文件,做如下修改: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 87ce321..88ad2af 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -16,7 +16,7 @@ defmodule TvRecipe.User do struct |> cast(params, [:username, :email, :password]) @@ -136,13 +104,13 @@ index 87ce321..88ad2af 100644 我们先写个测试验证一下: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 9748671..44cb21b 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -32,4 +32,13 @@ defmodule TvRecipe.UserTest do # 错误信息为“用户名已被人占用” - assert {:username, "用户名已被人占用"} in errors_on(changeset) + assert %{username: ["用户名已被人占用"]} = errors_on(changeset) end + + test "username should be case insensitive" do @@ -158,14 +126,14 @@ index 9748671..44cb21b 100644 运行测试的结果是: ```bash -$ mix test test/models/user_test.exs +$ mix test test/tv_recipe/users_test.exs warning: variable "changeset" is unused - test/models/user_test.exs:42 + test/tv_recipe/users_test.exs:42 ... 1) test username should be case insensitive (TvRecipe.UserTest) - test/models/user_test.exs:36 + test/tv_recipe/users_test.exs:36 match (=) failed code: {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) right: {:ok, @@ -175,7 +143,7 @@ warning: variable "changeset" is unused password: "some content", updated_at: ~N[2017-01-24 11:57:43.741109], username: "Chenxsan"}} stacktrace: - test/models/user_test.exs:42: (test) + test/tv_recipe/users_test.exs:42: (test) . @@ -197,10 +165,10 @@ Finished in 0.1 seconds 根据提示,我们的 `user.ex` 代码可以做如下修改: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 88ad2af..fc07824 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -16,6 +16,7 @@ defmodule TvRecipe.User do struct |> cast(params, [:username, :email, :password]) @@ -218,7 +186,7 @@ index 88ad2af..fc07824 100644 ## 数据库迁移 -在[用户注册一章](00-prepare.md),我们用 `mix phoenix.gen.html` 生成了许多样板文件,其中有一条: +在[用户注册一章](00-prepare.md),我们用 `mix phx.gen.html` 生成了许多样板文件,其中有一条: ```bash * creating priv/repo/migrations/20170123145857_create_user.exs @@ -297,10 +265,10 @@ $ mix ecto.migrate 最后要记得将此前 `user.ex` 文件中 `String.downcase` 的修改撤销掉: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index fc07824..88ad2af 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -16,7 +16,6 @@ defmodule TvRecipe.User do struct |> cast(params, [:username, :email, :password]) @@ -314,14 +282,14 @@ index fc07824..88ad2af 100644 再运行测试看看: ```bash -mix test test/models/user_test.exs +mix test test/tv_recipe/users_test.exs warning: variable "changeset" is unused - test/models/user_test.exs:42 + test/tv_recipe/users_test.exs:42 . 1) test username should be case insensitive (TvRecipe.UserTest) - test/models/user_test.exs:36 + test/tv_recipe/users_test.exs:36 ** (Ecto.ConstraintError) constraint error when attempting to insert struct: * unique: users_lower_username_index @@ -338,12 +306,12 @@ warning: variable "changeset" is unused (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 - test/models/user_test.exs:42: (test) + test/tv_recipe/users_test.exs:42: (test) . 2) test username should be unique (TvRecipe.UserTest) - test/models/user_test.exs:24 + test/tv_recipe/users_test.exs:24 ** (Ecto.ConstraintError) constraint error when attempting to insert struct: * unique: users_lower_username_index @@ -360,7 +328,7 @@ warning: variable "changeset" is unused (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 - test/models/user_test.exs:30: (test) + test/tv_recipe/users_test.exs:30: (test) . @@ -370,10 +338,10 @@ Finished in 0.1 seconds 情况变得更糟糕了,报告了 2 个错误。这是因为索引名称已经改变,而我们的代码还在使用默认的旧索引名。我们需要在 `unique_constraint` 里明确指出索引名称: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 88ad2af..08e4054 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -16,7 +16,7 @@ defmodule TvRecipe.User do struct |> cast(params, [:username, :email, :password]) @@ -387,9 +355,9 @@ index 88ad2af..08e4054 100644 再跑一遍测试: ```bash -$ mix test test/models/user_test.exs +$ mix test test/tv_recipe/users_test.exs warning: variable "changeset" is unused - test/models/user_test.exs:42 + test/tv_recipe/users_test.exs:42 ..... @@ -414,15 +382,15 @@ assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) 让我们完善下测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 9451c2d..975c7b1 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -40,5 +40,6 @@ defmodule TvRecipe.UserTest do # 尝试插入大小写不一致的用户名,应报告错误 another_user_changeset = User.changeset(%User{}, %{@valid_attrs | username: "Chenxsan", email: "chenxsan+1@gmail.com"}) assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) -+ assert {:username, "用户名已被人占用"} in errors_on(changeset) ++ assert %{username: ["用户名已被人占用"]} = errors_on(changeset) end end ``` diff --git a/04-user-register/03-username-format.md b/04-user-register/03-username-format.md index db0065b..5820ff4 100644 --- a/04-user-register/03-username-format.md +++ b/04-user-register/03-username-format.md @@ -4,16 +4,16 @@ 我们用测试来验证一下。 -打开 `test/models/user_test.exs` 文件,添加一个测试: +打开 `test/tv_recipe/users_test.exs` 文件,添加一个测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 975c7b1..644f4c3 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -42,4 +42,10 @@ defmodule TvRecipe.UserTest do assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) - assert {:username, "用户名已被人占用"} in errors_on(changeset) + assert %{username: ["用户名已被人占用"]} = errors_on(changeset) end + + test "username should only contains [a-zA-Z0-9_]" do @@ -26,15 +26,15 @@ index 975c7b1..644f4c3 100644 命令行下运行测试得到的结果是: ```bash -mix test test/models/user_test.exs +mix test test/tv_recipe/users_test.exs .. 1) test username should only contains [a-zA-Z0-9_] (TvRecipe.UserTest) - test/models/user_test.exs:46 + test/tv_recipe/users_test.exs:46 Expected false or nil, got true code: changeset.valid?() stacktrace: - test/models/user_test.exs:49: (test) + test/tv_recipe/users_test.exs:49: (test) ... @@ -45,15 +45,15 @@ Finished in 0.1 seconds 显然,我们需要添加一个规则,在哪儿?怎么定义? -还是在 `web/models/user.ex` 文件中。 +还是在 `lib/tv_recipe/users/user.ex` 文件中。 要限制字符,我们使用 [`validate_format`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_format/4): ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 08e4054..7d7d59f 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -16,6 +16,7 @@ defmodule TvRecipe.User do struct |> cast(params, [:username, :email, :password]) @@ -70,10 +70,10 @@ index 08e4054..7d7d59f 100644 但我们还要再加一个测试,用于验证用户名格式出错时的提示信息。 ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 644f4c3..73fc189 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -48,4 +48,9 @@ defmodule TvRecipe.UserTest do changeset = User.changeset(%User{}, attrs) refute changeset.valid? @@ -81,7 +81,7 @@ index 644f4c3..73fc189 100644 + + test "changeset with invalid username should throw errors" do + attrs = %{@valid_attrs | username: "陈三"} -+ assert {:username, "用户名只允许使用英文字母、数字及下划线"} in errors_on(%User{}, attrs) ++ assert %{username: ["用户名只允许使用英文字母、数字及下划线"]} = errors_on(%User{}, attrs) + end end ``` diff --git a/04-user-register/04-username-length.md b/04-user-register/04-username-length.md index e38dc9d..05a8731 100644 --- a/04-user-register/04-username-length.md +++ b/04-user-register/04-username-length.md @@ -8,38 +8,38 @@ 老规矩,先写测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 73fc189..26a7735 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -53,4 +53,16 @@ defmodule TvRecipe.UserTest do attrs = %{@valid_attrs | username: "陈三"} - assert {:username, "用户名只允许使用英文字母、数字及下划线"} in errors_on(%User{}, attrs) + assert %{username: ["用户名只允许使用英文字母、数字及下划线"]} = errors_on(%User{}, attrs) end + + test "username's length should be larger than 3" do + attrs = %{@valid_attrs | username: "ab"} + changeset = User.changeset(%User{}, attrs) -+ assert {:username, "用户名最短 3 位"} in errors_on(changeset) ++ assert %{username: ["用户名最短 3 位"]} = errors_on(changeset) + end + + test "username's length should be less than 15" do + attrs = %{@valid_attrs | username: String.duplicate("a", 16)} + changeset = User.changeset(%User{}, attrs) -+ assert {:username, "用户名最长 15 位"} in errors_on(changeset) ++ assert %{username: ["用户名最长 15 位"]} = errors_on(changeset) + end end ``` 显然,我们新增的这两个测试会失败,因为我们还没有加上限制规则。 -打开 `web/models/user.ex` 文件,添加两条规则: +打开 `lib/tv_recipe/users/user.ex` 文件,添加两条规则: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 7d7d59f..8c68e6d 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -17,6 +17,8 @@ defmodule TvRecipe.User do |> cast(params, [:username, :email, :password]) |> validate_required([:username, :email, :password], message: "请填写") diff --git a/04-user-register/05-username-exclude.md b/04-user-register/05-username-exclude.md index 9155fe9..afb6e20 100644 --- a/04-user-register/05-username-exclude.md +++ b/04-user-register/05-username-exclude.md @@ -5,18 +5,18 @@ 我们从测试写起: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 26a7735..f70d4a1 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -65,4 +65,9 @@ defmodule TvRecipe.UserTest do changeset = User.changeset(%User{}, attrs) - assert {:username, "用户名最长 15 位"} in errors_on(changeset) + assert %{username: ["用户名最长 15 位"]} = errors_on(changeset) end + + test "username should not be admin or administrator" do -+ assert {:username, "系统保留,无法注册,请更换"} in errors_on(%User{}, %{@valid_attrs | username: "admin"}) -+ assert {:username, "系统保留,无法注册,请更换"} in errors_on(%User{}, %{@valid_attrs | username: "administrator"}) ++ assert %{username: ["系统保留,无法注册,请更换"]} = errors_on(%User{}, %{@valid_attrs | username: "admin"}) ++ assert %{username: ["系统保留,无法注册,请更换"]} = errors_on(%User{}, %{@valid_attrs | username: "administrator"}) + end end ``` @@ -24,10 +24,10 @@ index 26a7735..f70d4a1 100644 然后是添加规则,照例还是在 `user.ex` 文件中: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 8c68e6d..35e4d0b 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -19,6 +19,7 @@ defmodule TvRecipe.User do |> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/, message: "用户名只允许使用英文字母、数字及下划线") |> validate_length(:username, min: 3, message: "用户名最短 3 位") diff --git a/04-user-register/06-email-rules.md b/04-user-register/06-email-rules.md index c0b6f24..021ec3c 100644 --- a/04-user-register/06-email-rules.md +++ b/04-user-register/06-email-rules.md @@ -15,18 +15,18 @@ 首先,添加测试规则,验证 `email` 为空时的错误提示: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index f70d4a1..bae1e57 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -70,4 +70,9 @@ defmodule TvRecipe.UserTest do - assert {:username, "系统保留,无法注册,请更换"} in errors_on(%User{}, %{@valid_attrs | username: "admin"}) - assert {:username, "系统保留,无法注册,请更换"} in errors_on(%User{}, %{@valid_attrs | username: "administrator"}) + assert %{username: ["系统保留,无法注册,请更换"]} = errors_on(%User{}, %{@valid_attrs | username: "admin"}) + assert %{username: ["系统保留,无法注册,请更换"]} = errors_on(%User{}, %{@valid_attrs | username: "administrator"}) end + + test "email should not be blank" do + attrs = %{@valid_attrs | email: ""} -+ assert {:email, "请填写"} in errors_on(%User{}, attrs) ++ assert %{email: ["请填写"]} = errors_on(%User{}, attrs) + end end ``` @@ -39,18 +39,18 @@ index f70d4a1..bae1e57 100644 我们先添加一个测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index bae1e57..67aab23 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -75,4 +75,9 @@ defmodule TvRecipe.UserTest do attrs = %{@valid_attrs | email: ""} - assert {:email, "请填写"} in errors_on(%User{}, attrs) + assert %{email: ["请填写"]} = errors_on(%User{}, attrs) end + + test "email should contain @" do + attrs = %{@valid_attrs | email: "ab"} -+ assert {:email, "邮箱格式错误"} in errors_on(%User{}, attrs) ++ assert %{email: ["邮箱格式错误"]} = errors_on(%User{}, attrs) + end end ``` @@ -60,10 +60,10 @@ index bae1e57..67aab23 100644 下面在 `user.ex` 文件中添加 `validate_format` 验证规则: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 35e4d0b..fef942b 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -21,6 +21,7 @@ defmodule TvRecipe.User do |> validate_length(:username, max: 15, message: "用户名最长 15 位") |> validate_exclusion(:username, ~w(admin administrator), message: "系统保留,无法注册,请更换") @@ -81,13 +81,13 @@ index 35e4d0b..fef942b 100644 仍是先写测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 67aab23..f6c99e5 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -80,4 +80,16 @@ defmodule TvRecipe.UserTest do attrs = %{@valid_attrs | email: "ab"} - assert {:email, "邮箱格式错误"} in errors_on(%User{}, attrs) + assert %{email: ["邮箱格式错误"]} = errors_on(%User{}, attrs) end + + test "email should be unique" do @@ -99,7 +99,7 @@ index 67aab23..f6c99e5 100644 + assert {:error, changeset} = TvRecipe.Repo.insert(User.changeset(%User{}, %{@valid_attrs | username: "samchen"})) + + # 错误信息为“邮箱已被人占用” -+ assert {:email, "邮箱已被人占用"} in errors_on(changeset) ++ assert %{email: ["邮箱已被人占用"]} = errors_on(changeset) + end end ``` @@ -109,10 +109,10 @@ index 67aab23..f6c99e5 100644 打开 `user.ex` 文件,添加 `message` 如下: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index fef942b..54e7e4c 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -22,6 +22,6 @@ defmodule TvRecipe.User do |> validate_exclusion(:username, ~w(admin administrator), message: "系统保留,无法注册,请更换") |> unique_constraint(:username, name: :users_lower_username_index, message: "用户名已被人占用") @@ -128,13 +128,13 @@ index fef942b..54e7e4c 100644 最后,还有一个测试,是关于 `email` 大小写的,即 `a@b` 与 `A@b` 应当认为是一致的: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index f6c99e5..82dcf6a 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -92,4 +92,14 @@ defmodule TvRecipe.UserTest do # 错误信息为“邮箱已被人占用” - assert {:email, "邮箱已被人占用"} in errors_on(changeset) + assert %{email: ["邮箱已被人占用"]} = errors_on(changeset) end + + test "email should be case insensitive" do @@ -144,7 +144,7 @@ index f6c99e5..82dcf6a 100644 + # 尝试插入大小写不一致的邮箱,应报告错误 + another_user_changeset = User.changeset(%User{}, %{@valid_attrs | username: "samchen", email: "chenXsan@gmail.com"}) + assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) -+ assert {:email, "邮箱已被人占用"} in errors_on(changeset) ++ assert %{email: ["邮箱已被人占用"]} = errors_on(changeset) + end end ``` @@ -196,10 +196,10 @@ index f6c99e5..82dcf6a 100644 4. 最后,将新索引的名称赋给 `unique_constraint` 的 `name` 参数: ```elixir - diff --git a/web/models/user.ex b/web/models/user.ex + diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 54e7e4c..9307a3c 100644 - --- a/web/models/user.ex - +++ b/web/models/user.ex + --- a/lib/tv_recipe/users/user.ex + +++ b/lib/tv_recipe/users/user.ex @@ -22,6 +22,6 @@ defmodule TvRecipe.User do |> validate_exclusion(:username, ~w(admin administrator), message: "系统保留,无法注册,请更换") |> unique_constraint(:username, name: :users_lower_username_index, message: "用户名已被人占用") @@ -212,7 +212,7 @@ index f6c99e5..82dcf6a 100644 再跑一遍测试: ```bash -$ mix test test/models/user_test.exs +$ mix test test/tv_recipe/users_test.exs .............. Finished in 0.2 seconds diff --git a/04-user-register/07-password-rules.md b/04-user-register/07-password-rules.md index 4a78cf7..75b3fad 100644 --- a/04-user-register/07-password-rules.md +++ b/04-user-register/07-password-rules.md @@ -13,23 +13,23 @@ 首先,是添加两个测试: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 82dcf6a..8689f4e 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -102,4 +102,14 @@ defmodule TvRecipe.UserTest do assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset) - assert {:email, "邮箱已被人占用"} in errors_on(changeset) + assert %{email: ["邮箱已被人占用"]} = errors_on(changeset) end + + test "password is required" do + attrs = %{@valid_attrs | password: ""} -+ assert {:password, "请填写"} in errors_on(%User{}, attrs) ++ assert %{password: ["请填写"]} = errors_on(%User{}, attrs) + end + + test "password's length should be larger than 6" do + attrs = %{@valid_attrs | password: String.duplicate("1", 5)} -+ assert {:password, "密码最短 6 位"} in errors_on(%User{}, attrs) ++ assert %{password: ["密码最短 6 位"]} = errors_on(%User{}, attrs) + end end ``` @@ -41,10 +41,10 @@ index 82dcf6a..8689f4e 100644 打开 `user.ex` 文件,添加一行 `validate_length`: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 9307a3c..3069e79 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -23,5 +23,6 @@ defmodule TvRecipe.User do |> unique_constraint(:username, name: :users_lower_username_index, message: "用户名已被人占用") |> validate_format(:email, ~r/@/, message: "邮箱格式错误") diff --git a/04-user-register/08-password-storage.md b/04-user-register/08-password-storage.md index e72c74c..bfcdced 100644 --- a/04-user-register/08-password-storage.md +++ b/04-user-register/08-password-storage.md @@ -19,10 +19,11 @@ index a71d654..3320fc8 100644 +++ b/mix.exs @@ -19,7 +19,7 @@ defmodule TvRecipe.Mixfile do def application do - [mod: {TvRecipe, []}, - applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, -- :phoenix_ecto, :postgrex]] -+ :phoenix_ecto, :postgrex, :comeonin]] + [ + mod: {TvRecipe.Application, []}, +- extra_applications: [:logger, :runtime_tools] ++ extra_applications: [:logger, :runtime_tools, :comeonin] + ] end # Specifies which paths to compile per environment. @@ -30,9 +31,9 @@ index a71d654..3320fc8 100644 {:phoenix_html, "~> 2.6"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, -- {:cowboy, "~> 1.0"}] -+ {:cowboy, "~> 1.0"}, -+ {:comeonin, "~> 3.0"}] +- {:plug_cowboy, "~> 2.0"} ++ {:plug_cowboy, "~> 2.0"}, ++ {:comeonin, "~> 3.0"} end ``` @@ -41,26 +42,26 @@ index a71d654..3320fc8 100644 接着在命令行下执行: ```bash -$ mix do deps.get, compile +$ mix do deps.get, deps.compile ``` 该命令从远程下载了我们新增的 `comeonin` 依赖并编译。 -那么,怎么确认 `comeonin` 安装成功?之前,我们一直是用 `mix phoenix.server` 命令来启动服务器的,接下来,我们要换一种启动方式: +那么,怎么确认 `comeonin` 安装成功?之前,我们一直是用 `mix phx.server` 命令来启动服务器的,接下来,我们要换一种启动方式: ```bash -$ iex -S mix phoenix.server +$ iex -S mix phx.server ``` 区别在哪?我们来看看后者启动后的结果: ``` -$ iex -S mix phoenix.server +$ iex -S mix phx.server Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] [info] Running TvRecipe.Endpoint with Cowboy using http://localhost:4000 Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> 25 Jan 09:53:09 - info: compiled 6 files into 2 files, copied 3 in 2.1 sec ``` -看到区别了么?我们用 `iex -S mix phoenix.server` 启动后,可以使用 Elixir 的 [`iex`](http://elixir-lang.org/docs/stable/iex/IEx.html)。 +看到区别了么?我们用 `iex -S mix phx.server` 启动后,可以使用 Elixir 的 [`iex`](http://elixir-lang.org/docs/stable/iex/IEx.html)。 比如,我们可以输入 `Com` 然后按 Tab 键: @@ -116,10 +117,10 @@ iex(1)> 25 Jan 09:53:09 - info: compiled 6 files into 2 files, copied 3 in 2.1 s 不,我们留着 `password`,但要给它加上 `virtual: true`,表示它是个临时字段,不存储到数据库中: ```elixir - diff --git a/web/models/user.ex b/web/models/user.ex + diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 3069e79..e60e839 100644 - --- a/web/models/user.ex - +++ b/web/models/user.ex + --- a/lib/tv_recipe/users/user.ex + +++ b/lib/tv_recipe/users/user.ex @@ -4,7 +4,8 @@ defmodule TvRecipe.User do schema "users" do field :username, :string @@ -179,10 +180,10 @@ iex(1)> 25 Jan 09:53:09 - info: compiled 6 files into 2 files, copied 3 in 2.1 s 我们可以在 `changeset` 末尾再加一道工序: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index e60e839..58447c0 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -25,5 +25,6 @@ defmodule TvRecipe.User do |> validate_format(:email, ~r/@/, message: "邮箱格式错误") |> unique_constraint(:email, name: :users_lower_email_index, message: "邮箱已被人占用") @@ -208,10 +209,10 @@ changeset = put_password_hash(changeset) 现在,我们来定义 `put_password_hash` 函数: ```elixir -diff --git a/web/models/user.ex b/web/models/user.ex +diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex index 58447c0..690a1ed 100644 ---- a/web/models/user.ex -+++ b/web/models/user.ex +--- a/lib/tv_recipe/users/user.ex ++++ b/lib/tv_recipe/users/user.ex @@ -27,4 +27,13 @@ defmodule TvRecipe.User do |> validate_length(:password, min: 6, message: "密码最短 6 位") |> put_password_hash() @@ -244,13 +245,13 @@ index 58447c0..690a1ed 100644 当然,我们还要添加一个测试,用 [Comeonin.Bcrypt.checkpw](https://hexdocs.pm/comeonin/Comeonin.Bcrypt.html#checkpw/2) 来保证 `put_password_hash` 函数的结果: ```elixir -diff --git a/test/models/user_test.exs b/test/models/user_test.exs +diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs index 8689f4e..6e946b0 100644 ---- a/test/models/user_test.exs -+++ b/test/models/user_test.exs +--- a/test/tv_recipe/users_test.exs ++++ b/test/tv_recipe/users_test.exs @@ -112,4 +112,9 @@ defmodule TvRecipe.UserTest do attrs = %{@valid_attrs | password: String.duplicate("1", 5)} - assert {:password, "密码最短 6 位"} in errors_on(%User{}, attrs) + assert %{password: ["密码最短 6 位"]} = errors_on(%User{}, attrs) end + + test "password should be hashed" do @@ -263,7 +264,7 @@ index 8689f4e..6e946b0 100644 运行测试: ```bash -mix test test/models/user_test.exs +mix test test/tv_recipe/users_test.exs ................. Finished in 4.2 seconds @@ -292,7 +293,7 @@ index 0ff4a98..1743d57 100644 再次运行测试: ```bash -mix test test/models/user_test.exs +mix test test/tv_recipe/users_test.exs ................. Finished in 0.2 seconds diff --git a/04-user-register/09-optimize-ui.md b/04-user-register/09-optimize-ui.md index ec23498..c07d2a5 100644 --- a/04-user-register/09-optimize-ui.md +++ b/04-user-register/09-optimize-ui.md @@ -28,10 +28,10 @@ Phoenix 生成的 `form.html.eex` 模板里使用了 Bootstrap [样式](https:// 但模板中生成的样式与 Bootstrap 的比,差了 `has-error` 这样的 CSS 状态类。我们可以给它补上: ```eex -diff --git a/web/templates/user/form.html.eex b/web/templates/user/form.html.eex +diff --git a/lib/tv_recipe_web/templates/user/form.html.eex b/lib/tv_recipe_web/templates/user/form.html.eex index 5857c33..b047466 100644 ---- a/web/templates/user/form.html.eex -+++ b/web/templates/user/form.html.eex +--- a/lib/tv_recipe_web/templates/user/form.html.eex ++++ b/lib/tv_recipe_web/templates/user/form.html.eex @@ -5,19 +5,19 @@ <% end %> @@ -79,30 +79,30 @@ index 5857c33..b047466 100644 ## 控制器的测试 -你可能对 `mix test test/models/user_test.exs` 命令已经烂熟于心。但 `mix test test/controllers/user_controller_test.exs` 呢? +你可能对 `mix test test/tv_recipe/users_test.exs` 命令已经烂熟于心。但 `mix test test/tv_recipe_web/controllers/user_controller_test.exs` 呢? -我们在生成用户的样板文件时,曾经生成过一个 `user_controller_test.exs` 文件,让我们运行下 `mix test test/controllers/user_controller_test.exs` 看看结果: +我们在生成用户的样板文件时,曾经生成过一个 `user_controller_test.exs` 文件,让我们运行下 `mix test test/tv_recipe_web/controllers/user_controller_test.exs` 看看结果: ```bash -$ mix test test/controllers/user_controller_test.exs +$ mix test test/tv_recipe_web/controllers/user_controller_test.exs Compiling 1 file (.ex) .... 1) test updates chosen resource and redirects when data is valid (TvRecipe.UserControllerTest) - test/controllers/user_controller_test.exs:47 + test/tv_recipe_web/controllers/user_controller_test.exs:47 ** (RuntimeError) expected redirection with status 302, got: 200 stacktrace: (phoenix) lib/phoenix/test/conn_test.ex:443: Phoenix.ConnTest.redirected_to/2 - test/controllers/user_controller_test.exs:50: (test) + test/tv_recipe_web/controllers/user_controller_test.exs:50: (test) .... 2) test creates resource and redirects when data is valid (TvRecipe.UserControllerTest) - test/controllers/user_controller_test.exs:18 + test/tv_recipe_web/controllers/user_controller_test.exs:18 ** (RuntimeError) expected redirection with status 302, got: 200 stacktrace: (phoenix) lib/phoenix/test/conn_test.ex:443: Phoenix.ConnTest.redirected_to/2 - test/controllers/user_controller_test.exs:20: (test) + test/tv_recipe_web/controllers/user_controller_test.exs:20: (test) @@ -114,10 +114,10 @@ Finished in 0.3 seconds 显然,从模板文件到现在,我们的代码已经变化,现在测试文件一样需要根据实际情况做调整: ```elixir -diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs +diff --git a/test/tv_recipe_web/controllers/user_controller_test.exs b/test/tv_recipe_web/controllers/user_controller_test.exs index 2e08483..95d3108 100644 ---- a/test/controllers/user_controller_test.exs -+++ b/test/controllers/user_controller_test.exs +--- a/test/tv_recipe_web/controllers/user_controller_test.exs ++++ b/test/tv_recipe_web/controllers/user_controller_test.exs @@ -2,7 +2,7 @@ defmodule TvRecipe.UserControllerTest do use TvRecipe.ConnCase @@ -129,8 +129,8 @@ index 2e08483..95d3108 100644 test "lists all entries on index", %{conn: conn} do @@ -18,7 +18,7 @@ defmodule TvRecipe.UserControllerTest do test "creates resource and redirects when data is valid", %{conn: conn} do - conn = post conn, user_path(conn, :create), user: @valid_attrs - assert redirected_to(conn) == user_path(conn, :index) + conn = post conn, Routes.user_path(conn, :create), user: @valid_attrs + assert redirected_to(conn) == Routes.user_path(conn, :index) - assert Repo.get_by(User, @valid_attrs) + assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) end @@ -138,8 +138,8 @@ index 2e08483..95d3108 100644 test "does not create resource and renders errors when data is invalid", %{conn: conn} do @@ -48,7 +48,7 @@ defmodule TvRecipe.UserControllerTest do user = Repo.insert! %User{} - conn = put conn, user_path(conn, :update, user), user: @valid_attrs - assert redirected_to(conn) == user_path(conn, :show, user) + conn = put conn, Routes.user_path(conn, :update, user), user: @valid_attrs + assert redirected_to(conn) == Routes.user_path(conn, :show, user) - assert Repo.get_by(User, @valid_attrs) + assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) end diff --git a/05-session/01-login.md b/05-session/01-login.md index 55cfcc0..0170214 100644 --- a/05-session/01-login.md +++ b/05-session/01-login.md @@ -1,6 +1,6 @@ # 登录 -这一次,我们没有 `mix phoenix.gen.html` 可以用,所以要一步一步写了。 +这一次,我们没有 `mix phx.gen.html` 可以用,所以要一步一步写了。 它的过程,跟[添加帮助文件一章](/02-explore-phoenix/02-explore-phoenix.md)一样。 @@ -10,11 +10,11 @@ Don't panic,错误是指引我们成功的路灯。 ## 添加路由 -首先在 `test/controllers` 目录下新建一个 `session_controller_test.exs` 文件: +首先在 `test/tv_recipe_web/controllers` 目录下新建一个 `session_controller_test.exs` 文件: ```elixir -defmodule TvRecipe.SessionControllerTest do - use TvRecipe.ConnCase +defmodule TvRecipeWeb.SessionControllerTest do + use TvRecipeWeb.ConnCase end ``` @@ -22,7 +22,7 @@ end ```elixir test "renders form for new sessions", %{conn: conn} do - conn = get conn, session_path(conn, :new) + conn = get conn, Routes.session_path(conn, :new) # 200 响应,页面上带有“登录” assert html_response(conn, 200) =~ "登录" end @@ -30,8 +30,8 @@ end 运行测试,结果如下: ```bash -$ mix test test/controllers/session_controller_test.exs -** (CompileError) test/controllers/session_controller_test.exs:5: undefined function session_path/2 +$ mix test test/tv_recipe_web/controllers/session_controller_test.exs +** (CompileError) test/tv_recipe_web/controllers/session_controller_test.exs:5: undefined function session_path/2 (stdlib) lists.erl:1338: :lists.foreach/2 (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6 (elixir) lib/code.ex:370: Code.require_file/2 @@ -62,10 +62,10 @@ get "/", PageController, :index 我们回头去看控制器的代码,会在开头处看到这么一行: ```elixir -use TvRecipe.Web, :controller +use TvRecipeWeb, :controller ``` -而 `TvRecipe.Web` 是定义在 `web/web.ex` 文件,其中会有这样的内容: +而 `TvRecipeWeb` 是定义在 `tv_recipe_web/tv_recipe_web.ex` 文件,其中会有这样的内容: ```elixir def controller do @@ -81,17 +81,17 @@ use TvRecipe.Web, :controller end end ``` -我们看到了 `import TvRecipe.Router.Helpers` 一行,这正是我们在控制器中可以直接使用 `user_path` 等函数的原因 - `use TvRecipe.Web, :controller` 做了准备工作。 +我们看到了 `import TvRecipe.Router.Helpers` 一行,这正是我们在控制器中可以直接使用 `user_path` 等函数的原因 - `use TvRecipeWeb, :controller` 做了准备工作。 现在,我们知道要怎么定义 `session_path` 了。 打开 `router.ex` 文件,添加一个新路由: ```elixir -diff --git a/web/router.ex b/web/router.ex +diff --git a/lib/tv_recipe_web/router.ex b/lib/tv_recipe_web/router.ex index 4ddc1cc..aac327c 100644 ---- a/web/router.ex -+++ b/web/router.ex +--- a/lib/tv_recipe_web/router.ex ++++ b/lib/tv_recipe_web/router.ex @@ -18,6 +18,7 @@ defmodule TvRecipe.Router do get "/", PageController, :index @@ -102,23 +102,23 @@ index 4ddc1cc..aac327c 100644 运行测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 8 files (.ex) - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 - ** (UndefinedFunctionError) function TvRecipe.SessionController.init/1 is undefined (module TvRecipe.SessionController + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 + ** (UndefinedFunctionError) function TvRecipeWeb.SessionController.init/1 is undefined (module TvRecipeWeb.SessionController is not available) stacktrace: - TvRecipe.SessionController.init(:new) - (tv_recipe) web/router.ex:1: anonymous fn/1 in TvRecipe.Router.match_route/4 + TvRecipeWeb.SessionController.init(:new) + (tv_recipe) lib/tv_recipe_web/router.ex:1: anonymous fn/1 in TvRecipe.Router.match_route/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_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/session_controller_test.exs:5: (test) + test/tv_recipe_web/controllers/session_controller_test.exs:5: (test) @@ -133,8 +133,8 @@ Finished in 0.08 seconds 在 `web/controllers` 目录下新建一个 `session_controller.ex` 文件,内容如下: ```elixir -defmodule TvRecipe.SessionController do - use TvRecipe.Web, :controller +defmodule TvRecipeWeb.SessionController do + use TvRecipeWeb, :controller def new(conn, _params) do render conn, "new.html" @@ -146,58 +146,58 @@ end 现在运行测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 1 file (.ex) Generated tv_recipe app - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 - ** (UndefinedFunctionError) function TvRecipe.SessionView.render/2 is undefined (module TvRecipe.SessionView is not ava + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 + ** (UndefinedFunctionError) function TvRecipeWeb.SessionView.render/2 is undefined (module TvRecipeWeb.SessionView is not ava ilable) stacktrace: - TvRecipe.SessionView.render("new.html", %{conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{layou + TvRecipeWeb.SessionView.render("new.html", %{conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{layou t: {TvRecipe.LayoutView, "app.html"}}, before_send: [#Function<0.101282891/1 in Plug.CSRFProtection.call/2>, #Function<4.111 648917/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.61377594/1 in Plug.Session.before_send/2>, #Function<1.115972179/ 1 in Plug.Logger.call/2>], body_params: %{}, cookies: %{}, halted: false, host: "www.example.com", method: "GET", owner: #PI D<0.302.0>, params: %{}, path_info: ["sessions", "new"], path_params: %{}, peer: {{127, 0, 0, 1}, 111317}, port: 80, private -: %{TvRecipe.Router => {[], %{}}, :phoenix_action => :new, :phoenix_controller => TvRecipe.SessionController, :phoenix_endpo +: %{TvRecipe.Router => {[], %{}}, :phoenix_action => :new, :phoenix_controller => TvRecipeWeb.SessionController, :phoenix_endpo int => TvRecipe.Endpoint, :phoenix_flash => %{}, :phoenix_format => "html", :phoenix_layout => {TvRecipe.LayoutView, :app}, :phoenix_pipelines => [:browser], :phoenix_recycled => true, :phoenix_route => #Function<12.75217690/1 in TvRecipe.Router.ma -tch_route/4>, :phoenix_router => TvRecipe.Router, :phoenix_template => "new.html", :phoenix_view => TvRecipe.SessionView, :p +tch_route/4>, :phoenix_router => TvRecipe.Router, :phoenix_template => "new.html", :phoenix_view => TvRecipeWeb.SessionView, :p lug_session => %{}, :plug_session_fetch => :done, :plug_skip_csrf_protection => true}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{}, req_headers: [], request_path: "/sessions/new", resp_body: nil, resp_cookies: % {}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "eedn739jkdct1hr8r3nod6nst95b2 qvu"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}], sch eme: :http, script_name: [], secret_key_base: "XfacEiZ/QVO87L4qirM0thXcedgcx5zYhLPAsmVPnL8AVu6qB/Et84yvJ6712aSn", state: :un -set, status: nil}, view_module: TvRecipe.SessionView, view_template: "new.html"}) +set, status: nil}, view_module: TvRecipeWeb.SessionView, view_template: "new.html"}) (tv_recipe) web/templates/layout/app.html.eex:29: TvRecipe.LayoutView."app.html"/1 (phoenix) lib/phoenix/view.ex:335: Phoenix.View.render_to_iodata/3 (phoenix) lib/phoenix/controller.ex:642: Phoenix.Controller.do_render/4 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.action/2 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.phoenix_controller_pipeline/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.action/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.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_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/session_controller_test.exs:5: (test) + test/tv_recipe_web/controllers/session_controller_test.exs:5: (test) Finished in 0.1 seconds 1 test, 1 failure ``` -测试失败,因为 `TvRecipe.SessionView` 未定义。 +测试失败,因为 `TvRecipeWeb.SessionView` 未定义。 ## 创建 `SessionView` 模块 在 `web/views` 目录下新建一个 `session_view.ex` 文件,内容如下: ```elixir -defmodule TvRecipe.SessionView do - use TvRecipe.Web, :view +defmodule TvRecipeWeb.SessionView do + use TvRecipeWeb, :view end ``` 在 Phoenix 下,View 与 templates 是分开的,其中 View 是模块(module),而 templates 在编译后,会变成 View 模块中的函数。这也是为什么我们在定义模板之前,要先定义视图的原因。 @@ -205,14 +205,14 @@ end 此时运行测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 1 file (.ex) Generated tv_recipe app - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 - ** (Phoenix.Template.UndefinedError) Could not render "new.html" for TvRecipe.SessionView, please define a matching cla + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 + ** (Phoenix.Template.UndefinedError) Could not render "new.html" for TvRecipeWeb.SessionView, please define a matching cla use for render/2 or define a template at "web/templates/session". No templates were compiled for this module. Assigns: @@ -221,31 +221,31 @@ ore_send: [#Function<0.101282891/1 in Plug.CSRFProtection.call/2>, #Function<4.1 /2>, #Function<0.61377594/1 in Plug.Session.before_send/2>, #Function<1.115972179/1 in Plug.Logger.call/2>], body_params: %{ }, cookies: %{}, halted: false, host: "www.example.com", method: "GET", owner: #PID<0.300.0>, params: %{}, path_info: ["sess ions", "new"], path_params: %{}, peer: {{127, 0, 0, 1}, 111317}, port: 80, private: %{TvRecipe.Router => {[], %{}}, :phoenix -_action => :new, :phoenix_controller => TvRecipe.SessionController, :phoenix_endpoint => TvRecipe.Endpoint, :phoenix_flash = +_action => :new, :phoenix_controller => TvRecipeWeb.SessionController, :phoenix_endpoint => TvRecipe.Endpoint, :phoenix_flash = > %{}, :phoenix_format => "html", :phoenix_layout => {TvRecipe.LayoutView, :app}, :phoenix_pipelines => [:browser], :phoenix _recycled => true, :phoenix_route => #Function<12.75217690/1 in TvRecipe.Router.match_route/4>, :phoenix_router => TvRecipe. -Router, :phoenix_template => "new.html", :phoenix_view => TvRecipe.SessionView, :plug_session => %{}, :plug_session_fetch => +Router, :phoenix_template => "new.html", :phoenix_view => TvRecipeWeb.SessionView, :plug_session => %{}, :plug_session_fetch => :done, :plug_skip_csrf_protection => true}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{ }, req_headers: [], request_path: "/sessions/new", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max- age=0, private, must-revalidate"}, {"x-request-id", "vi7asqkbb9153m6ku8btf8r50p38rsqn"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}], scheme: :http, script_name: [], secret_key_ba se: "XfacEiZ/QVO87L4qirM0thXcedgcx5zYhLPAsmVPnL8AVu6qB/Et84yvJ6712aSn", state: :unset, status: nil}, template_not_found: TvR -ecipe.SessionView, view_module: TvRecipe.SessionView, view_template: "new.html"} +ecipe.SessionView, view_module: TvRecipeWeb.SessionView, view_template: "new.html"} stacktrace: (phoenix) lib/phoenix/template.ex:364: Phoenix.Template.raise_template_not_found/3 (tv_recipe) web/templates/layout/app.html.eex:29: TvRecipe.LayoutView."app.html"/1 (phoenix) lib/phoenix/view.ex:335: Phoenix.View.render_to_iodata/3 (phoenix) lib/phoenix/controller.ex:642: Phoenix.Controller.do_render/4 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.action/2 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.phoenix_controller_pipeline/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.action/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.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_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/session_controller_test.exs:5: (test) + test/tv_recipe_web/controllers/session_controller_test.exs:5: (test) @@ -261,12 +261,12 @@ Finished in 0.1 seconds 现在运行测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 1 file (.ex) - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 Assertion with =~ failed code: html_response(conn, 200) =~ "登录" left: "\n\n \n \n Get Started\n \n t>\n \n\n" right: "登录" stacktrace: - test/controllers/session_controller_test.exs:7: (test) + test/tv_recipe_web/controllers/session_controller_test.exs:7: (test) @@ -327,12 +327,12 @@ Finished in 0.1 seconds 但测试结果告诉我们: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 1 file (.ex) - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 ** (ArgumentError) assign @changeset not available in eex template. Please make sure all proper assigns have been set. If this @@ -368,41 +368,41 @@ index 9c1f842..1df67cc 100644 - - <% end %> - -+<%= form_for @conn, session_path(@conn, :create), [as: :session], fn f -> %> ++<%= form_for @conn, Routes.session_path(@conn, :create), [as: :session], fn f -> %>
"> <%= label f, :email, class: "control-label" %> <%= text_input f, :email, class: "form-control" %> ``` -`session_path(@conn, :create)` 是表单数据要提交的路径,`as: :session` 则表示表单数据提交时,是保存在 `session` 的键名下的。 +`Routes.session_path(@conn, :create)` 是表单数据要提交的路径,`as: :session` 则表示表单数据提交时,是保存在 `session` 的键名下的。 现在运行测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 10 files (.ex) - 1) test renders form for new sessions (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:4 + 1) test renders form for new sessions (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:4 ** (ArgumentError) No helper clause for TvRecipe.Router.Helpers.session_path/2 defined for action :create. The following session_path actions are defined under your router: * :new stacktrace: (phoenix) lib/phoenix/router/helpers.ex:269: Phoenix.Router.Helpers.raise_route_error/5 - (tv_recipe) web/templates/session/new.html.eex:2: TvRecipe.SessionView."new.html"/1 + (tv_recipe) web/templates/session/new.html.eex:2: TvRecipeWeb.SessionView."new.html"/1 (tv_recipe) web/templates/layout/app.html.eex:29: TvRecipe.LayoutView."app.html"/1 (phoenix) lib/phoenix/view.ex:335: Phoenix.View.render_to_iodata/3 (phoenix) lib/phoenix/controller.ex:642: Phoenix.Controller.do_render/4 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.action/2 - (tv_recipe) web/controllers/session_controller.ex:1: TvRecipe.SessionController.phoenix_controller_pipeline/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.action/2 + (tv_recipe) web/controllers/session_controller.ex:1: TvRecipeWeb.SessionController.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_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/session_controller_test.exs:5: (test) + test/tv_recipe_web/controllers/session_controller_test.exs:5: (test) @@ -414,10 +414,10 @@ Finished in 0.07 seconds 我们需要在 `router.ex` 文件添加一个路由: ```elixir -diff --git a/web/router.ex b/web/router.ex +diff --git a/lib/tv_recipe_web/router.ex b/lib/tv_recipe_web/router.ex index aac327c..e0406d2 100644 ---- a/web/router.ex -+++ b/web/router.ex +--- a/lib/tv_recipe_web/router.ex ++++ b/lib/tv_recipe_web/router.ex @@ -19,6 +19,7 @@ defmodule TvRecipe.Router do get "/", PageController, :index resources "/users", UserController @@ -434,19 +434,20 @@ index aac327c..e0406d2 100644 如果我们此时在浏览器里访问 `/sessions/new` 页面,并提交用户登录数据,会怎样?不不不,不要在浏览器里尝试,我们用测试代码: ```elixir -diff --git a/test/controllers/session_controller_test.exs b/test/controllers/session_controller_test.exs +diff --git a/test/tv_recipe_web/controllers/session_controller_test.exs b/test/tv_recipe_web/controllers/session_controller_test.exs index 0372448..6835e40 100644 ---- a/test/controllers/session_controller_test.exs -+++ b/test/controllers/session_controller_test.exs +--- a/test/tv_recipe_web/controllers/session_controller_test.exs ++++ b/test/tv_recipe_web/controllers/session_controller_test.exs @@ -1,9 +1,24 @@ - defmodule TvRecipe.SessionControllerTest do + defmodule TvRecipeWeb.SessionControllerTest do use TvRecipe.ConnCase -+ alias TvRecipe.{Repo, User} ++ alias TvRecipe.Repo ++ alias TvRecipe.Users.User + @valid_user_attrs %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("a", 6)} + test "renders form for new sessions", %{conn: conn} do - conn = get conn, session_path(conn, :new) + conn = get conn, Routes.session_path(conn, :new) # 200 响应,页面上带有“登录” assert html_response(conn, 200) =~ "登录" end @@ -456,29 +457,29 @@ index 0372448..6835e40 100644 + # 插入新用户 + Repo.insert! user_changeset + # 用户登录 -+ conn = post conn, session_path(conn, :create), session: @valid_user_attrs ++ conn = post conn, Routes.session_path(conn, :create), session: @valid_user_attrs + # 显示“欢迎你”的消息 + assert get_flash(conn, :info) == "欢迎你" + # 重定向到主页 -+ assert redirected_to(conn) == page_path(conn, :index) ++ assert redirected_to(conn) == Routes.page_path(conn, :index) + end end ``` 我们的测试结果是: ```bash -$ mix test test/controllers/session_controller_test.exs +$ mix test test/tv_recipe_web/controllers/session_controller_test.exs Compiling 1 file (.ex) warning: variable "user" is unused - test/controllers/session_controller_test.exs:16 + test/tv_recipe_web/controllers/session_controller_test.exs:16 . - 1) test login user and redirect to home page when data is valid (TvRecipe.SessionControllerTest) - test/controllers/session_controller_test.exs:13 - ** (UndefinedFunctionError) function TvRecipe.SessionController.create/2 is undefined or private + 1) test login user and redirect to home page when data is valid (TvRecipeWeb.SessionControllerTest) + test/tv_recipe_web/controllers/session_controller_test.exs:13 + ** (UndefinedFunctionError) function TvRecipeWeb.SessionController.create/2 is undefined or private ``` -`TvRecipe.SessionController.create` 未定义。 +`TvRecipeWeb.SessionController.create` 未定义。 打开 `session_controller.ex` 文件,添加 `create` 动作: @@ -488,9 +489,10 @@ index 66a5304..40ad02f 100644 --- a/web/controllers/session_controller.ex +++ b/web/controllers/session_controller.ex @@ -1,7 +1,20 @@ - defmodule TvRecipe.SessionController do - use TvRecipe.Web, :controller -+ alias TvRecipe.{Repo, User} + defmodule TvRecipeWeb.SessionController do + use TvRecipeWeb, :controller ++ alias TvRecipe.Repo ++ alias TvRecipe.Users.User def new(conn, _params) do render conn, "new.html" @@ -504,7 +506,7 @@ index 66a5304..40ad02f 100644 + user && Comeonin.Bcrypt.checkpw(password, user.password_hash) -> + conn + |> put_flash(:info, "欢迎你") -+ |> redirect(to: page_path(conn, :index)) ++ |> redirect(to: Routes.page_path(conn, :index)) + end + end end @@ -520,7 +522,7 @@ index 66a5304..40ad02f 100644 现在运行测试: ```bash -$ mix test test/controllers/session_controller_test.exs +$ mix test test/tv_recipe_web/controllers/session_controller_test.exs .. Finished in 0.2 seconds @@ -536,13 +538,13 @@ Finished in 0.2 seconds 同样的,我们先写测试: ```elixir -diff --git a/test/controllers/session_controller_test.exs b/test/controllers/session_controller_test.exs +diff --git a/test/tv_recipe_web/controllers/session_controller_test.exs b/test/tv_recipe_web/controllers/session_controller_test.exs index cc35f0a..dd5bc02 100644 ---- a/test/controllers/session_controller_test.exs -+++ b/test/controllers/session_controller_test.exs -@@ -21,4 +21,24 @@ defmodule TvRecipe.SessionControllerTest do +--- a/test/tv_recipe_web/controllers/session_controller_test.exs ++++ b/test/tv_recipe_web/controllers/session_controller_test.exs +@@ -21,4 +21,24 @@ defmodule TvRecipeWeb.SessionControllerTest do # 重定向到主页 - assert redirected_to(conn) == page_path(conn, :index) + assert redirected_to(conn) == Routes.page_path(conn, :index) end + + test "redirect to session new when email exists but with wrong password", %{conn: conn} do @@ -550,7 +552,7 @@ index cc35f0a..dd5bc02 100644 + # 插入新用户 + Repo.insert! user_changeset + # 用户登录 -+ conn = post conn, session_path(conn, :create), session: %{@valid_user_attrs | password: ""} ++ conn = post conn, Routes.session_path(conn, :create), session: %{@valid_user_attrs | password: ""} + # 显示“用户名或密码错误” + assert get_flash(conn, :error) == "用户名或密码错误" + # 返回登录页 @@ -558,7 +560,7 @@ index cc35f0a..dd5bc02 100644 + end + + test "redirect to session new when nobody login", %{conn: conn} do -+ conn = post conn, session_path(conn, :create), session: @valid_user_attrs ++ conn = post conn, Routes.session_path(conn, :create), session: @valid_user_attrs + # 显示“用户名或密码错误” + assert get_flash(conn, :error) == "用户名或密码错误" + # 返回登录页 @@ -573,10 +575,10 @@ diff --git a/web/controllers/session_controller.ex b/web/controllers/session_con index 40ad02f..400a33c 100644 --- a/web/controllers/session_controller.ex +++ b/web/controllers/session_controller.ex -@@ -15,6 +15,18 @@ defmodule TvRecipe.SessionController do +@@ -15,6 +15,18 @@ defmodule TvRecipeWeb.SessionController do conn |> put_flash(:info, "欢迎你") - |> redirect(to: page_path(conn, :index)) + |> redirect(to: Routes.page_path(conn, :index)) + # 用户存在,但密码错误 + user -> + conn @@ -596,7 +598,7 @@ index 40ad02f..400a33c 100644 再次测试: ```bash -mix test test/controllers/session_controller_test.exs +mix test test/tv_recipe_web/controllers/session_controller_test.exs .... Finished in 0.2 seconds @@ -615,27 +617,27 @@ Finished in 0.2 seconds 我们来改造下我们的测试代码: ```elixir -diff --git a/test/controllers/session_controller_test.exs b/test/controllers/session_controller_test.exs +diff --git a/test/tv_recipe_web/controllers/session_controller_test.exs b/test/tv_recipe_web/controllers/session_controller_test.exs index dd5bc02..52e8801 100644 ---- a/test/controllers/session_controller_test.exs -+++ b/test/controllers/session_controller_test.exs -@@ -13,13 +13,19 @@ defmodule TvRecipe.SessionControllerTest do +--- a/test/tv_recipe_web/controllers/session_controller_test.exs ++++ b/test/tv_recipe_web/controllers/session_controller_test.exs +@@ -13,13 +13,19 @@ defmodule TvRecipeWeb.SessionControllerTest do test "login user and redirect to home page when data is valid", %{conn: conn} do user_changeset = User.changeset(%User{}, @valid_user_attrs) # 插入新用户 - Repo.insert! user_changeset + user = Repo.insert! user_changeset # 用户登录 - conn = post conn, session_path(conn, :create), session: @valid_user_attrs + conn = post conn, Routes.session_path(conn, :create), session: @valid_user_attrs # 显示“欢迎你”的消息 assert get_flash(conn, :info) == "欢迎你" # 重定向到主页 - assert redirected_to(conn) == page_path(conn, :index) + assert redirected_to(conn) == Routes.page_path(conn, :index) + # 读取首页,页面上包含已登录用户的用户名 -+ conn = get conn, page_path(conn, :index) ++ conn = get conn, Routes.page_path(conn, :index) + assert html_response(conn, 200) =~ Map.get(@valid_user_attrs, :username) + # 读取用户页,页面上包含已登录用户的用户名 -+ conn = get conn, user_path(conn, :show, user) ++ conn = get conn, Routes.user_path(conn, :show, user) + assert html_response(conn, 200) =~ Map.get(@valid_user_attrs, :username) end ``` @@ -660,7 +662,7 @@ index 82259d8..2d39904 100644 @@ -733,7 +735,7 @@ index 0000000..84b17f7 --- /dev/null +++ b/web/controllers/auth.ex @@ -0,0 +1,16 @@ -+defmodule TvRecipe.Auth do ++defmodule TvRecipeWeb.Auth do + import Plug.Conn + + @doc """ @@ -763,7 +765,7 @@ index 0000000..84b17f7 user && Comeonin.Bcrypt.checkpw(password, user.password_hash) -> conn |> put_flash(:info, "欢迎你") - |> redirect(to: page_path(conn, :index)) + |> redirect(to: Routes.page_path(conn, :index)) ``` 用户登录时,我们根据他们提供的邮箱取得数据库中的用户,然后比对密码,如果密码正确,我们就得到了 `user`。 @@ -779,13 +781,13 @@ diff --git a/web/controllers/session_controller.ex b/web/controllers/session_con index 400a33c..b5218f2 100644 --- a/web/controllers/session_controller.ex +++ b/web/controllers/session_controller.ex -@@ -13,6 +13,7 @@ defmodule TvRecipe.SessionController do +@@ -13,6 +13,7 @@ defmodule TvRecipeWeb.SessionController do # 用户存在,且密码正确 user && Comeonin.Bcrypt.checkpw(password, user.password_hash) -> conn + |> put_session(:user_id, user.id) |> put_flash(:info, "欢迎你") - |> redirect(to: page_path(conn, :index)) + |> redirect(to: Routes.page_path(conn, :index)) ``` 然后我们就能在 `auth.ex` 文件中读取 session 中的 `:user_id` 了: @@ -799,22 +801,22 @@ index 84b17f7..994112d 100644 def call(conn, repo) do + user_id = get_session(conn, :user_id) -+ user = user_id && repo.get(TvRecipe.User, user_id) ++ user = user_id && repo.get(TvRecipe.Users.User, user_id) assign(conn, :current_user, user) end ``` 最后,将 `Auth` plug 加入 `:browser` pipeline 中: ```elixir -diff --git a/web/router.ex b/web/router.ex +diff --git a/lib/tv_recipe_web/router.ex b/lib/tv_recipe_web/router.ex index e0406d2..1265c86 100644 ---- a/web/router.ex -+++ b/web/router.ex -@@ -7,6 +7,7 @@ defmodule TvRecipe.Router do +--- a/lib/tv_recipe_web/router.ex ++++ b/lib/tv_recipe_web/router.ex +@@ -7,6 +7,7 @@ defmodule TvRecipeWeb.Router do plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers -+ plug TvRecipe.Auth, repo: TvRecipe.Repo ++ plug TvRecipeWeb.Auth, repo: TvRecipe.Repo end pipeline :api do diff --git a/05-session/02-auto-login-user.md b/05-session/02-auto-login-user.md index 6444ad0..c0c1cdd 100644 --- a/05-session/02-auto-login-user.md +++ b/05-session/02-auto-login-user.md @@ -12,7 +12,7 @@ {:ok, _user} -> conn |> put_flash(:info, "User created successfully.") - |> redirect(to: user_path(conn, :index)) + |> redirect(to: Routes.user_path(conn, :index)) {:error, changeset} -> render(conn, "new.html", changeset: changeset) end @@ -32,12 +32,12 @@ index 95d3108..26055e3 100644 @@ -17,8 +17,11 @@ defmodule TvRecipe.UserControllerTest do test "creates resource and redirects when data is valid", %{conn: conn} do - conn = post conn, user_path(conn, :create), user: @valid_attrs -- assert redirected_to(conn) == user_path(conn, :index) -+ assert redirected_to(conn) == page_path(conn, :index) + conn = post conn, Routes.user_path(conn, :create), user: @valid_attrs +- assert redirected_to(conn) == Routes.user_path(conn, :index) ++ assert redirected_to(conn) == Routes.page_path(conn, :index) assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) + # 注册后自动登录,检查首页是否包含用户名 -+ conn = get conn, page_path(conn, :index) ++ conn = get conn, Routes.page_path(conn, :index) + assert html_response(conn, 200) =~ Map.get(@valid_attrs, :username) end ``` @@ -56,9 +56,9 @@ index 7d13c5f..8d8a6f5 100644 + {:ok, user} -> conn |> put_flash(:info, "User created successfully.") -- |> redirect(to: user_path(conn, :index)) +- |> redirect(to: Routes.user_path(conn, :index)) + |> put_session(:user_id, user.id) -+ |> redirect(to: page_path(conn, :index)) ++ |> redirect(to: Routes.page_path(conn, :index)) {:error, changeset} -> render(conn, "new.html", changeset: changeset) end diff --git a/05-session/03-logout.md b/05-session/03-logout.md index 6c86653..afbaa5c 100644 --- a/05-session/03-logout.md +++ b/05-session/03-logout.md @@ -11,7 +11,7 @@ index c5f4616..511d0ab 100644 +++ b/test/controllers/session_controller_test.exs @@ -26,6 +26,8 @@ defmodule TvRecipe.SessionControllerTest do # 读取用户页,页面上包含已登录用户的用户名 - conn = get conn, user_path(conn, :show, user) + conn = get conn, Routes.user_path(conn, :show, user) assert html_response(conn, 200) =~ Map.get(@valid_user_attrs, :username) + # 登录后的页面显示“退出” + assert html_response(conn, 200) =~ "退出" @@ -52,7 +52,7 @@ index 2d39904..6c87a08 100644
  • Get Started
  • <%= if @current_user do %>
  • <%= link @current_user.username, to: user_path(@conn, :show, @current_user) %>
  • -+
  • <%= link "退出", to: session_path(@conn, :delete, @current_user), method: "delete" %>
  • ++
  • <%= link "退出", to: Routes.session_path(@conn, :delete, @current_user), method: "delete" %>
  • <% end %> @@ -135,12 +135,12 @@ index 511d0ab..969662a 100644 + user = Repo.insert!(changeset) + + # 登录该用户 -+ conn = post conn, session_path(conn, :create), session: Map.delete(@valid_user_attrs, :username) ++ conn = post conn, Routes.session_path(conn, :create), session: Map.delete(@valid_user_attrs, :username) + + # 点击退出 -+ conn = delete conn, session_path(conn, :delete, user) ++ conn = delete conn, Routes.session_path(conn, :delete, user) + assert get_flash(conn, :info) == "退出成功" -+ assert redirected_to(conn) == page_path(conn, :index) ++ assert redirected_to(conn) == Routes.page_path(conn, :index) + end + end @@ -161,7 +161,7 @@ index b5218f2..2a887ee 100644 + conn + |> delete_session(:user_id) + |> put_flash(:info, "退出成功") -+ |> redirect(to: page_path(conn, :index)) ++ |> redirect(to: Routes.page_path(conn, :index)) + end end diff --git a/05-session/04-login-logout-buttons.md b/05-session/04-login-logout-buttons.md index 31de2ba..18e6082 100644 --- a/05-session/04-login-logout-buttons.md +++ b/05-session/04-login-logout-buttons.md @@ -14,11 +14,11 @@ index 969662a..98fbb5a 100644 # 插入新用户 user = Repo.insert! user_changeset + # 未登录情况下访问首页,应带有登录/注册文字 -+ conn = get conn, page_path(conn, :index) ++ conn = get conn, Routes.page_path(conn, :index) + assert html_response(conn, 200) =~ "登录" + assert html_response(conn, 200) =~ "注册" # 用户登录 - conn = post conn, session_path(conn, :create), session: @valid_user_attrs + conn = post conn, Routes.session_path(conn, :create), session: @valid_user_attrs # 显示“欢迎你”的消息 ``` @@ -31,11 +31,11 @@ index 6c87a08..b13f370 100644 +++ b/web/templates/layout/app.html.eex @@ -20,6 +20,9 @@ <%= if @current_user do %> -
  • <%= link @current_user.username, to: user_path(@conn, :show, @current_user) %>
  • -
  • <%= link "退出", to: session_path(@conn, :delete, @current_user), method: "delete" %>
  • +
  • <%= link @current_user.username, to: Routes.user_path(@conn, :show, @current_user) %>
  • +
  • <%= link "退出", to: Routes.session_path(@conn, :delete, @current_user), method: "delete" %>
  • + <% else %> -+
  • <%= link "登录", to: session_path(@conn, :new) %>
  • -+
  • <%= link "注册", to: user_path(@conn, :new) %>
  • ++
  • <%= link "登录", to: Routes.session_path(@conn, :new) %>
  • ++
  • <%= link "注册", to: Routes.user_path(@conn, :new) %>
  • <% end %> @@ -63,7 +63,7 @@ index 6ac524c..0c2eb0a 100644 |> put_session(:user_id, user.id) |> put_flash(:info, "欢迎你") + |> configure_session(renew: true) - |> redirect(to: page_path(conn, :index)) + |> redirect(to: Routes.page_path(conn, :index)) # 用户存在,但密码错误 user -> diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex @@ -75,7 +75,7 @@ index 8d8a6f5..8b9b38b 100644 |> put_flash(:info, "User created successfully.") |> put_session(:user_id, user.id) + |> configure_session(renew: true) - |> redirect(to: page_path(conn, :index)) + |> redirect(to: Routes.page_path(conn, :index)) {:error, changeset} -> render(conn, "new.html", changeset: changeset) ``` @@ -88,7 +88,7 @@ diff --git a/web/controllers/auth.ex b/web/controllers/auth.ex index 994112d..e298b68 100644 --- a/web/controllers/auth.ex +++ b/web/controllers/auth.ex -@@ -15,4 +15,10 @@ defmodule TvRecipe.Auth do +@@ -15,4 +15,10 @@ defmodule TvRecipeWeb.Auth do assign(conn, :current_user, user) end @@ -111,8 +111,8 @@ index 0c2eb0a..6f29ce0 100644 - |> put_session(:user_id, user.id) |> put_flash(:info, "欢迎你") - |> configure_session(renew: true) -+ |> TvRecipe.Auth.login(user) - |> redirect(to: page_path(conn, :index)) ++ |> TvRecipeWeb.Auth.login(user) + |> redirect(to: Routes.page_path(conn, :index)) # 用户存在,但密码错误 user -> diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex @@ -125,8 +125,8 @@ index 8b9b38b..b9234b1 100644 |> put_flash(:info, "User created successfully.") - |> put_session(:user_id, user.id) - |> configure_session(renew: true) -+ |> TvRecipe.Auth.login(user) - |> redirect(to: page_path(conn, :index)) ++ |> TvRecipeWeb.Auth.login(user) + |> redirect(to: Routes.page_path(conn, :index)) {:error, changeset} -> render(conn, "new.html", changeset: changeset) ``` diff --git a/06-restrict-access/06-restrict-access.md b/06-restrict-access/06-restrict-access.md index 5b363e4..5af01b3 100644 --- a/06-restrict-access/06-restrict-access.md +++ b/06-restrict-access/06-restrict-access.md @@ -11,21 +11,21 @@ diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_c index 26055e3..ac6894e 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs -@@ -66,4 +66,18 @@ defmodule TvRecipe.UserControllerTest do - assert redirected_to(conn) == user_path(conn, :index) +@@ -66,4 +66,18 @@ defmodule TvRecipeWeb.UserControllerTest do + assert redirected_to(conn) == Routes.user_path(conn, :index) refute Repo.get(User, user.id) end + + test "guest access user action redirected to login page", %{conn: conn} do + user = Repo.insert! %User{} + Enum.each([ -+ get(conn, user_path(conn, :index)), -+ get(conn, user_path(conn, :show, user)), -+ get(conn, user_path(conn, :edit, user)), -+ put(conn, user_path(conn, :update, user), user: %{}), -+ delete(conn, user_path(conn, :delete, user)) ++ get(conn, Routes.user_path(conn, :index)), ++ get(conn, Routes.user_path(conn, :show, user)), ++ get(conn, Routes.user_path(conn, :edit, user)), ++ put(conn, Routes.user_path(conn, :update, user), user: %{}), ++ delete(conn, Routes.user_path(conn, :delete, user)) + ], fn conn -> -+ assert redirected_to(conn) == session_path(conn, :new) ++ assert redirected_to(conn) == Routes.session_path(conn, :new) + assert conn.halted + end) + end @@ -39,15 +39,15 @@ index b9234b1..7bb7dac 100644 --- a/web/controllers/user_controller.ex +++ b/web/controllers/user_controller.ex @@ -1,5 +1,6 @@ - defmodule TvRecipe.UserController do - use TvRecipe.Web, :controller + defmodule TvRecipeWeb.UserController do + use TvRecipeWeb, :controller + plug :login_require when action in [:index, :show, :edit, :update, :delete] alias TvRecipe.User -@@ -63,4 +64,20 @@ defmodule TvRecipe.UserController do +@@ -63,4 +64,20 @@ defmodule TvRecipeWeb.UserController do |> put_flash(:info, "User deleted successfully.") - |> redirect(to: user_path(conn, :index)) + |> redirect(to: Routes.user_path(conn, :index)) end + + @doc """ @@ -61,7 +61,7 @@ index b9234b1..7bb7dac 100644 + else + conn + |> put_flash(:info, "请先登录") -+ |> redirect(to: session_path(conn, :new)) ++ |> redirect(to: Routes.session_path(conn, :new)) + |> halt() + end + end @@ -99,7 +99,7 @@ conn $ mix test ..................... - 1) test renders form for editing chosen resource (TvRecipe.UserControllerTest) + 1) test renders form for editing chosen resource (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:44 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -110,7 +110,7 @@ $ mix test - 2) test lists all entries on index (TvRecipe.UserControllerTest) + 2) test lists all entries on index (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:8 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -121,7 +121,7 @@ $ mix test - 3) test renders page not found when id is nonexistent (TvRecipe.UserControllerTest) + 3) test renders page not found when id is nonexistent (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:38 expected error to be sent as 404 status, but response sent 302 without error stacktrace: @@ -130,10 +130,10 @@ $ mix test .. - 4) test deletes chosen resource (TvRecipe.UserControllerTest) + 4) test deletes chosen resource (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:63 Assertion with == failed - code: redirected_to(conn) == user_path(conn, :index) + code: redirected_to(conn) == Routes.user_path(conn, :index) left: "/sessions/new" right: "/users" stacktrace: @@ -141,7 +141,7 @@ $ mix test - 5) test shows chosen resource (TvRecipe.UserControllerTest) + 5) test shows chosen resource (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:32 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -152,10 +152,10 @@ $ mix test - 6) test updates chosen resource and redirects when data is valid (TvRecipe.UserControllerTest) + 6) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:50 Assertion with == failed - code: redirected_to(conn) == user_path(conn, :show, user) + code: redirected_to(conn) == Routes.user_path(conn, :show, user) left: "/sessions/new" right: "/users/1121" stacktrace: @@ -163,7 +163,7 @@ $ mix test - 7) test does not update chosen resource and renders errors when data is invalid (TvRecipe.UserControllerTest) + 7) test does not update chosen resource and renders errors when data is invalid (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:57 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -186,8 +186,8 @@ Finished in 0.4 seconds ```elixir test "shows chosen resource", %{conn: conn} do user = Repo.insert! User.changeset(%User{}, @valid_attrs) - conn = post conn, session_path(conn, :create), session: @valid_attrs # <= 这一行,登录用户 - conn = get conn, user_path(conn, :show, user) + conn = post conn, Routes.session_path(conn, :create), session: @valid_attrs # <= 这一行,登录用户 + conn = get conn, Routes.user_path(conn, :show, user) assert html_response(conn, 200) =~ "Show user" end ``` @@ -207,75 +207,38 @@ index ac6894e..e11df40 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs @@ -1,10 +1,22 @@ - defmodule TvRecipe.UserControllerTest do + defmodule TvRecipeWeb.UserControllerTest do use TvRecipe.ConnCase -- alias TvRecipe.User -+ alias TvRecipe.{Repo, User} + alias TvRecipe.Repo + alias TvRecipe.Users + alias TvRecipe.Users.User @valid_attrs %{email: "chenxsan@gmail.com", password: "some content", username: "chenxsan"} @invalid_attrs %{} -+ setup %{conn: conn} = context do -+ if context[:logged_in] == true do -+ # 如果上下文里 :logged_in 值为 true -+ user = Repo.insert! User.changeset(%User{}, @valid_attrs) -+ conn = post conn, session_path(conn, :create), session: @valid_attrs -+ {:ok, [conn: conn, user: user]} -+ else -+ :ok -+ end -+ end -+ -+ @tag logged_in: true - test "lists all entries on index", %{conn: conn} do - conn = get conn, user_path(conn, :index) - assert html_response(conn, 200) =~ "Listing users" -@@ -29,24 +41,28 @@ defmodule TvRecipe.UserControllerTest do - assert html_response(conn, 200) =~ "New user" - end - -+ @tag logged_in: true - test "shows chosen resource", %{conn: conn} do - user = Repo.insert! %User{} - conn = get conn, user_path(conn, :show, user) - assert html_response(conn, 200) =~ "Show user" - end - -+ @tag logged_in: true - test "renders page not found when id is nonexistent", %{conn: conn} do - assert_error_sent 404, fn -> - get conn, user_path(conn, :show, -1) - end - end +- defp create_user(_) do +- user = fixture(:user) +- %{user: user} +- end -+ @tag logged_in: true - test "renders form for editing chosen resource", %{conn: conn} do - user = Repo.insert! %User{} - conn = get conn, user_path(conn, :edit, user) - assert html_response(conn, 200) =~ "Edit user" - end ++ defp login_user(%{conn: conn}) do ++ user = fixture(:user) ++ conn = post conn, Routes.session_path(conn, :create), session: @valid_attrs ++ %{conn: conn, user: user} ++ end -+ @tag logged_in: true - test "updates chosen resource and redirects when data is valid", %{conn: conn} do - user = Repo.insert! %User{} - conn = put conn, user_path(conn, :update, user), user: @valid_attrs -@@ -54,12 +70,14 @@ defmodule TvRecipe.UserControllerTest do - assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) - end + describe "edit user" do +- setup [:create_user] ++ setup [:login_user] -+ @tag logged_in: true - test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do - user = Repo.insert! %User{} - conn = put conn, user_path(conn, :update, user), user: @invalid_attrs - assert html_response(conn, 200) =~ "Edit user" + test "renders form for editing chosen user", %{conn: conn, user: user} do + conn = get(conn, Routes.user_path(conn, :edit, user)) + assert html_response(conn, 200) =~ "Edit User" end + end -+ @tag logged_in: true - test "deletes chosen resource", %{conn: conn} do - user = Repo.insert! %User{} - conn = delete conn, user_path(conn, :delete, user) ``` -我们根据 `logged_in` 的值返回不同 `conn`:一个是用户登录的 conn,一个是未登录的 conn。 +我们根据 `describe` 设置不同的 `setup`:需要用户登录则添加 `setup [:login_user]`, 不需要则不添加或留空 `setup []` 现在运行测试: @@ -283,7 +246,7 @@ index ac6894e..e11df40 100644 $ mix test ........................... - 1) test updates chosen resource and redirects when data is valid (TvRecipe.UserControllerTest) + 1) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:66 ** (RuntimeError) expected redirection with status 302, got: 200 stacktrace: @@ -306,13 +269,12 @@ diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_c index e11df40..c8263c6 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs -@@ -65,7 +65,7 @@ defmodule TvRecipe.UserControllerTest do - @tag logged_in: true +@@ -65,7 +65,7 @@ defmodule TvRecipeWeb.UserControllerTest do test "updates chosen resource and redirects when data is valid", %{conn: conn} do user = Repo.insert! %User{} -- conn = put conn, user_path(conn, :update, user), user: @valid_attrs -+ conn = put conn, user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"} - assert redirected_to(conn) == user_path(conn, :show, user) +- conn = put conn, Routes.user_path(conn, :update, user), user: @valid_attrs ++ conn = put conn, Routes.user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"} + assert redirected_to(conn) == Routes.user_path(conn, :show, user) assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) end ``` @@ -331,8 +293,8 @@ index 7bb7dac..c0056fd 100644 --- a/web/controllers/user_controller.ex +++ b/web/controllers/user_controller.ex @@ -1,14 +1,9 @@ - defmodule TvRecipe.UserController do - use TvRecipe.Web, :controller + defmodule TvRecipeWeb.UserController do + use TvRecipeWeb, :controller - plug :login_require when action in [:index, :show, :edit, :update, :delete] + plug :login_require when action in [:show, :edit, :update] @@ -346,7 +308,7 @@ index 7bb7dac..c0056fd 100644 def new(conn, _params) do changeset = User.changeset(%User{}) render(conn, "new.html", changeset: changeset) -@@ -53,18 +48,6 @@ defmodule TvRecipe.UserController do +@@ -53,18 +48,6 @@ defmodule TvRecipeWeb.UserController do end end @@ -359,7 +321,7 @@ index 7bb7dac..c0056fd 100644 - - conn - |> put_flash(:info, "User deleted successfully.") -- |> redirect(to: user_path(conn, :index)) +- |> redirect(to: Routes.user_path(conn, :index)) - end - @doc """ @@ -372,52 +334,49 @@ diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_c index c8263c6..a2ccee0 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs -@@ -16,12 +16,6 @@ defmodule TvRecipe.UserControllerTest do +@@ -16,12 +16,6 @@ defmodule TvRecipeWeb.UserControllerTest do end end -- @tag logged_in: true - test "lists all entries on index", %{conn: conn} do -- conn = get conn, user_path(conn, :index) +- conn = get conn, Routes.user_path(conn, :index) - assert html_response(conn, 200) =~ "Listing users" - end end end -- @tag logged_in: true - test "lists all entries on index", %{conn: conn} do -- conn = get conn, user_path(conn, :index) +- conn = get conn, Routes.user_path(conn, :index) - assert html_response(conn, 200) =~ "Listing users" - end - test "renders form for new resources", %{conn: conn} do - conn = get conn, user_path(conn, :new) + conn = get conn, Routes.user_path(conn, :new) assert html_response(conn, 200) =~ "New user" -@@ -77,22 +71,12 @@ defmodule TvRecipe.UserControllerTest do +@@ -77,22 +71,12 @@ defmodule TvRecipeWeb.UserControllerTest do assert html_response(conn, 200) =~ "Edit user" end -- @tag logged_in: true - test "deletes chosen resource", %{conn: conn} do - user = Repo.insert! %User{} -- conn = delete conn, user_path(conn, :delete, user) -- assert redirected_to(conn) == user_path(conn, :index) +- conn = delete conn, Routes.user_path(conn, :delete, user) +- assert redirected_to(conn) == Routes.user_path(conn, :index) - refute Repo.get(User, user.id) - end - test "guest access user action redirected to login page", %{conn: conn} do user = Repo.insert! %User{} Enum.each([ -- get(conn, user_path(conn, :index)), - get(conn, user_path(conn, :show, user)), - get(conn, user_path(conn, :edit, user)), - put(conn, user_path(conn, :update, user), user: %{}), -- delete(conn, user_path(conn, :delete, user)) +- get(conn, Routes.user_path(conn, :index)), + get(conn, Routes.user_path(conn, :show, user)), + get(conn, Routes.user_path(conn, :edit, user)), + put(conn, Routes.user_path(conn, :update, user), user: %{}), +- delete(conn, Routes.user_path(conn, :delete, user)) ], fn conn -> - assert redirected_to(conn) == session_path(conn, :new) + assert redirected_to(conn) == Routes.session_path(conn, :new) assert conn.halted ], fn conn -> - assert redirected_to(conn) == session_path(conn, :new) + assert redirected_to(conn) == Routes.session_path(conn, :new) assert conn.halted diff --git a/web/templates/user/edit.html.eex b/web/templates/user/edit.html.eex index 7e08f2b..beae173 100644 @@ -472,7 +431,7 @@ diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_c index a2ccee0..fd57531 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs -@@ -82,4 +82,19 @@ defmodule TvRecipe.UserControllerTest do +@@ -82,4 +82,19 @@ defmodule TvRecipeWeb.UserControllerTest do assert conn.halted end) end @@ -481,12 +440,12 @@ index a2ccee0..fd57531 100644 + test "does not allow access to other user path", %{conn: conn, user: user} do + another_user = Repo.insert! %User{} + Enum.each([ -+ get(conn, user_path(conn, :show, another_user)), -+ get(conn, user_path(conn, :edit, another_user)), -+ put(conn, user_path(conn, :update, another_user), user: %{}) ++ get(conn, Routes.user_path(conn, :show, another_user)), ++ get(conn, Routes.user_path(conn, :edit, another_user)), ++ put(conn, Routes.user_path(conn, :update, another_user), user: %{}) + ], fn conn -> + assert get_flash(conn, :error) == "禁止访问未授权页面" -+ assert redirected_to(conn) == user_path(conn, :show, user) ++ assert redirected_to(conn) == Routes.user_path(conn, :show, user) + assert conn.halted + end) + end @@ -501,14 +460,14 @@ index c0056fd..520d986 100644 --- a/web/controllers/user_controller.ex +++ b/web/controllers/user_controller.ex @@ -1,6 +1,7 @@ - defmodule TvRecipe.UserController do - use TvRecipe.Web, :controller + defmodule TvRecipeWeb.UserController do + use TvRecipeWeb, :controller plug :login_require when action in [:show, :edit, :update] + plug :self_require when action in [:show, :edit, :update] alias TvRecipe.User -@@ -63,4 +64,21 @@ defmodule TvRecipe.UserController do +@@ -63,4 +64,21 @@ defmodule TvRecipeWeb.UserController do |> halt() end end @@ -525,7 +484,7 @@ index c0056fd..520d986 100644 + else + conn + |> put_flash(:error, "禁止访问未授权页面") -+ |> redirect(to: user_path(conn, :show, conn.assigns.current_user)) ++ |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user)) + |> halt() + end + end @@ -539,7 +498,7 @@ index c0056fd..520d986 100644 $ mix test .................... - 1) test shows chosen resource (TvRecipe.UserControllerTest) + 1) test shows chosen resource (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:39 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -550,7 +509,7 @@ $ mix test - 2) test renders form for editing chosen resource (TvRecipe.UserControllerTest) + 2) test renders form for editing chosen resource (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:53 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -561,10 +520,10 @@ $ mix test .... - 3) test updates chosen resource and redirects when data is valid (TvRecipe.UserControllerTest) + 3) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:60 Assertion with == failed - code: redirected_to(conn) == user_path(conn, :show, user) + code: redirected_to(conn) == Routes.user_path(conn, :show, user) left: "/users/2948" right: "/users/2949" stacktrace: @@ -572,7 +531,7 @@ $ mix test - 4) test renders page not found when id is nonexistent (TvRecipe.UserControllerTest) + 4) test renders page not found when id is nonexistent (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:46 expected error to be sent as 404 status, but response sent 302 without error stacktrace: @@ -581,7 +540,7 @@ $ mix test . - 5) test does not update chosen resource and renders errors when data is invalid (TvRecipe.UserControllerTest) + 5) test does not update chosen resource and renders errors when data is invalid (TvRecipeWeb.UserControllerTest) test/controllers/user_controller_test.exs:68 ** (RuntimeError) expected response with status 200, got: 302, with body: You are being redirected. @@ -602,7 +561,7 @@ diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_c index fd57531..a1b75c6 100644 --- a/test/controllers/user_controller_test.exs +++ b/test/controllers/user_controller_test.exs -@@ -3,6 +3,7 @@ defmodule TvRecipe.UserControllerTest do +@@ -3,6 +3,7 @@ defmodule TvRecipeWeb.UserControllerTest do alias TvRecipe.{Repo, User} @valid_attrs %{email: "chenxsan@gmail.com", password: "some content", username: "chenxsan"} @@ -610,52 +569,46 @@ index fd57531..a1b75c6 100644 @invalid_attrs %{} setup %{conn: conn} = context do -@@ -36,37 +37,26 @@ defmodule TvRecipe.UserControllerTest do +@@ -36,37 +37,26 @@ defmodule TvRecipeWeb.UserControllerTest do end - @tag logged_in: true - test "shows chosen resource", %{conn: conn} do - user = Repo.insert! %User{} + test "shows chosen resource", %{conn: conn, user: user} do - conn = get conn, user_path(conn, :show, user) + conn = get conn, Routes.user_path(conn, :show, user) assert html_response(conn, 200) =~ "Show user" end - @tag logged_in: true - test "renders page not found when id is nonexistent", %{conn: conn} do - assert_error_sent 404, fn -> -- get conn, user_path(conn, :show, -1) - @tag logged_in: true +- get conn, Routes.user_path(conn, :show, -1) - test "renders page not found when id is nonexistent", %{conn: conn} do - assert_error_sent 404, fn -> -- get conn, user_path(conn, :show, -1) +- get conn, Routes.user_path(conn, :show, -1) - end - end - -- @tag logged_in: true - test "renders form for editing chosen resource", %{conn: conn} do - user = Repo.insert! %User{} + test "renders form for editing chosen resource", %{conn: conn, user: user} do - conn = get conn, user_path(conn, :edit, user) + conn = get conn, Routes.user_path(conn, :edit, user) assert html_response(conn, 200) =~ "Edit user" end - @tag logged_in: true - test "updates chosen resource and redirects when data is valid", %{conn: conn} do - user = Repo.insert! %User{} -- conn = put conn, user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"} +- conn = put conn, Routes.user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"} + test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user} do -+ conn = put conn, user_path(conn, :update, user), user: @another_valid_attrs - assert redirected_to(conn) == user_path(conn, :show, user) ++ conn = put conn, Routes.user_path(conn, :update, user), user: @another_valid_attrs + assert redirected_to(conn) == Routes.user_path(conn, :show, user) - assert Repo.get_by(User, @valid_attrs |> Map.delete(:password)) + assert Repo.get_by(User, @another_valid_attrs |> Map.delete(:password)) end - @tag logged_in: true - test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do - user = Repo.insert! %User{} + test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user} do - conn = put conn, user_path(conn, :update, user), user: @invalid_attrs + conn = put conn, Routes.user_path(conn, :update, user), user: @invalid_attrs assert html_response(conn, 200) =~ "Edit user" end ``` diff --git a/07-recipe/01-gen-html.md b/07-recipe/01-gen-html.md index c87a8b7..a5116be 100644 --- a/07-recipe/01-gen-html.md +++ b/07-recipe/01-gen-html.md @@ -13,10 +13,10 @@ episode|integer|第几集|必填|1 content|text|内容|必填| user_id|integer|关联用户 id|必填| -这里我们可以直接使用 `mix phoenix.gen.html` 命令来生成菜谱相关的所有文件: +这里我们可以直接使用 `mix phx.gen.html` 命令来生成菜谱相关的所有文件: ```bash -$ mix phoenix.gen.html Recipe recipes name title season:integer episode:integer content:text user_id:references:users +$ mix phx.gen.html Recipes Recipe recipes name title season:integer episode:integer content:text user_id:references:users * creating web/controllers/recipe_controller.ex * creating web/templates/recipe/edit.html.eex * creating web/templates/recipe/form.html.eex @@ -37,7 +37,7 @@ Remember to update your repository by running migrations: $ mix ecto.migrate ``` -![mix phoenix.gen.html Recipe](/img/07-generate-recipe.png) +![mix phx.gen.html Recipe](/img/07-generate-recipe.png) 我们先按照提示把 `resources "/recipes", RecipeController` 加入 `web/router.ex` 文件中: @@ -46,7 +46,7 @@ diff --git a/web/router.ex b/web/router.ex index e0811dc..a6d7cd5 100644 --- a/web/router.ex +++ b/web/router.ex -@@ -20,6 +20,7 @@ defmodule TvRecipe.Router do +@@ -20,6 +20,7 @@ defmodule TvRecipeWeb.Router do get "/", PageController, :index resources "/users", UserController, except: [:index, :delete] resources "/sessions", SessionController, only: [:new, :create, :delete] @@ -56,21 +56,24 @@ index e0811dc..a6d7cd5 100644 但请不要着急执行 `mix ecto.migrate`,我们有几个需要调整的地方: +注: 运行了的话可使用 `mix ecto.rollback` 回撤修改 + 1. 新建的 `priv/repo/migrations/20170206013306_create_recipe.exs` 文件中,有如下一句代码: ```elixir add :user_id, references(:users, on_delete: :nothing) ``` `on_delete` 决定 `recipe` 关联的 `user` 被删时,我们要如何处置 `recipe`。`:nothing` 表示不动 `recipe`,`:delete_all` 表示悉数删除,这里我们使用 `:delete_all`。 -2. 新建的 `web/models/recipe.ex` 文件中,有一句代码: +2. 新建的 `lib/tv_recipe/recipes/recipe.ex` 文件中,有一句代码要替换: ```elixir - belongs_to :user, TvRecipe.User + -field :user_id, :id + +belongs_to :user, TvRecipe.Users.User ``` 因为 `Recipe` 与 `User` 的关系是双向的,所以我们需要在 `user.ex` 文件中增加一句: ```elixir - has_many :recipes, TvRecipe.Recipe + has_many :recipes, TvRecipe.Recipes.Recipe ``` 3. 我们需要在 `recipe.ex` 文件中给 `season` 与 `episode` 设置默认值: diff --git a/07-recipe/02-recipe-scheme.md b/07-recipe/02-recipe-scheme.md index ce94dd5..57e605f 100644 --- a/07-recipe/02-recipe-scheme.md +++ b/07-recipe/02-recipe-scheme.md @@ -2,7 +2,7 @@ 在开发用户时,我们曾经分章节完成各个属性。但这里不再细分。 -我们来看下 `mix phoenix.gen.html` 命令生成的 `recipe_test.exs` 文件内容: +我们来看下 `mix phx.gen.html` 命令生成的 `recipe_test.exs` 文件内容: ```elixir defmodule TvRecipe.RecipeTest do @@ -31,10 +31,10 @@ end 我们先增加测试: ```elixir -diff --git a/test/models/recipe_test.exs b/test/models/recipe_test.exs +diff --git a/test/tv_recipe/recipes/recipe_test.exs b/test/tv_recipe/recipes/recipe_test.exs index a974aad..27f02ea 100644 ---- a/test/models/recipe_test.exs -+++ b/test/models/recipe_test.exs +--- a/test/tv_recipe/recipes/recipe_test.exs ++++ b/test/tv_recipe/recipes/recipe_test.exs @@ -15,4 +15,29 @@ defmodule TvRecipe.RecipeTest do changeset = Recipe.changeset(%Recipe{}, @invalid_attrs) refute changeset.valid? @@ -42,37 +42,37 @@ index a974aad..27f02ea 100644 + + test "name is required" do + attrs = %{@valid_attrs | name: ""} -+ assert {:name, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{name: ["请填写"]} = errors_on(%Recipe{}, attrs) + end + + test "title is required" do + attrs = %{@valid_attrs | title: ""} -+ assert {:title, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{title: ["请填写"]} = errors_on(%Recipe{}, attrs) + end + + test "season is required" do + attrs = %{@valid_attrs | season: nil} -+ assert {:season, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{season: ["请填写"]} = errors_on(%Recipe{}, attrs) + end + + test "episode is required" do + attrs = %{@valid_attrs | episode: nil} -+ assert {:episode, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{episode: ["请填写"]} = errors_on(%Recipe{}, attrs) + end + + test "season should greater than 0" do + attrs = %{@valid_attrs | season: 0} -+ assert {:season, "请输入大于 0 的数字"} in errors_on(%Recipe{}, attrs) ++ assert %{season: ["请输入大于 0 的数字"]} = errors_on(%Recipe{}, attrs) + end + + test "episode should greater than 0" do + attrs = %{@valid_attrs | episode: 0} -+ assert {:episode, "请输入大于 0 的数字"} in errors_on(%Recipe{}, attrs) ++ assert %{episode: ["请输入大于 0 的数字"]} = errors_on(%Recipe{}, attrs) + end + + test "content is required" do + attrs = %{@valid_attrs | content: ""} -+ assert {:content, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{content: ["请填写"]} = errors_on(%Recipe{}, attrs) + end end ``` @@ -106,13 +106,13 @@ index 946d45c..8d34ed2 100644 我们先处理 `user_id` 必填的规则,补充一个测试,如下: ```elixir -diff --git a/test/models/recipe_test.exs b/test/models/recipe_test.exs +diff --git a/test/tv_recipe/recipes/recipe_test.exs b/test/tv_recipe/recipes/recipe_test.exs index 27f02ea..3a9630b 100644 ---- a/test/models/recipe_test.exs -+++ b/test/models/recipe_test.exs +--- a/test/tv_recipe/recipes/recipe_test.exs ++++ b/test/tv_recipe/recipes/recipe_test.exs @@ -3,7 +3,7 @@ defmodule TvRecipe.RecipeTest do - alias TvRecipe.Recipe + alias TvRecipe..Recipes.Recipe - @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content"} + @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content", user_id: 1} @@ -121,12 +121,12 @@ index 27f02ea..3a9630b 100644 test "changeset with valid attributes" do @@ -40,4 +40,9 @@ defmodule TvRecipe.RecipeTest do attrs = %{@valid_attrs | content: ""} - assert {:content, "请填写"} in errors_on(%Recipe{}, attrs) + assert %{content: ["请填写"]} = errors_on(%Recipe{}, attrs) end + + test "user_id is required" do + attrs = %{@valid_attrs | user_id: nil} -+ assert {:user_id, "请填写"} in errors_on(%Recipe{}, attrs) ++ assert %{user_id: ["请填写"]} = errors_on(%Recipe{}, attrs) + end end ``` @@ -151,7 +151,7 @@ index 8d34ed2..0520582 100644 运行新增的测试: ```bash -$ mix test test/models/recipe_test.exs:54 +$ mix test test/tv_recipe/recipes/recipe_test.exs:54 Including tags: [line: "54"] Excluding tags: [:test] @@ -167,34 +167,35 @@ Finished in 0.1 seconds 我们在 `recipe_test.exs` 文件中再增加一个测试,确保 `user_id` 所指的用户存在: ```elixir -diff --git a/test/models/recipe_test.exs b/test/models/recipe_test.exs +diff --git a/test/tv_recipe/recipes/recipe_test.exs b/test/tv_recipe/recipes/recipe_test.exs index 3a9630b..2e1191c 100644 ---- a/test/models/recipe_test.exs -+++ b/test/models/recipe_test.exs +--- a/test/tv_recipe/recipes/recipe_test.exs ++++ b/test/tv_recipe/recipes/recipe_test.exs @@ -1,7 +1,7 @@ defmodule TvRecipe.RecipeTest do use TvRecipe.ModelCase - alias TvRecipe.Recipe -+ alias TvRecipe.{Repo, Recipe} ++ alias TvRecipe.Repo ++ alias TvRecipe.Recipes.Recipe @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content", user_id: 1} @invalid_attrs %{} @@ -45,4 +45,9 @@ defmodule TvRecipe.RecipeTest do attrs = %{@valid_attrs | user_id: nil} - assert {:user_id, "请填写"} in errors_on(%Recipe{}, attrs) + assert %{user_id: ["请填写"]} = 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) ++ assert %{user_id: ["用户不存在"]} = errors_on(changeset) + end end ``` 运行新增的测试: ```bash -$ mix test test/models/recipe_test.exs:59 +$ mix test test/tv_recipe/recipes/recipe_test.exs:59 Compiling 13 files (.ex) Including tags: [line: "59"] Excluding tags: [:test] @@ -202,7 +203,7 @@ Excluding tags: [:test] 1) test user_id should exist in users table (TvRecipe.RecipeTest) - test/models/recipe_test.exs:59 + test/tv_recipe/recipes/recipe_test.exs:59 ** (Ecto.ConstraintError) constraint error when attempting to insert struct: * foreign_key: recipes_user_id_fkey @@ -221,7 +222,7 @@ Excluding tags: [:test] (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:60: (test) + test/tv_recipe/recipes/recipe_test.exs:60: (test) @@ -246,7 +247,7 @@ index 0520582..a0b42fd 100644 再次运行测试: ```bash -$ mix test test/models/recipe_test.exs:59 +$ mix test test/tv_recipe/recipes/recipe_test.exs:59 Compiling 13 files (.ex) Including tags: [line: "59"] Excluding tags: [:test] diff --git a/07-recipe/03-recipe-controller.md b/07-recipe/03-recipe-controller.md index d550164..a5d5a5d 100644 --- a/07-recipe/03-recipe-controller.md +++ b/07-recipe/03-recipe-controller.md @@ -9,66 +9,93 @@ 一个粗暴的解决办法,是在每个测试中新建一个用户,然后把用户 id 传给 `@valid_attrs`,但那样又要重复一堆代码,我们可以把新建用户部分抽取到 `setup` 中: ```elixir -diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs -index 646ebf2..51fdeab 100644 ---- a/test/controllers/recipe_controller_test.exs -+++ b/test/controllers/recipe_controller_test.exs -@@ -1,10 +1,16 @@ - defmodule TvRecipe.RecipeControllerTest do - use TvRecipe.ConnCase - -- alias TvRecipe.Recipe -+ alias TvRecipe.{Repo, User, Recipe} - @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content"} - @invalid_attrs %{} - -+ setup do -+ user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)}) -+ attrs = Map.put(@valid_attrs, :user_id, user.id) -+ {:ok, [attrs: attrs]} -+ end +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) + - test "lists all entries on index", %{conn: conn} do - conn = get conn, recipe_path(conn, :index) - assert html_response(conn, 200) =~ "Listing recipes" -@@ -15,10 +21,10 @@ defmodule TvRecipe.RecipeControllerTest do - assert html_response(conn, 200) =~ "New recipe" - end -+ user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)}) -+ attrs = Map.put(@valid_attrs, :user_id, user.id) -+ {:ok, [attrs: attrs]} ++ context ++ |> Map.put(:attrs, attrs) + end + - test "lists all entries on index", %{conn: conn} do - conn = get conn, recipe_path(conn, :index) - assert html_response(conn, 200) =~ "Listing recipes" -@@ -15,10 +21,10 @@ defmodule TvRecipe.RecipeControllerTest do - assert html_response(conn, 200) =~ "New recipe" - end - -- test "creates resource and redirects when data is valid", %{conn: conn} do -- conn = post conn, recipe_path(conn, :create), recipe: @valid_attrs -+ test "creates resource and redirects when data is valid", %{conn: conn, attrs: attrs} do -+ conn = post conn, recipe_path(conn, :create), recipe: attrs - assert redirected_to(conn) == recipe_path(conn, :index) -- assert Repo.get_by(Recipe, @valid_attrs) -+ assert Repo.get_by(Recipe, attrs) - end - - test "does not create resource and renders errors when data is invalid", %{conn: conn} do -@@ -44,11 +50,11 @@ defmodule TvRecipe.RecipeControllerTest do - assert html_response(conn, 200) =~ "Edit recipe" + 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 - -- test "updates chosen resource and redirects when data is valid", %{conn: conn} do -+ test "updates chosen resource and redirects when data is valid", %{conn: conn, attrs: attrs} do - recipe = Repo.insert! %Recipe{} -- conn = put conn, recipe_path(conn, :update, recipe), recipe: @valid_attrs -+ conn = put conn, recipe_path(conn, :update, recipe), recipe: attrs - assert redirected_to(conn) == recipe_path(conn, :show, recipe) -- assert Repo.get_by(Recipe, @valid_attrs) -+ assert Repo.get_by(Recipe, attrs) + +- 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`,然后返回。 @@ -101,103 +128,82 @@ delete|需要 都要登录?难道未登录用户不能查看其它用户创建的菜谱?当然可以,但我们将新建路由来满足这些需求。这一节,我们开发的是 Recipe 相关的管理动作。 -前面章节中我们已经尝试过使用 `tag` 来标注用户登录状态下的测试,现在根据上面罗列的需求来修改 `recipe_controller_test.exs` 文件中的测试: +前面章节中我们已经尝试过使用 `setup [:login_user]` 来标注用户登录状态下的测试,现在根据上面罗列的需求来修改 `recipe_controller_test.exs` 文件中的测试: ```elixir -diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/recipe_controller_test.exs -index 51fdeab..5632f8c 100644 ---- a/test/controllers/recipe_controller_test.exs -+++ b/test/controllers/recipe_controller_test.exs -@@ -5,51 +5,65 @@ defmodule TvRecipe.RecipeControllerTest do - @valid_attrs %{content: "some content", episode: 42, name: "some content", season: 42, title: "some content"} - @invalid_attrs %{} - -- setup do -- user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)}) -- attrs = Map.put(@valid_attrs, :user_id, user.id) -- {:ok, [attrs: attrs]} -+ setup %{conn: conn} = context do +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(@valid_attrs, :user_id, user.id) -+ if context[:logged_in] == true do -+ conn = post conn, session_path(conn, :create), session: user_attrs -+ {:ok, [conn: conn, attrs: attrs]} -+ else -+ {:ok, [attrs: attrs]} -+ end - end -- ++ attrs = Map.put(@create_attrs, :user_id, user.id) ++ conn = post conn, Routes.session_path(conn, :create), session: user_attrs + -+ @tag logged_in: true - test "lists all entries on index", %{conn: conn} do - conn = get conn, recipe_path(conn, :index) - assert html_response(conn, 200) =~ "Listing recipes" - end - -+ @tag logged_in: true - test "renders form for new resources", %{conn: conn} do - conn = get conn, recipe_path(conn, :new) - assert html_response(conn, 200) =~ "New recipe" - end - -+ @tag logged_in: true - test "creates resource and redirects when data is valid", %{conn: conn, attrs: attrs} do - conn = post conn, recipe_path(conn, :create), recipe: attrs - assert redirected_to(conn) == recipe_path(conn, :index) - assert Repo.get_by(Recipe, attrs) - end - -+ @tag logged_in: true - test "does not create resource and renders errors when data is invalid", %{conn: conn} do - conn = post conn, recipe_path(conn, :create), recipe: @invalid_attrs - assert html_response(conn, 200) =~ "New recipe" - end - -+ @tag logged_in: true - test "shows chosen resource", %{conn: conn} do - recipe = Repo.insert! %Recipe{} - conn = get conn, recipe_path(conn, :show, recipe) - assert html_response(conn, 200) =~ "Show recipe" - end - -+ @tag logged_in: true -+ @tag logged_in: true - test "renders page not found when id is nonexistent", %{conn: conn} do - assert_error_sent 404, fn -> - get conn, recipe_path(conn, :show, -1) - end - end - -+ @tag logged_in: true - test "renders form for editing chosen resource", %{conn: conn} do - recipe = Repo.insert! %Recipe{} - conn = get conn, recipe_path(conn, :edit, recipe) - assert html_response(conn, 200) =~ "Edit recipe" - end - -+ @tag logged_in: true - test "updates chosen resource and redirects when data is valid", %{conn: conn, attrs: attrs} do - recipe = Repo.insert! %Recipe{} - conn = put conn, recipe_path(conn, :update, recipe), recipe: attrs -@@ -57,12 +71,14 @@ defmodule TvRecipe.RecipeControllerTest do - assert Repo.get_by(Recipe, attrs) - end - -+ @tag logged_in: true - test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do - recipe = Repo.insert! %Recipe{} - conn = put conn, recipe_path(conn, :update, recipe), recipe: @invalid_attrs - assert html_response(conn, 200) =~ "Edit recipe" - end - -+ @tag logged_in: true - test "deletes chosen resource", %{conn: conn} do - recipe = Repo.insert! %Recipe{} - conn = delete conn, recipe_path(conn, :delete, recipe) -``` - -我们给所有测试代码都加上了 `@tag logged_in` 的标签。 ++ 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]` 的设置。 接下来我们需要一个验证用户登录状态的 plug,不巧我们在 `user_controller.ex` 文件中已经定义了一个 `login_require` 的 plug,现在是其它地方也要用到它 - 再放在 `user_controller.ex` 中并不合适,我们将它移到 `auth.ex` 文件中: @@ -207,14 +213,14 @@ index e298b68..3dd3e7f 100644 --- a/web/controllers/auth.ex +++ b/web/controllers/auth.ex @@ -1,5 +1,7 @@ - defmodule TvRecipe.Auth do + defmodule TvRecipeWeb.Auth do import Plug.Conn + import Phoenix.Controller -+ alias TvRecipe.Router.Helpers ++ use TvRecipeWeb, :controller @doc """ 初始化选项 -@@ -21,4 +23,37 @@ defmodule TvRecipe.Auth do +@@ -21,4 +23,37 @@ defmodule TvRecipeWeb.Auth do |> configure_session(renew: true) end @@ -229,7 +235,7 @@ index e298b68..3dd3e7f 100644 + else + conn + |> put_flash(:info, "请先登录") -+ |> redirect(to: Helpers.session_path(conn, :new)) ++ |> redirect(to: Routes.session_path(conn, :new)) + |> halt() + end + end @@ -246,7 +252,7 @@ index e298b68..3dd3e7f 100644 + else + conn + |> put_flash(:error, "禁止访问未授权页面") -+ |> redirect(to: Helpers.user_path(conn, :show, conn.assigns.current_user)) ++ |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user)) + |> halt() + end + end @@ -272,7 +278,7 @@ index 520d986..0f023d3 100644 - else - conn - |> put_flash(:info, "请先登录") -- |> redirect(to: session_path(conn, :new)) +- |> redirect(to: Routes.session_path(conn, :new)) - |> halt() - end - end @@ -291,7 +297,7 @@ index 520d986..0f023d3 100644 - else - conn - |> put_flash(:info, "请先登录") -- |> redirect(to: session_path(conn, :new)) +- |> redirect(to: Routes.session_path(conn, :new)) - |> halt() - end - end @@ -308,7 +314,7 @@ index 520d986..0f023d3 100644 - else - conn - |> put_flash(:error, "禁止访问未授权页面") -- |> redirect(to: user_path(conn, :show, conn.assigns.current_user)) +- |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user)) - |> halt() - end - end @@ -317,23 +323,20 @@ index 520d986..0f023d3 100644 注意,我们并非只是简单的移动文本到 `auth.ex` 文件中。在 `auth.ex` 头部,我们还引入了两行代码,并调整了两个 plug: ```elixir -import Phoenix.Controller -alias TvRecipe.Router.Helpers + import Phoenix.Controller + use TvRecipeWeb, :controller ``` -`import Phoenix.Controller` 导入 `put_flash` 等方法,而 `alias TvRecipe.Router.Helpers` 让我们在 `auth.ex` 中可以快速书写各种路径。 +`import Phoenix.Controller` 导入 `put_flash` 等方法,而 `use TvRecipeWeb, :controller` 让我们在 `auth.ex` 中可以快速书写各种路径。 -接着在 `web.ex` 文件中 `import` 它: +接着在 `user_controller.ex` 文件中 `import` 它: ```elixir -diff --git a/web/web.ex b/web/web.ex +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 TvRecipe.Router.Helpers - import TvRecipe.Gettext -+ import TvRecipe.Auth, only: [login_require: 2, self_require: 2] ++ import TvRecipeWeb.Auth, only: [login_require: 2, self_require: 2] end end ``` @@ -347,8 +350,9 @@ index 96a0276..c74b492 100644 --- a/web/controllers/recipe_controller.ex +++ b/web/controllers/recipe_controller.ex @@ -1,6 +1,6 @@ - defmodule TvRecipe.RecipeController do - use TvRecipe.Web, :controller + defmodule TvRecipeWeb.RecipeController do + use TvRecipeWeb, :controller + import TvRecipeWeb.Auth, only: [login_require: 2] - + plug :login_require alias TvRecipe.Recipe @@ -375,7 +379,7 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index 5632f8c..faf67ca 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -16,7 +16,7 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -16,7 +16,7 @@ defmodule TvRecipeWeb.RecipeControllerTest do {:ok, [attrs: attrs]} end end @@ -383,24 +387,24 @@ index 5632f8c..faf67ca 100644 + @tag logged_in: true test "lists all entries on index", %{conn: conn} do - conn = get conn, recipe_path(conn, :index) -@@ -85,4 +85,20 @@ defmodule TvRecipe.RecipeControllerTest do - assert redirected_to(conn) == recipe_path(conn, :index) + 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, recipe_path(conn, :index)), -+ get(conn, recipe_path(conn, :new)), -+ get(conn, recipe_path(conn, :create), recipe: %{}), -+ get(conn, recipe_path(conn, :show, recipe)), -+ get(conn, recipe_path(conn, :edit, recipe)), -+ put(conn, recipe_path(conn, :update, recipe), recipe: %{}), -+ put(conn, recipe_path(conn, :delete, recipe)), ++ 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) == session_path(conn, :new) ++ assert redirected_to(conn) == Routes.session_path(conn, :new) + assert conn.halted + end) + end @@ -427,18 +431,17 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index faf67ca..d8157a2 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -101,4 +101,17 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -101,4 +101,17 @@ defmodule TvRecipeWeb.RecipeControllerTest do assert conn.halted 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 创建一个菜谱 + new_attrs = %{attrs | user_id: user.id} -+ post conn, recipe_path(conn, :create), recipe: new_attrs ++ post conn, Routes.recipe_path(conn, :create), recipe: new_attrs + # 用户 chenxsan 只能创建自己的菜谱,无法替 samchen 创建菜谱 + assert Repo.get_by(Recipe, attrs) + # samchen 不应该有菜谱 @@ -452,7 +455,7 @@ index faf67ca..d8157a2 100644 $ mix test .......................... - 1) test creates resource and redirects when data is valid but with other user_id (TvRecipe.RecipeControllerTest) + 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) @@ -534,54 +537,44 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index d8157a2..d953315 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -7,13 +7,12 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -7,13 +7,12 @@ defmodule TvRecipeWeb.RecipeControllerTest do - setup %{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) +- defp init_attrs(%{user: user} = context) do ++ defp init_attrs(%{conn: conn} = context) do - attrs = Map.put(@valid_attrs, :user_id, user.id) -+ Repo.insert! User.changeset(%User{}, user_attrs) - if context[:logged_in] == true do - conn = post conn, session_path(conn, :create), session: user_attrs -- {:ok, [conn: conn, attrs: attrs]} -+ {:ok, [conn: conn]} - else -- {:ok, [attrs: attrs]} -+ :ok - end +- + context + |> Map.put :attrs, @@valid_attrs end -@@ -30,10 +29,10 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -30,10 +29,10 @@ defmodule TvRecipeWeb.RecipeControllerTest do end - @tag logged_in: true - test "creates resource and redirects when data is valid", %{conn: conn, attrs: attrs} do -- conn = post conn, recipe_path(conn, :create), recipe: attrs +- 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, recipe_path(conn, :create), recipe: @valid_attrs - assert redirected_to(conn) == recipe_path(conn, :index) ++ 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 - @tag logged_in: true -@@ -64,11 +63,11 @@ defmodule TvRecipe.RecipeControllerTest do -@@ -64,11 +63,11 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -64,11 +63,11 @@ defmodule TvRecipeWeb.RecipeControllerTest do +@@ -64,11 +63,11 @@ defmodule TvRecipeWeb.RecipeControllerTest do end - @tag logged_in: true - 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, recipe_path(conn, :update, recipe), recipe: attrs -+ conn = put conn, recipe_path(conn, :update, recipe), recipe: @valid_attrs - assert redirected_to(conn) == recipe_path(conn, :show, 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 - @tag logged_in: true -@@ -102,16 +101,4 @@ defmodule TvRecipe.RecipeControllerTest do + +@@ -102,16 +101,4 @@ defmodule TvRecipeWeb.RecipeControllerTest do end) end @@ -591,7 +584,7 @@ index d8157a2..d953315 100644 - user = Repo.insert! User.changeset(%User{}, %{email: "chenxsan+1@gmail.com", username: "samchen", password: String.duplicate("1", 6)}) - # 将新用户的 id 更新入 attrs,尝试替 samchen 创建一个菜谱 - new_attrs = %{attrs | user_id: user.id} -- post conn, recipe_path(conn, :create), recipe: new_attrs +- post conn, Routes.recipe_path(conn, :create), recipe: new_attrs - # 用户 chenxsan 只能创建自己的菜谱,无法替 samchen 创建菜谱 - assert Repo.get_by(Recipe, attrs) - # samchen 不应该有菜谱 @@ -613,6 +606,16 @@ index c74b492..967b7bc 100644 @@ -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 = @@ -662,32 +665,17 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index d953315..b901b61 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -7,10 +7,10 @@ defmodule TvRecipe.RecipeControllerTest do - - setup %{conn: conn} = context do - user_attrs = %{email: "chenxsan@gmail.com", username: "chenxsan", password: String.duplicate("1", 6)} -- Repo.insert! User.changeset(%User{}, user_attrs) -+ user = Repo.insert! User.changeset(%User{}, user_attrs) - if context[:logged_in] == true do - conn = post conn, session_path(conn, :create), session: user_attrs -- {:ok, [conn: conn]} -+ {:ok, [conn: conn, user: user]} - else - :ok - end -@@ -29,10 +29,10 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -29,10 +29,10 @@ defmodule TvRecipeWeb.RecipeControllerTest do end - @tag logged_in: true - 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, recipe_path(conn, :create), recipe: @valid_attrs - assert redirected_to(conn) == recipe_path(conn, :index) + 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 - @tag logged_in: true ``` 运行测试: @@ -711,23 +699,22 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index b901b61..cdbc420 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -101,4 +101,19 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -101,4 +101,19 @@ defmodule TvRecipeWeb.RecipeControllerTest do end) end -+ @tag logged_in: true + test "user should not allowed to show recipe of other people", %{conn: conn, user: user} do + # 当前登录用户创建了一个菜谱 -+ conn = post conn, recipe_path(conn, :create), recipe: @valid_attrs ++ 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, session_path(conn, :create), session: new_user_attrs ++ conn = post conn, Routes.session_path(conn, :create), session: new_user_attrs + # 读取前头的 recipe 失败,因为它不属于新用户所有 + assert_error_sent 404, fn -> -+ get conn, recipe_path(conn, :show, recipe) ++ get conn, Routes.recipe_path(conn, :show, recipe) + end + end end @@ -739,7 +726,7 @@ mix test Compiling 1 file (.ex) ................................ - 1) test user should not allowed to show recipe of other people (TvRecipe.RecipeControllerTest) + 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: @@ -771,7 +758,7 @@ index 967b7bc..22554ea 100644 def show(conn, %{"id" => id}) do - recipe = Repo.get!(Recipe, id) -+ recipe = Repo.get!(assoc(conn.assigns.current_user, :recipes), id) ++ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id) render(conn, "show.html", recipe: recipe) end ``` @@ -781,7 +768,7 @@ index 967b7bc..22554ea 100644 $ mix test .......................... - 1) test shows chosen resource (TvRecipe.RecipeControllerTest) + 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: @@ -816,7 +803,7 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index cdbc420..d93bbd1 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -42,8 +42,8 @@ defmodule TvRecipe.RecipeControllerTest do +@@ -42,8 +42,8 @@ defmodule TvRecipeWeb.RecipeControllerTest do end @tag logged_in: true @@ -824,7 +811,7 @@ index cdbc420..d93bbd1 100644 - recipe = Repo.insert! %Recipe{} + test "shows chosen resource", %{conn: conn, user: user} do + recipe = Repo.insert! %Recipe{user_id: user.id} - conn = get conn, recipe_path(conn, :show, recipe) + conn = get conn, Routes.recipe_path(conn, :show, recipe) assert html_response(conn, 200) =~ "Show recipe" end ``` @@ -851,7 +838,7 @@ index 22554ea..f317b59 100644 def index(conn, _params) do - recipes = Repo.all(Recipe) -+ recipes = Repo.all(assoc(conn.assigns.current_user, :recipes)) ++ recipes = assoc(conn.assigns.current_user, :recipes) |> Repo.all() render(conn, "index.html", recipes: recipes) end @@ -860,14 +847,14 @@ index 22554ea..f317b59 100644 def edit(conn, %{"id" => id}) do - recipe = Repo.get!(Recipe, id) -+ recipe = Repo.get!(assoc(conn.assigns.current_user, :recipes), 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 = Repo.get!(assoc(conn.assigns.current_user, :recipes), id) ++ recipe = assoc(conn.assigns.current_user, :recipes) |> Repo.get!(id) changeset = Recipe.changeset(recipe, recipe_params) case Repo.update(changeset) do @@ -876,7 +863,7 @@ index 22554ea..f317b59 100644 def delete(conn, %{"id" => id}) do - recipe = Repo.get!(Recipe, id) -+ recipe = Repo.get!(assoc(conn.assigns.current_user, :recipes), 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). @@ -888,50 +875,17 @@ diff --git a/test/controllers/recipe_controller_test.exs b/test/controllers/reci index d93bbd1..190ede9 100644 --- a/test/controllers/recipe_controller_test.exs +++ b/test/controllers/recipe_controller_test.exs -@@ -56,30 +56,30 @@ defmodule TvRecipe.RecipeControllerTest do - end - - @tag logged_in: true -- test "renders form for editing chosen resource", %{conn: conn} do -- recipe = Repo.insert! %Recipe{} - end - - @tag logged_in: true -- test "renders form for editing chosen resource", %{conn: conn} do -- recipe = Repo.insert! %Recipe{} -+ test "renders form for editing chosen resource", %{conn: conn, user: user} do -+ recipe = Repo.insert! %Recipe{user_id: user.id} - conn = get conn, recipe_path(conn, :edit, recipe) - assert html_response(conn, 200) =~ "Edit recipe" - end - - @tag logged_in: true -- test "updates chosen resource and redirects when data is valid", %{conn: conn} do -- recipe = Repo.insert! %Recipe{} -+ test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user} do -+ recipe = Repo.insert! %Recipe{user_id: user.id} - conn = put conn, recipe_path(conn, :update, recipe), recipe: @valid_attrs - assert redirected_to(conn) == recipe_path(conn, :show, recipe) - assert Repo.get_by(Recipe, @valid_attrs) - end - - @tag logged_in: true -- test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do -- recipe = Repo.insert! %Recipe{} -+ test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user} do -+ recipe = Repo.insert! %Recipe{user_id: user.id} - conn = put conn, recipe_path(conn, :update, recipe), recipe: @invalid_attrs - assert html_response(conn, 200) =~ "Edit recipe" - end - - @tag logged_in: true -- test "deletes chosen resource", %{conn: conn} do -- recipe = Repo.insert! %Recipe{} -+ test "deletes chosen resource", %{conn: conn, user: user} do -+ recipe = Repo.insert! %Recipe{user_id: user.id} - conn = delete conn, recipe_path(conn, :delete, recipe) - assert redirected_to(conn) == recipe_path(conn, :index) - refute Repo.get(Recipe, recipe.id) +@@ -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 ``` ## 数据的完整性 @@ -950,7 +904,9 @@ index 4dbc961..2e2b518 100644 use TvRecipe.ModelCase - alias TvRecipe.{Recipe} -+ alias TvRecipe.{Repo, User, 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 %{} @@ -964,7 +920,7 @@ index 4dbc961..2e2b518 100644 + |> Ecto.build_assoc(:recipes) + |> Recipe.changeset(@valid_attrs) + {:error, changeset} = Repo.insert changeset -+ assert {:user_id, "does not exist"} in errors_on(changeset) ++ assert %{user_id: ["does not exist"]} = errors_on(changeset) + end + end diff --git a/07-recipe/04-recipe-view.md b/07-recipe/04-recipe-view.md index 69a76dd..75b185e 100644 --- a/07-recipe/04-recipe-view.md +++ b/07-recipe/04-recipe-view.md @@ -1,6 +1,6 @@ # 菜谱视图 -我们执行 [`mix phoenix.gen.html` 命令](https://github.com/phoenixframework/phoenix/blob/master/lib/mix/tasks/phoenix.gen.html.ex#L14)时,它会生成如下文件: +我们执行 [`mix phx.gen.html` 命令](https://github.com/phoenixframework/phoenix/blob/master/lib/mix/tasks/phoenix.gen.html.ex#L14)时,它会生成如下文件: * a schema in web/models * a view in web/views @@ -16,8 +16,8 @@ 首先在 `test/views` 目录下新建一个 `recipe_view_test.exs` 文件,然后准备好如下内容: ```elixir -defmodule TvRecipe.RecipeViewTest do - use TvRecipe.ConnCase, async: true +defmodule TvRecipeWeb.RecipeViewTest do + use TvRecipeWeb.ConnCase, async: true # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View @@ -50,14 +50,15 @@ index be4148a..8174c14 100644 # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View -+ alias TvRecipe.Recipe ++ alias TvRecipe.Recipes.Recipe ++ @recipe1 %{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999"} ++ @recipe2 %{id: "2", name: "煮饭", title: "侠饭", season: "1", episode: "1", content: "浸泡", user_id: "888"} + + test "render index.html", %{conn: conn} do -+ recipes = [%Recipe{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999"}, -+ %Recipe{id: "2", name: "煮饭", title: "侠饭", season: "1", episode: "1", content: "浸泡", user_id: "888"}] -+ content = render_to_string(TvRecipe.RecipeView, "index.html", conn: conn, recipes: recipes) ++ recipes = [struct(Recipe, @recipe1), struct(Recipe, @recipe2)] ++ content = render_to_string(TvRecipeWeb.RecipeView, "index.html", conn: conn, recipes: recipes) + # 页面上包含标题 Listing recipes -+ assert String.contains?(content, "Listing recipes") ++ assert String.contains?(content, "Listing Recipes") + for recipe <- recipes do + # 页面上包含菜谱名 + assert String.contains?(content, recipe.name) @@ -163,7 +164,7 @@ index a1b75c6..7bd839c 100644 +++ b/test/controllers/user_controller_test.exs @@ -29,6 +29,7 @@ defmodule TvRecipe.UserControllerTest do # 注册后自动登录,检查首页是否包含用户名 - conn = get conn, page_path(conn, :index) + conn = get conn, Routes.page_path(conn, :index) assert html_response(conn, 200) =~ Map.get(@valid_attrs, :username) + assert html_response(conn, 200) =~ "菜谱" end @@ -176,11 +177,11 @@ index b13f370..49240c9 100644 @@ -19,6 +19,7 @@
  • Get Started
  • <%= if @current_user do %> -
  • <%= link @current_user.username, to: user_path(@conn, :show, @current_user) %>
  • -+
  • <%= link "菜谱", to: recipe_path(@conn, :index) %>
  • -
  • <%= link "退出", to: session_path(@conn, :delete, @current_user), method: "delete" %>
  • +
  • <%= link @current_user.username, to: Routes.user_path(@conn, :show, @current_user) %>
  • ++
  • <%= link "菜谱", to: Routes.recipe_path(@conn, :index) %>
  • +
  • <%= link "退出", to: Routes.session_path(@conn, :delete, @current_user), method: "delete" %>
  • <% else %> -
  • <%= link "登录", to: session_path(@conn, :new) %>
  • +
  • <%= link "登录", to: Routes.session_path(@conn, :new) %>
  • ``` 运行测试: diff --git a/07-recipe/05-recipe-tv-url.md b/07-recipe/05-recipe-tv-url.md index bbbea68..0fb4e57 100644 --- a/07-recipe/05-recipe-tv-url.md +++ b/07-recipe/05-recipe-tv-url.md @@ -73,12 +73,12 @@ index 8b093ed..f1ba3f9 100644 --- a/test/models/recipe_test.exs +++ b/test/models/recipe_test.exs @@ -60,4 +60,9 @@ defmodule TvRecipe.RecipeTest do - assert {:user_id, "does not exist"} in errors_on(changeset) + assert %{user_id: ["does not exist"]} = errors_on(changeset) end + test "url should be valid" do + attrs = Map.put(@valid_attrs, :url, "fjsalfa") -+ assert {:url, "url 错误"} in errors_on(%Recipe{}, attrs) ++ assert %{url: ["url 错误"]} = errors_on(%Recipe{}, attrs) + end + end @@ -92,9 +92,9 @@ mix test 1) test url should be valid (TvRecipe.RecipeTest) test/models/recipe_test.exs:63 Assertion with in failed - code: {:url, "url 错误"} in errors_on(%Recipe{}, attrs) - left: {:url, "url 错误"} - right: [] + code: %{url: ["url 错误"]} = errors_on(%Recipe{}, attrs) + left: %{url: ["url 错误"]} + right: %{} stacktrace: test/models/recipe_test.exs:65: (test) @@ -122,9 +122,11 @@ index 104db50..3b849c8 100644 + + defp validate_url(changeset, field, _options \\ []) do + validate_change changeset, field, fn _, url -> -+ case url |> String.to_charlist |> :http_uri.parse do -+ {:ok, _} -> [] -+ {:error, _} -> [url: "url 错误"] ++ with %{host: _, scheme: scheme} <- :uri_string.parse(url), ++ true <- String.starts_with?(scheme, "http") do ++ [] ++ else ++ _ -> [url: "url 错误"] + end + end + end @@ -141,27 +143,32 @@ diff --git a/test/views/recipe_view_test.exs b/test/views/recipe_view_test.exs index 8174c14..9695647 100644 --- a/test/views/recipe_view_test.exs +++ b/test/views/recipe_view_test.exs +- @recipe1 %{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999"} +- @recipe2 %{id: "2", name: "煮饭", title: "侠饭", season: "1", episode: "1", content: "浸泡", user_id: "888"} +# 使用带有 url field 的数据 ++ @recipe1 %{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999", url: "http://localhost"} ++ @recipe2 %{id: "2", name: "煮饭", title: "侠饭", season: "1", episode: "1", content: "浸泡", user_id: "888", url: "http://localhost"} @@ -28,4 +28,23 @@ defmodule TvRecipe.RecipeViewTest do end end + test "render new.html", %{conn: conn} do -+ changeset = Recipe.changeset(%Recipe{}) ++ changeset = Recipe.changeset(%Recipe{}, %{}) + content = render_to_string(TvRecipe.RecipeView, "new.html", conn: conn, changeset: changeset) + assert String.contains?(content, "url") + end + + test "render show.html", %{conn: conn} do -+ recipe = %Recipe{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999", url: "https://github.com/chenxsan/PhoenixFramework"} ++ recipe = struct(Recipe, @recipe1) + content = render_to_string(TvRecipe.RecipeView, "show.html", conn: conn, recipe: recipe) -+ assert String.contains?(content, recipe.url) ++ assert String.contains?(content, @recipe1.url) + end + + test "render edit.html", %{conn: conn} do -+ recipe = %Recipe{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999", url: "https://github.com/chenxsan/PhoenixFramework"} -+ changeset = Recipe.changeset(recipe) ++ recipe = struct(Recipe, @recipe1) ++ changeset = Recipe.changeset(%Recipe{}, @recipe1) + content = render_to_string(TvRecipe.RecipeView, "edit.html", conn: conn, changeset: changeset, recipe: recipe) -+ assert String.contains?(content, recipe.url) ++ assert String.contains?(content, @recipe1.url) + end + end