Repository: phoenixframework/phoenix
Pull request #5725 (2024-02-15 09:56:16)

Prevent session fixation attack (phx.gen.auth + CSRF)
Plug.CSRFProtection [docs](https://hexdocs.pm/plug/Plug.CSRFProtection.html):
> we recommend developers to invoke delete_csrf_token/0 every time after they log a user in, to avoid CSRF fixation attacks.

I'm curious as to why that advice is not implemented in the `phx.gen.auth` templates.

There probably is a good reason I'm missing. If so, add that reason to the documentation?

Further reading:
https://hexdocs.pm/plug/Plug.CSRFProtection.html
https://security.stackexchange.com/a/22936
https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

josevalim: Can you please add these changes to `Auth.renew_session` instead? In there, you can simply call `delete_csrf_token()` at the top of the function. Thank you.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96eeecc40..38e8ffa09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -99,7 +99,7 @@ future styling).
 
 ## 1.7.7 (2023-07-10)
 
-## Enhancements
+### Enhancements
   * Support incoming binary payloads to channels over longpoll transport
 
 ## 1.7.6 (2023-06-16)
@@ -107,7 +107,7 @@ future styling).
 ### Bug Fixes
   * Support websock_adapter 0.5.3
 
-## Enhancements
+### Enhancements
   *  Allow using Phoenix.ChannelTest socket/connect in another process
 
 ## 1.7.5 (2023-06-15)
diff --git a/guides/asset_management.md b/guides/asset_management.md
index b87e2a03b..0533bac02 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -117,6 +117,7 @@ let opts = {
   target: "es2017",
   outdir: "../priv/static/assets",
   external: ["*.css", "fonts/*", "images/*"],
+  nodePaths: ["../deps"],
   loader: loader,
   plugins: plugins,
 };
diff --git a/guides/components.md b/guides/components.md
index b667969be..c75ccbef4 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -185,7 +185,7 @@ Likewise, for comprehensions may be written as:
 
 ## Layouts
 
-Layouts are just function components. They are defined in a module, just like all other function component templates. In a newly generated app, this is `lib/hello_web/components/layouts.ex`. You will also find in a `layouts` folder with the two built-in layouts generated by Phoenix. The default _root layout_ is called `root.html.heex`, and it is the layout into which all templates will be rendered by default. The second is the _app layout_, called `app.html.heex`, which is rendered within the root layout and includes our contents.
+Layouts are just function components. They are defined in a module, just like all other function component templates. In a newly generated app, this is `lib/hello_web/components/layouts.ex`. You will also find a `layouts` folder with two built-in layouts generated by Phoenix. The default _root layout_ is called `root.html.heex`, and it is the layout into which all templates will be rendered by default. The second is the _app layout_, called `app.html.heex`, which is rendered within the root layout and includes our contents.
 
 You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question! If we look at `lib/hello_web/components/layouts/root.html.heex`, just about at the end of the `<body>`, we will see this.
 
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index dd9630b80..5dd47ff5a 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -150,24 +150,35 @@ Finally, note that since we are using multiple buildpacks, you might run into an
 
 ## Making our Project ready for Heroku
 
-Every new Phoenix project ships with a config file `config/runtime.exs` (formerly `config/prod.secret.exs`) which loads configuration and secrets from [environment variables](https://devcenter.heroku.com/articles/config-vars). This aligns well with Heroku best practices, so the only work left for us to do is to configure URLs and SSL.
+Every new Phoenix project ships with a config file `config/runtime.exs` (formerly `config/prod.secret.exs`) which loads configuration and secrets from [environment variables](https://devcenter.heroku.com/articles/config-vars). This aligns well with Heroku best practices ([12-factor apps](https://12factor.net/)), so the only work left for us to do is to configure URLs and SSL.
 
-First let's tell Phoenix enforce we only use the SSL version of the website. Find the endpoint config in your `config/runtime.exs`:
+First let's tell Phoenix to only use the SSL version of the website. Find the endpoint config in your `config/prod.exs`:
 
 ```elixir
 config :scaffold, ScaffoldWeb.Endpoint,
-  url: [host: host, port: 443, scheme: "https"],
+  url: [port: 443, scheme: "https"],
 ```
 
 ... and add `force_ssl`
 
 ```elixir
 config :scaffold, ScaffoldWeb.Endpoint,
-  url: [host: host, port: 443, scheme: "https"],
+  url: [port: 443, scheme: "https"],
   force_ssl: [rewrite_on: [:x_forwarded_proto]],
 ```
 
-Then in your `config/runtime.exs` (formerly `config/prod.secret.exs`) and uncomment the `# ssl: true,` line in your repository configuration. It will look like this:
+`force_ssl` need to be set here because it is a _compile_ time config. It will not work when set from `runtime.exs`.
+
+Then in your `config/runtime.exs` (formerly `config/prod.secret.exs`):
+
+... add `host`
+
+```elixir
+config :scaffold, ScaffoldWeb.Endpoint,
+  url: [host: host, port: 443, scheme: "https"]
+```
+
+and uncomment the `# ssl: true,` line in your repository configuration. It will look like this:
 
 ```elixir
 config :hello, Hello.Repo,
diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md
index eba6016c5..2b14c1081 100644
--- a/guides/howto/custom_error_pages.md
+++ b/guides/howto/custom_error_pages.md
@@ -89,8 +89,8 @@ Phoenix generates an `ErrorHTML` for us, but it doesn't give us a `lib/hello_web
     <meta charset="utf-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1"/>
     <title>Welcome to Phoenix!</title>
-    <link rel="stylesheet" href="/css/app.css"/>
-    <script defer type="text/javascript" src="/js/app.js"></script>
+    <link rel="stylesheet" href="/assets/app.css"/>
+    <script defer type="text/javascript" src="/assets/app.js"></script>
   </head>
   <body>
     <header>
@@ -101,7 +101,7 @@ Phoenix generates an `ErrorHTML` for us, but it doesn't give us a `lib/hello_web
           </ul>
         </nav>
         <a href="https://phoenixframework.org/" class="phx-logo">
-          <img src="/images/phoenix.png" alt="Phoenix Framework Logo"/>
+          <img src="/images/logo.svg" alt="Phoenix Framework Logo"/>
         </a>
       </section>
     </header>
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index bce3d8d60..99259abd0 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -165,7 +165,7 @@ If your deployment environment does not support distributed Elixir or direct com
 ### Client Libraries
 
 Any networked device can connect to Phoenix Channels as long as it has a client library.
-The following libraries exist today, and new ones are always welcome; to write your own, see our how-to guide "Writing a Channels Client".
+The following libraries exist today, and new ones are always welcome; to write your own, see our how-to guide [Writing a Channels Client](writing_a_channels_client.md).
 
 #### Official
 
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index c553c5ab2..1917140ca 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -213,13 +213,14 @@ defmodule Mix.Tasks.Phx.New do
           Mix.shell().info([:green, "* running ", :reset, "mix assets.setup"])
 
           # First compile only builders so we can install in parallel
-          cmd(project, "mix deps.compile castore #{Enum.join(builders, " ")}", false)
+          # TODO: Once we require Erlang/OTP 28, castore and jason may no longer be required
+          cmd(project, "mix deps.compile castore jason #{Enum.join(builders, " ")}", log: false)
         end
 
         tasks =
           Enum.map(builders, fn builder ->
             cmd = "mix do loadpaths --no-compile + #{builder}.install"
-            Task.async(fn -> cmd(project, cmd, false) end)
+            Task.async(fn -> cmd(project, cmd, log: false, cd: project.web_path) end)
           end)
 
         if rebar_available?() do
@@ -317,12 +318,14 @@ defmodule Mix.Tasks.Phx.New do
 
   ## Helpers
 
-  defp cmd(%Project{} = project, cmd, log? \\ true) do
+  defp cmd(%Project{} = project, cmd, opts \\ []) do
+    {log?, opts} = Keyword.pop(opts, :log, true)
+
     if log? do
       Mix.shell().info([:green, "* running ", :reset, cmd])
     end
 
-    case Mix.shell().cmd(cmd, cmd_opts(project)) do
+    case Mix.shell().cmd(cmd, opts ++ cmd_opts(project)) do
       0 -> []
       _ -> ["$ #{cmd}"]
     end
diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index 840ec607d..81d31ef1d 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -188,7 +188,7 @@ defmodule Phx.New.Generator do
     {adapter_app, adapter_module, adapter_config} =
       get_ecto_adapter(db, String.downcase(project.app), project.app_mod)
 
-    {web_adapter_app, web_adapter_vsn, web_adapter_module} = get_web_adapter(web_adapter)
+    {web_adapter_app, web_adapter_vsn, web_adapter_module, web_adapter_docs} = get_web_adapter(web_adapter)
 
     pubsub_server = get_pubsub_server(project.app_mod)
 
@@ -235,6 +235,7 @@ defmodule Phx.New.Generator do
       web_adapter_app: web_adapter_app,
       web_adapter_module: web_adapter_module,
       web_adapter_vsn: web_adapter_vsn,
+      web_adapter_docs: web_adapter_docs,
       generators: nil_if_empty(project.generators ++ adapter_generators(adapter_config)),
       namespaced?: namespaced?(project),
       dev: dev
@@ -303,20 +304,20 @@ defmodule Phx.New.Generator do
     Mix.raise("Unknown database #{inspect(db)}")
   end
 
-  defp get_web_adapter("cowboy"), do: {:plug_cowboy, "~> 2.7", Phoenix.Endpoint.Cowboy2Adapter}
-  defp get_web_adapter("bandit"), do: {:bandit, "~> 1.2", Bandit.PhoenixAdapter}
+  defp get_web_adapter("cowboy"), do: {:plug_cowboy, "~> 2.7", Phoenix.Endpoint.Cowboy2Adapter, "https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html"}
+  defp get_web_adapter("bandit"), do: {:bandit, "~> 1.2", Bandit.PhoenixAdapter, "https://hexdocs.pm/bandit/Bandit.html#t:options/0"}
   defp get_web_adapter(other), do: Mix.raise("Unknown web adapter #{inspect(other)}")
 
   defp fs_db_config(app, module) do
     [
       dev: [
-        database: {:literal, ~s|Path.expand("../#{app}_dev.db", Path.dirname(__ENV__.file))|},
+        database: {:literal, ~s|Path.expand("../#{app}_dev.db", __DIR__)|},
         pool_size: 5,
         stacktrace: true,
         show_sensitive_data_on_connection_error: true
       ],
       test: [
-        database: {:literal, ~s|Path.expand("../#{app}_test.db", Path.dirname(__ENV__.file))|},
+        database: {:literal, ~s|Path.expand("../#{app}_test.db", __DIR__)|},
         pool_size: 5,
         pool: Ecto.Adapters.SQL.Sandbox
       ],
diff --git a/installer/templates/phx_single/config/runtime.exs b/installer/templates/phx_single/config/runtime.exs
index 2c8b3d889..0ca7d2684 100644
--- a/installer/templates/phx_single/config/runtime.exs
+++ b/installer/templates/phx_single/config/runtime.exs
@@ -43,7 +43,7 @@ if config_env() == :prod do
     http: [
       # Enable IPv6 and bind on all interfaces.
       # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
-      # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+      # See the documentation on <%= @web_adapter_docs %>
       # for details about using IPv6 vs IPv4 and loopback vs public addresses.
       ip: {0, 0, 0, 0, 0, 0, 0, 0},
       port: port
diff --git a/installer/templates/phx_single/lib/app_name_web.ex b/installer/templates/phx_single/lib/app_name_web.ex
index a2d52e1d3..10a189404 100644
--- a/installer/templates/phx_single/lib/app_name_web.ex
+++ b/installer/templates/phx_single/lib/app_name_web.ex
@@ -105,7 +105,7 @@ defmodule <%= @web_namespace %> do
   end
 
   @doc """
-  When used, dispatch to the appropriate controller/view/etc.
+  When used, dispatch to the appropriate controller/live_view/etc.
   """
   defmacro __using__(which) when is_atom(which) do
     apply(__MODULE__, which, [])
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index f77af333c..8c22ea6c2 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -5,6 +5,7 @@ defmodule Mix.Phoenix.Schema do
 
   defstruct module: nil,
             repo: nil,
+            repo_alias: nil,
             table: nil,
             collection: nil,
             embedded?: false,
@@ -78,6 +79,7 @@ defmodule Mix.Phoenix.Schema do
     basename = Phoenix.Naming.underscore(schema_name)
     module = Module.concat([base, schema_name])
     repo = opts[:repo] || Module.concat([base, "Repo"])
+    repo_alias = if String.ends_with?(Atom.to_string(repo), ".Repo"), do: "", else: ", as: Repo"
     file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
     table = opts[:table] || schema_plural
     {cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
@@ -88,6 +90,7 @@ defmodule Mix.Phoenix.Schema do
     api_prefix = Application.get_env(otp_app, :generators)[:api_prefix] || "/api"
     embedded? = Keyword.get(opts, :embedded, false)
     generate? = Keyword.get(opts, :schema, true)
+
     singular =
       module
       |> Module.split()
@@ -113,6 +116,7 @@ defmodule Mix.Phoenix.Schema do
       migration?: Keyword.get(opts, :migration, true),
       module: module,
       repo: repo,
+      repo_alias: repo_alias,
       table: table,
       embedded?: embedded?,
       alias: module |> Module.split() |> List.last() |> Module.concat(nil),
diff --git a/lib/mix/tasks/phx.gen.cert.ex b/lib/mix/tasks/phx.gen.cert.ex
index d3ada0f18..363b6d151 100644
--- a/lib/mix/tasks/phx.gen.cert.ex
+++ b/lib/mix/tasks/phx.gen.cert.ex
@@ -235,20 +235,18 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
   defp new_cert(public_key, common_name, hostnames) do
     <<serial::unsigned-64>> = :crypto.strong_rand_bytes(8)
 
-    # Dates must be in 'YYMMDD' format
-    {{year, month, day}, _} =
-      :erlang.timestamp()
-      |> :calendar.now_to_datetime()
-
-    yy = year |> Integer.to_string() |> String.slice(2, 2)
-    mm = month |> Integer.to_string() |> String.pad_leading(2, "0")
-    dd = day |> Integer.to_string() |> String.pad_leading(2, "0")
-
-    not_before = yy <> mm <> dd
-
-    yy2 = (year + 1) |> Integer.to_string() |> String.slice(2, 2)
-
-    not_after = yy2 <> mm <> dd
+    today = Date.utc_today()
+
+    not_before =
+      today
+      |> Date.to_iso8601(:basic)
+      |> String.slice(2, 6)
+
+    not_after =
+      today
+      |> Date.add(365)
+      |> Date.to_iso8601(:basic)
+      |> String.slice(2, 6)
 
     otp_tbs_certificate(
       version: :v3,
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index ec18b3c4c..c7e1be7af 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -257,7 +257,11 @@ defmodule Phoenix.CodeReloader.Server do
     # assets in priv are copied to the build directory.
     Mix.Project.build_structure(config)
     args = ["--purge-consolidation-path-if-stale", consolidation_path | compile_args]
-    result = run_compilers(compilers, args, [])
+
+    result =
+      with_logger_app(config, fn ->
+        run_compilers(compilers, args, [])
+      end)
 
     cond do
       result == :error ->
@@ -320,4 +324,17 @@ defmodule Phoenix.CodeReloader.Server do
       :noop
     end
   end
+
+  # TODO: remove once we depend on Elixir 1.17
+  defp with_logger_app(config, fun) do
+    app = Keyword.fetch!(config, :app)
+    logger_config_app = Application.get_env(:logger, :compile_time_application)
+
+    try do
+      Logger.configure(compile_time_application: app)
+      fun.()
+    after
+      Logger.configure(compile_time_application: logger_config_app)
+    end
+  end
 end
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index b32758a75..472a0214c 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -60,6 +60,8 @@ defmodule <%= inspect auth_module %> do
   #     end
   #
   defp renew_session(conn) do
+    delete_csrf_token()
+
     conn
     |> configure_session(renew: true)
     |> clear_session()
diff --git a/priv/templates/phx.gen.context/context.ex b/priv/templates/phx.gen.context/context.ex
index 4215d5c5d..51f2d28d8 100644
--- a/priv/templates/phx.gen.context/context.ex
+++ b/priv/templates/phx.gen.context/context.ex
@@ -4,5 +4,5 @@ defmodule <%= inspect context.module %> do
   """
 
   import Ecto.Query, warn: false
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
 end
diff --git a/priv/templates/phx.gen.socket/socket.ex b/priv/templates/phx.gen.socket/socket.ex
index e44c2d0ea..772da12e4 100644
--- a/priv/templates/phx.gen.socket/socket.ex
+++ b/priv/templates/phx.gen.socket/socket.ex
@@ -42,7 +42,7 @@ defmodule <%= module %>Socket do
     {:ok, socket}
   end
 
-  # Socket id's are topics that allow you to identify all sockets for a given user:
+  # Socket IDs are topics that allow you to identify all sockets for a given user:
   #
   #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
   #


Test patch:
diff --git a/priv/templates/phx.gen.auth/confirmation_controller_test.exs b/priv/templates/phx.gen.auth/confirmation_controller_test.exs
index 09390630f..d38e11f88 100644
--- a/priv/templates/phx.gen.auth/confirmation_controller_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_controller_test.exs
@@ -2,7 +2,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
   import <%= inspect context.module %>Fixtures
 
   setup do
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
index 9717c5711..c1ac8b654 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
@@ -5,7 +5,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   import <%= inspect context.module %>Fixtures
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
 
   setup do
     %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index e6af571a5..a206a394f 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -5,7 +5,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   import <%= inspect context.module %>Fixtures
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
 
   setup do
     %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
diff --git a/priv/templates/phx.gen.auth/forgot_password_live_test.exs b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
index 2d3aecbe9..a249a694e 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
@@ -5,7 +5,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   import <%= inspect context.module %>Fixtures
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
 
   describe "Forgot password page" do
     test "renders email page", %{conn: conn} do
diff --git a/priv/templates/phx.gen.auth/reset_password_controller_test.exs b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
index 99594fcbc..f622b51e0 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
@@ -2,7 +2,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %>
+  alias <%= inspect schema.repo %><%= schema.repo_alias %>
   import <%= inspect context.module %>Fixtures
 
   setup do
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index 1e17d58cb..f13c3421c 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -1,4 +1,4 @@
-Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
+Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
 
 defmodule Phoenix.DupContext do
 end
@@ -15,64 +15,77 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
   end
 
   test "new context", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       schema = Schema.new("Blog.Post", "posts", [], [])
       context = Context.new("Blog", schema, [])
 
       assert %Context{
-        alias: Blog,
-        base_module: Phoenix,
-        basename: "blog",
-        module: Phoenix.Blog,
-        web_module: PhoenixWeb,
-        schema: %Mix.Phoenix.Schema{
-          alias: Post,
-          human_plural: "Posts",
-          human_singular: "Post",
-          module: Phoenix.Blog.Post,
-          plural: "posts",
-          singular: "post"
-        }} = context
+               alias: Blog,
+               base_module: Phoenix,
+               basename: "blog",
+               module: Phoenix.Blog,
+               web_module: PhoenixWeb,
+               schema: %Mix.Phoenix.Schema{
+                 alias: Post,
+                 human_plural: "Posts",
+                 human_singular: "Post",
+                 module: Phoenix.Blog.Post,
+                 plural: "posts",
+                 singular: "post"
+               }
+             } = context
 
       assert String.ends_with?(context.dir, "lib/phoenix/blog")
       assert String.ends_with?(context.file, "lib/phoenix/blog.ex")
       assert String.ends_with?(context.test_file, "test/phoenix/blog_test.exs")
-      assert String.ends_with?(context.test_fixtures_file, "test/support/fixtures/blog_fixtures.ex")
+
+      assert String.ends_with?(
+               context.test_fixtures_file,
+               "test/support/fixtures/blog_fixtures.ex"
+             )
+
       assert String.ends_with?(context.schema.file, "lib/phoenix/blog/post.ex")
-    end
+    end)
   end
 
   test "new nested context", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       schema = Schema.new("Site.Blog.Post", "posts", [], [])
       context = Context.new("Site.Blog", schema, [])
 
       assert %Context{
-        alias: Blog,
-        base_module: Phoenix,
-        basename: "blog",
-        module: Phoenix.Site.Blog,
-        web_module: PhoenixWeb,
-        schema: %Mix.Phoenix.Schema{
-          alias: Post,
-          human_plural: "Posts",
-          human_singular: "Post",
-          module: Phoenix.Site.Blog.Post,
-          plural: "posts",
-          singular: "post"
-        }} = context
+               alias: Blog,
+               base_module: Phoenix,
+               basename: "blog",
+               module: Phoenix.Site.Blog,
+               web_module: PhoenixWeb,
+               schema: %Mix.Phoenix.Schema{
+                 alias: Post,
+                 human_plural: "Posts",
+                 human_singular: "Post",
+                 module: Phoenix.Site.Blog.Post,
+                 plural: "posts",
+                 singular: "post"
+               }
+             } = context
 
       assert String.ends_with?(context.dir, "lib/phoenix/site/blog")
       assert String.ends_with?(context.file, "lib/phoenix/site/blog.ex")
       assert String.ends_with?(context.test_file, "test/phoenix/site/blog_test.exs")
-      assert String.ends_with?(context.test_fixtures_file, "test/support/fixtures/site/blog_fixtures.ex")
+
+      assert String.ends_with?(
+               context.test_fixtures_file,
+               "test/support/fixtures/site/blog_fixtures.ex"
+             )
+
       assert String.ends_with?(context.schema.file, "lib/phoenix/site/blog/post.ex")
-    end
+    end)
   end
 
   test "new existing context", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       File.mkdir_p!("lib/phoenix/blog")
+
       File.write!("lib/phoenix/blog.ex", """
       defmodule Phoenix.Blog do
       end
@@ -85,23 +98,27 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
       refute Context.pre_existing_test_fixtures?(context)
 
       File.mkdir_p!("test/phoenix/blog")
+
       File.write!(context.test_file, """
       defmodule Phoenix.BlogTest do
       end
       """)
+
       assert Context.pre_existing_tests?(context)
 
       File.mkdir_p!("test/support/fixtures")
+
       File.write!(context.test_fixtures_file, """
       defmodule Phoenix.BlogFixtures do
       end
       """)
+
       assert Context.pre_existing_test_fixtures?(context)
-    end
+    end)
   end
 
   test "invalid mix arguments", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       assert_raise Mix.Error, ~r/Expected the context, "blog", to be a valid module name/, fn ->
         Gen.Context.run(~w(blog Post posts title:string))
       end
@@ -114,13 +131,17 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
         Gen.Context.run(~w(Blog Blog blogs))
       end
 
-      assert_raise Mix.Error, ~r/Cannot generate context Phoenix because it has the same name as the application/, fn ->
-        Gen.Context.run(~w(Phoenix Post blogs))
-      end
+      assert_raise Mix.Error,
+                   ~r/Cannot generate context Phoenix because it has the same name as the application/,
+                   fn ->
+                     Gen.Context.run(~w(Phoenix Post blogs))
+                   end
 
-      assert_raise Mix.Error, ~r/Cannot generate schema Phoenix because it has the same name as the application/, fn ->
-        Gen.Context.run(~w(Blog Phoenix blogs))
-      end
+      assert_raise Mix.Error,
+                   ~r/Cannot generate schema Phoenix because it has the same name as the application/,
+                   fn ->
+                     Gen.Context.run(~w(Blog Phoenix blogs))
+                   end
 
       assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
         Gen.Context.run(~w(Blog.Post posts))
@@ -129,90 +150,95 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
       assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
         Gen.Context.run(~w(Blog Post))
       end
-    end
+    end)
   end
 
   test "generates context and handles existing contexts", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       Gen.Context.run(~w(Blog Post posts slug:unique secret:redact title:string))
 
-      assert_file "lib/phoenix/blog/post.ex", fn file ->
+      assert_file("lib/phoenix/blog/post.ex", fn file ->
         assert file =~ "field :title, :string"
         assert file =~ "field :secret, :string, redact: true"
-      end
+      end)
 
-      assert_file "lib/phoenix/blog.ex", fn file ->
+      assert_file("lib/phoenix/blog.ex", fn file ->
         assert file =~ "def get_post!"
         assert file =~ "def list_posts"
         assert file =~ "def create_post"
         assert file =~ "def update_post"
         assert file =~ "def delete_post"
         assert file =~ "def change_post"
-      end
+      end)
 
-      assert_file "test/phoenix/blog_test.exs", fn file ->
+      assert_file("test/phoenix/blog_test.exs", fn file ->
         assert file =~ "use Phoenix.DataCase"
         assert file =~ "describe \"posts\" do"
         assert file =~ "import Phoenix.BlogFixtures"
-      end
+      end)
 
-      assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+      assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
         assert file =~ "defmodule Phoenix.BlogFixtures do"
         assert file =~ "def post_fixture(attrs \\\\ %{})"
         assert file =~ "title: \"some title\""
-      end
+      end)
 
       assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
-      assert_file path, fn file ->
+
+      assert_file(path, fn file ->
         assert file =~ "create table(:posts)"
         assert file =~ "add :title, :string"
         assert file =~ "add :secret, :string"
         assert file =~ "create unique_index(:posts, [:slug])"
-      end
-
+      end)
 
-      send self(), {:mix_shell_input, :yes?, true}
+      send(self(), {:mix_shell_input, :yes?, true})
       Gen.Context.run(~w(Blog Comment comments title:string))
 
-      assert_received {:mix_shell, :info, ["You are generating into an existing context" <> notice]}
-      assert notice =~ "Phoenix.Blog context currently has 6 functions and 1 file in its directory"
+      assert_received {:mix_shell, :info,
+                       ["You are generating into an existing context" <> notice]}
+
+      assert notice =~
+               "Phoenix.Blog context currently has 6 functions and 1 file in its directory"
+
       assert_received {:mix_shell, :yes?, ["Would you like to proceed?"]}
 
-      assert_file "lib/phoenix/blog/comment.ex", fn file ->
+      assert_file("lib/phoenix/blog/comment.ex", fn file ->
         assert file =~ "field :title, :string"
-      end
+      end)
 
-      assert_file "test/phoenix/blog_test.exs", fn file ->
+      assert_file("test/phoenix/blog_test.exs", fn file ->
         assert file =~ "use Phoenix.DataCase"
         assert file =~ "describe \"comments\" do"
         assert file =~ "import Phoenix.BlogFixtures"
-      end
+      end)
 
-      assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+      assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
         assert file =~ "defmodule Phoenix.BlogFixtures do"
         assert file =~ "def comment_fixture(attrs \\\\ %{})"
         assert file =~ "title: \"some title\""
-      end
+      end)
 
       assert [path] = Path.wildcard("priv/repo/migrations/*_create_comments.exs")
-      assert_file path, fn file ->
+
+      assert_file(path, fn file ->
         assert file =~ "create table(:comments)"
         assert file =~ "add :title, :string"
-      end
+      end)
 
-      assert_file "lib/phoenix/blog.ex", fn file ->
+      assert_file("lib/phoenix/blog.ex", fn file ->
         assert file =~ "def get_comment!"
         assert file =~ "def list_comments"
         assert file =~ "def create_comment"
         assert file =~ "def update_comment"
         assert file =~ "def delete_comment"
         assert file =~ "def change_comment"
-      end
-    end
+      end)
+    end)
   end
 
   test "generates with unique fields", config do
-    in_tmp_project config.test, fn ->
+    in_tmp_project(config.test, fn ->
       Gen.Context.run(~w(Blog Post posts
             slug:string:unique
             subject:unique
@@ -224,57 +250,70 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
             published?:boolean
           ))
 
-      assert_received {:mix_shell, :info, ["""
+      assert_received {:mix_shell, :info,
+                       [
+                         """
 
-        Some of the generated database columns are unique. Please provide
-        unique implementations for the following fixture function(s) in
-        test/support/fixtures/blog_fixtures.ex:
+                         Some of the generated database columns are unique. Please provide
+                         unique implementations for the following fixture function(s) in
+                         test/support/fixtures/blog_fixtures.ex:
 
-            def unique_post_price do
-              raise "implement the logic to generate a unique post price"
-            end
+                             def unique_post_price do
+                               raise "implement the logic to generate a unique post price"
+                             end
 
-            def unique_post_published_at do
-              raise "implement the logic to generate a unique post published_at"
-            end
-        """]}
+                             def unique_post_published_at do
+                               raise "implement the logic to generate a unique post published_at"
+                             end
+                         """
+                       ]}
 
-      assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+      assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
         assert file =~ ~S|def unique_post_order, do: System.unique_integer([:positive])|
-        assert file =~ ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
-        assert file =~ ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
-        assert file =~ ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
+
+        assert file =~
+                 ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
+
+        assert file =~
+                 ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
+
+        assert file =~
+                 ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
+
         refute file =~ ~S|def unique_post_author|
+
         assert file =~ """
-          def unique_post_price do
-            raise "implement the logic to generate a unique post price"
-          end
-        """
+                 def unique_post_price do
+                   raise "implement the logic to generate a unique post price"
+                 end
+               """
 
         assert file =~ """
-                body: unique_post_body(),
-                order: unique_post_order(),
-                price: unique_post_price(),
-                published?: true,
-                published_at: unique_post_published_at(),
-                slug: unique_post_slug(),
-                subject: unique_post_subject()
-        """
-      end
+                       body: unique_post_body(),
+                       order: unique_post_order(),
+                       price: unique_post_price(),
+                       published?: true,
+                       published_at: unique_post_published_at(),
+                       slug: unique_post_slug(),
+                       subject: unique_post_subject()
+               """
+      end)
 
       assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
-      assert_file path, fn file ->
+
+      assert_file(path, fn file ->
         assert file =~ "create table(:posts)"
         assert file =~ "create unique_index(:posts, [:order])"
         assert file =~ "create unique_index(:posts, [:price])"
         assert file =~ "create unique_index(:posts, [:slug])"
         assert file =~ "create unique_index(:posts, [:subject])"
-      end
-    end
+      end)
+    end)
   end
 
-  test "does not prompt on unimplemented functions with only string, text and integer unique fields", config do
-    in_tmp_project config.test, fn ->
+  test "does not prompt on unimplemented functions with only string, text and integer unique fields",
+       config do
+    in_tmp_project(config.test, fn ->
       Gen.Context.run(~w(Blog Post posts
             slug:string:unique
             subject:unique
@@ -282,57 +321,60 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
             order:integer:unique
           ))
 
-      refute_received {:mix_shell, :info, ["\nSome of the generated database columns are unique." <> _]}
-    end
+      refute_received {:mix_shell, :info,
+                       ["\nSome of the generated database columns are unique." <> _]}
+    end)
   end
 
-  test "generates into existing context without prompt with --merge-with-existing-context", config do
-    in_tmp_project config.test, fn ->
+  test "generates into existing context without prompt with --merge-with-existing-context",
+       config do
+    in_tmp_project(config.test, fn ->
       Gen.Context.run(~w(Blog Post posts title))
 
-      assert_file "lib/phoenix/blog.ex", fn file ->
+      assert_file("lib/phoenix/blog.ex", fn file ->
         assert file =~ "def get_post!"
         assert file =~ "def list_posts"
         assert file =~ "def create_post"
         assert file =~ "def update_post"
         assert file =~ "def delete_post"
         assert file =~ "def change_post"
-      end
+      end)
 
       Gen.Context.run(~w(Blog Comment comments message:string --merge-with-existing-context))
 
-      refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+      refute_received {:mix_shell, :info,
+                       ["You are generating into an existing context" <> _notice]}
 
-      assert_file "lib/phoenix/blog.ex", fn file ->
+      assert_file("lib/phoenix/blog.ex", fn file ->
         assert file =~ "def get_comment!"
         assert file =~ "def list_comments"
         assert file =~ "def create_comment"
         assert file =~ "def update_comment"
         assert file =~ "def delete_comment"
         assert file =~ "def change_comment"
-      end
-    end
+      end)
+    end)
   end
 
   test "when more than 50 attributes are given", config do
-    in_tmp_project config.test, fn ->
-      long_attribute_list = Enum.map_join(0..55, " ", &("attribute#{&1}:string"))
+    in_tmp_project(config.test, fn ->
+      long_attribute_list = Enum.map_join(0..55, " ", &"attribute#{&1}:string")
       Gen.Context.run(~w(Blog Post posts title #{long_attribute_list}))
 
-      assert_file "test/phoenix/blog_test.exs", fn file ->
+      assert_file("test/phoenix/blog_test.exs", fn file ->
         refute file =~ "...}"
-      end
-    end
+      end)
+    end)
   end
 
+  test "generates context with no schema and repo option", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Context.run(~w(Blog Post posts title:string --no-schema --repo=Foo.RepoX))
 
-  test "generates context with no schema", config do
-    in_tmp_project config.test, fn ->
-      Gen.Context.run(~w(Blog Post posts title:string --no-schema))
-
-      refute_file "lib/phoenix/blog/post.ex"
+      refute_file("lib/phoenix/blog/post.ex")
 
-      assert_file "lib/phoenix/blog.ex", fn file ->
+      assert_file("lib/phoenix/blog.ex", fn file ->
+        assert file =~ "alias Foo.RepoX, as: Repo"
         assert file =~ "def get_post!"
         assert file =~ "def list_posts"
         assert file =~ "def create_post"
@@ -340,22 +382,22 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
         assert file =~ "def delete_post"
         assert file =~ "def change_post"
         assert file =~ "raise \"TODO\""
-      end
+      end)
 
-      assert_file "test/phoenix/blog_test.exs", fn file ->
+      assert_file("test/phoenix/blog_test.exs", fn file ->
         assert file =~ "use Phoenix.DataCase"
         assert file =~ "describe \"posts\" do"
         assert file =~ "import Phoenix.BlogFixtures"
-      end
+      end)
 
-      assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+      assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
         assert file =~ "defmodule Phoenix.BlogFixtures do"
         assert file =~ "def post_fixture(attrs \\\\ %{})"
         assert file =~ "title: \"some title\""
-      end
+      end)
 
       assert Path.wildcard("priv/repo/migrations/*_create_posts.exs") == []
-    end
+    end)
   end
 
   test "generates context with enum", config do