Repository: phoenixframework/phoenix
Pull request #4625 (2021-12-08 22:32:02)

Update the Phoenix.Token.verify/4 docs to mention the default value for max_age
The default should be called out so it doesn't lead to confusion or bugs when working with this function. Verbiage taken directly from https://github.com/elixir-plug/plug_crypto/blob/v1.2.2/lib/plug/crypto.ex#L257


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0259ba6a4..780ea3c10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ See the [upgrade guide](https://gist.github.com/chrismccord/2ab350f154235ad4a4d0
 
 Phoenix v1.6 requires Elixir v1.9+.
 
+## 1.6.5 (2021-12-16)
+
+### Enhancements
+  * [Endpoint] Support `check_origin: :conn` to enforce origin on the connection's host, port, and scheme
+
 ## 1.6.4 (2021-12-08)
 
 ### Bug Fixes
diff --git a/assets/js/phoenix/socket.js b/assets/js/phoenix/socket.js
index 653d5c4ce..dc4639820 100644
--- a/assets/js/phoenix/socket.js
+++ b/assets/js/phoenix/socket.js
@@ -78,7 +78,7 @@ import Timer from "./timer"
  *
  * Defaults to 20s (double the server long poll timer).
  *
- * @param {{Object|function)} [opts.params] - The optional params to pass when connecting
+ * @param {(Object|function)} [opts.params] - The optional params to pass when connecting
  * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.
  *
  * Defaults to "arraybuffer"
diff --git a/config/config.exs b/config/config.exs
index 783d73c57..38a43e8c1 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -18,7 +18,7 @@ if Mix.env() == :dev do
   end
 
   config :esbuild,
-    version: "0.13.5",
+    version: "0.14.0",
     module: esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/phoenix.esm.js)),
     main: esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/phoenix.cjs.js)),
     cdn:
diff --git a/guides/contexts.md b/guides/contexts.md
index 6a66cf364..1944fc265 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -28,7 +28,7 @@ Phoenix includes the `mix phx.gen.html`, `mix phx.gen.json`, `mix phx.gen.live`,
 
 In order to run the context generators, we need to come up with a module name that groups the related functionality that we're building. In the [Ecto guide](ecto.html), we saw how we can use Changesets and Repos to validate and persist user schemas, but we didn't integrate this with our application at large. In fact, we didn't think about where a "user" in our application should live at all. Let's take a step back and think about the different parts of our system. We know that we'll have products to showcase on pages for sale, along with descriptions, pricing, etc. Along with selling products, we know we'll need to support carting, order checkout, and so on. While the products being purchased are related to the cart and checkout processes, showcasing a product and managing the *exhibition* of our products is distinctly different than tracking what a user has placed in their cart or how an order is placed. A `Catalog` context is a natural place for the management of our product details and the showcasing of those products we have for sale.
 
-> Naming things is hard. If you're stuck when trying to come up with a context name when the grouped functionality in your system isn't yet clear, you can simply use the plural form of the resource you're creating. For example, a `Products` context for managing products. As you grow your application and the parts of your system become clear, you can simply rename the context to a more refined name at a later time.
+> Naming things is hard. If you're stuck when trying to come up with a context name when the grouped functionality in your system isn't yet clear, you can simply use the plural form of the resource you're creating. For example, a `Products` context for managing products. As you grow your application and the parts of your system become clear, you can simply rename the context to a more refined one.
 
 To jump-start our catalog context, we'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting products, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root:
 
@@ -325,7 +325,7 @@ Our basic catalog features are nice, but let's take it up a notch by categorizin
 
 For now, categories will contain only textual information. Our first order of business is to decide where categories live in the application. We have our `Catalog` context, which manages the exhibition of our products. Product categorization is a natural fit here. Phoenix is also smart enough to generate code inside an existing context, which makes adding new resources to a context a breeze. Run the following command at your project root:
 
-> Sometimes it may be tricky to determine if two resources belong to the same context or not. In those cases, prefer distinct contexts per resource and refactor later if necessary. Otherwise you can easily end-up with large contexts of loosely related entities. Also keep in mind that the fact two resources are related does not necessarily mean they belong to the same context, otherwise you would quickly end-up with one large context, as the majority of resources in an application are connected to each other. To sum it up: if you are unsure, you should prefer explicit modules (contexts) between resources.
+> Sometimes it may be tricky to determine if two resources belong to the same context or not. In those cases, prefer distinct contexts per resource and refactor later if necessary. Otherwise you can easily end-up with large contexts of loosely related entities. Also keep in mind that the fact two resources are related does not necessarily mean they belong to the same context, otherwise you would quickly end-up with one large context, as the majority of resources in an application are connected to each other. To sum it up: if you are unsure, you should prefer separate modules (contexts).
 
 ```console
 $ mix phx.gen.context Catalog Category categories \
@@ -662,9 +662,9 @@ Our database is ready to go with new `carts` and `cart_items` tables, but now we
 
 ### Cross-context data
 
-Dependencies in your software are often unavoidable, but we can do our best to limit them where possible and lessen the maintenance burden when a dependency is necessary. So far, we've done a great job isolating the two main contexts of our application from each other, but now we have a necessary dependency to handle.
+So far, we've done a great job isolating the two main contexts of our application from each other, but now we have a necessary dependency to handle.
 
-Our `Catalog.Product` resource serves to keep the responsibilities of representing a product inside the catalog, but ultimately for an item to exist in the cart, a product from the catalog must be present. Given this, our `ShoppingCart` context will have a data dependency on the `Catalog` context. With that in mind, we have two options. One is to expose APIs on the `Catalog` context that allows us to efficiently fetch product data for use in the `ShoppingCart` system, or we can use database joins to fetch the dependent data. Both are valid options given your tradeoffs and application size, but joining data from the database when you have a hard data dependency is just fine for a large class of applications. Additionally, joining data yourself results in increased application code complexity, worse performance, and potential data inconsistencies. Also, if you decide to break out coupled contexts into entirely separate applications and databases at a later time, you still gain the benefits of isolation. This is because your public context APIs will likely remain unchanged.
+Our `Catalog.Product` resource serves to keep the responsibilities of representing a product inside the catalog, but ultimately for an item to exist in the cart, a product from the catalog must be present. Given this, our `ShoppingCart` context will have a data dependency on the `Catalog` context. With that in mind, we have two options. One is to expose APIs on the `Catalog` context that allows us to efficiently fetch product data for use in the `ShoppingCart` system, which we would manually stitch together. Or we can use database joins to fetch the dependent data. Both are valid options given your tradeoffs and application size, but joining data from the database when you have a hard data dependency is just fine for a large class of applications and is the approach we will take here.
 
 Now that we know where our data dependencies exist, let's add our schema associations so we can tie shopping cart items to products. First, let's make a quick change to our cart schema in `lib/hello/shopping_cart/cart.ex` to associate a cart to its items:
 
@@ -1343,4 +1343,8 @@ As we explored the context API, you might have wondered:
 
 > If one of the goals of our context is to encapsulate Ecto Repo access, why does `create_user/1` return an `Ecto.Changeset` struct when we fail to create a user?
 
-The answer is we've decided to expose `%Ecto.Changeset{}` as a public *data-structure* in our application. We saw before how changesets allow us to track field changes, perform validations, and generate error messages. Its use here is decoupled from the private Repo access and Ecto changeset API internals. We're exposing a data structure that the caller understands which contains the rich information like field errors. Conveniently for us, the `phoenix_ecto` project implements the necessary `Phoenix.Param` and `Phoenix.HTML.FormData` protocols which know how to handle `%Ecto.Changeset{}`'s for things like form generation and error messages. You can also think about it as being as if you had defined your own `%Accounts.Changes{}` struct for the same purpose and implemented the Phoenix protocols for the web-layer integration.
+Although Changesets are part of Ecto, they are not tied to the database, and they can be used to map data from and to any source, which makes it a general and useful data structure for tracking field changes, perform validations, and generate error messages.
+
+For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer. Regardless if you are talking to an API or the database.
+
+Finally, note that your controllers and views are not hardcoded to work exclusively with Ecto either. Instead, Phoenix defines protocols such as `Phoenix.Param` and `Phoenix.HTML.FormData`, which allow any library to extend how Phoenix generates URL parameters or renders forms. Conveniently for us, the `phoenix_ecto` project implements those protocols, but you could as well bring your own data structures and implement them yourself.
diff --git a/guides/ecto.md b/guides/ecto.md
index 56697b627..394b42f39 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -137,8 +137,8 @@ When `phx.new` generated our application, it included some basic repository conf
 config :hello, Hello.Repo,
   username: "postgres",
   password: "postgres",
-  database: "hello_dev",
   hostname: "localhost",
+  database: "hello_dev",
   show_sensitive_data_on_connection_error: true,
   pool_size: 10
 ...
diff --git a/guides/mix_tasks.md b/guides/mix_tasks.md
index 5ebff708c..657a16ed4 100644
--- a/guides/mix_tasks.md
+++ b/guides/mix_tasks.md
@@ -580,10 +580,10 @@ Notice that this task has updated `config/config.exs`. If we take a look, we'll
 ```elixir
 . . .
 config :hello, OurCustom.Repo,
-  database: "hello_repo",
   username: "user",
   password: "pass",
-  hostname: "localhost"
+  hostname: "localhost",
+  database: "hello_repo",
 . . .
 ```
 
diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index b0ed82b42..5fbada487 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -324,16 +324,16 @@ defmodule Phx.New.Generator do
       dev: [
         username: user,
         password: pass,
-        database: "#{app}_dev",
         hostname: "localhost",
+        database: "#{app}_dev",
         show_sensitive_data_on_connection_error: true,
         pool_size: 10
       ],
       test: [
         username: user,
         password: pass,
-        database: {:literal, ~s|"#{app}_test\#{System.get_env("MIX_TEST_PARTITION")}"|},
         hostname: "localhost",
+        database: {:literal, ~s|"#{app}_test\#{System.get_env("MIX_TEST_PARTITION")}"|},
         pool: Ecto.Adapters.SQL.Sandbox,
         pool_size: 10,
       ],
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 19420a522..860d8aa9d 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -33,7 +33,7 @@ config :swoosh, :api_client, false<% end %><%= if @assets do %>
 
 # Configure esbuild (the version is required)
 config :esbuild,
-  version: "0.13.5",
+  version: "0.14.0",
   default: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
index 4d93c5b96..94e0d08a1 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
@@ -13,7 +13,7 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
 
 # Configure esbuild (the version is required)
 config :esbuild,
-  version: "0.13.5",
+  version: "0.14.0",
   default: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex
index 500b5c414..21afa6346 100644
--- a/lib/mix/tasks/phx.gen.release.ex
+++ b/lib/mix/tasks/phx.gen.release.ex
@@ -6,7 +6,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
   The following release files are created:
 
-    * `lib/app_name/release.exs` - A release module containing tasks for running
+    * `lib/app_name/release.ex` - A release module containing tasks for running
       migrations inside a release
 
     * `rel/overlays/bin/migrate` - A migrate script for conveniently invoking
@@ -45,6 +45,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
     app = Mix.Phoenix.otp_app()
     app_namespace = Mix.Phoenix.base()
+    web_namespace = app_namespace |> Mix.Phoenix.web_module() |> inspect()
 
     binding = [
       app_namespace: app_namespace,
@@ -124,7 +125,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
     Add the following to the top of your config/runtime.exs:
 
         if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
-          config :#{app}, #{app_namespace}.Endpoint, server: true
+          config :#{app}, #{web_namespace}.Endpoint, server: true
         end
     """)
 
@@ -135,7 +136,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
         host = System.get_env("PHX_HOST") || "example.com"
 
-        config :#{app}, #{app_namespace}.Endpoint,
+        config :#{app}, #{web_namespace}.Endpoint,
           ...,
           url: [host: host, port: 443]
     """)
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 543ba62fc..de943ea20 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -798,6 +798,10 @@ defmodule Phoenix.Endpoint do
             "//*.other.com"
           ]
 
+      Or to accept any origin matching the request connection's host, port, and scheme:
+
+          check_origin: :conn
+
       Or a custom MFA function:
 
           check_origin: {MyAppWeb.Auth, :my_check_origin?, []}
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index d1ced76fa..815b76ab9 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -531,7 +531,8 @@ defmodule Phoenix.Router do
 
   defp build_pipes(name, pipe_through) do
     plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
-    {conn, body} = Plug.Builder.compile(__ENV__, plugs, init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug)
+    opts = [init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug]
+    {conn, body} = Plug.Builder.compile(__ENV__, plugs, opts)
 
     quote do
       defp unquote(name)(unquote(conn)), do: unquote(body)
@@ -676,29 +677,35 @@ defmodule Phoenix.Router do
   See `pipeline/2` for more information.
   """
   defmacro plug(plug, opts \\ []) do
+    {plug, opts} = expand_plug_and_opts(plug, opts, __CALLER__)
+
+    quote do
+      if pipeline = @phoenix_pipeline do
+        @phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
+      else
+        raise "cannot define plug at the router level, plug must be defined inside a pipeline"
+      end
+    end
+  end
+
+  defp expand_plug_and_opts(plug, opts, caller) do
     runtime? = Phoenix.plug_init_mode() == :runtime
 
     plug =
       if runtime? do
-        expand_alias(plug, __CALLER__)
+        expand_alias(plug, caller)
       else
         plug
       end
 
     opts =
       if runtime? and Macro.quoted_literal?(opts) do
-        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+        Macro.prewalk(opts, &expand_alias(&1, caller))
       else
         opts
       end
 
-    quote do
-      if pipeline = @phoenix_pipeline do
-        @phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
-      else
-        raise "cannot define plug at the router level, plug must be defined inside a pipeline"
-      end
-    end
+    {plug, opts}
   end
 
   defp expand_alias({:__aliases__, _, _} = alias, env),
@@ -874,6 +881,13 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(options, do: context) do
+    options =
+      if Macro.quoted_literal?(options) do
+        Macro.prewalk(options, &expand_alias(&1, __CALLER__))
+      else
+        options
+      end
+
     do_scope(options, context)
   end
 
@@ -894,15 +908,23 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(path, options, do: context) do
-    options = Macro.expand(options, %{__CALLER__ | function: {:init, 1}})
+    options =
+      if Macro.quoted_literal?(options) do
+        Macro.prewalk(options, &expand_alias(&1, __CALLER__))
+      else
+        options
+      end
 
-    options = quote do
-      path = unquote(path)
-      case unquote(options) do
-        alias when is_atom(alias) -> [path: path, alias: alias]
-        options when is_list(options) -> Keyword.put(options, :path, path)
+    options =
+      quote do
+        path = unquote(path)
+
+        case unquote(options) do
+          alias when is_atom(alias) -> [path: path, alias: alias]
+          options when is_list(options) -> Keyword.put(options, :path, path)
+        end
       end
-    end
+
     do_scope(options, context)
   end
 
@@ -923,7 +945,7 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(path, alias, options, do: context) do
-    alias = Macro.expand(alias, %{__CALLER__ | function: {:init, 1}})
+    alias = expand_alias(alias, __CALLER__)
 
     options = quote do
       unquote(options)
@@ -985,7 +1007,7 @@ defmodule Phoenix.Router do
 
   """
   defmacro forward(path, plug, plug_opts \\ [], router_opts \\ []) do
-    plug = Macro.expand(plug, %{__CALLER__ | function: {:init, 1}})
+    {plug, plug_opts} = expand_plug_and_opts(plug, plug_opts, __CALLER__)
     router_opts = Keyword.put(router_opts, :as, nil)
 
     quote unquote: true, bind_quoted: [path: path, plug: plug] do
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 31b4134fc..7b9f15f41 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -354,24 +354,24 @@ defmodule Phoenix.Socket do
   See `Phoenix.Channel` for more information
   """
   defmacro channel(topic_pattern, module, opts \\ []) do
-    # Tear the alias to simply store the root in the AST.
-    # This will make Elixir unable to track the dependency between
-    # endpoint <-> socket and avoid recompiling the endpoint
-    # (alongside the whole project) whenever the socket changes.
-    module = tear_alias(module)
+    module = expand_alias(module, __CALLER__)
+
+    opts =
+      if Macro.quoted_literal?(opts) do
+        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+      else
+        opts
+      end
 
     quote do
       @phoenix_channels {unquote(topic_pattern), unquote(module), unquote(opts)}
     end
   end
 
-  defp tear_alias({:__aliases__, meta, [h|t]}) do
-    alias = {:__aliases__, meta, [h]}
-    quote do
-      Module.concat([unquote(alias)|unquote(t)])
-    end
-  end
-  defp tear_alias(other), do: other
+  defp expand_alias({:__aliases__, _, _} = alias, env),
+    do: Macro.expand(alias, %{env | function: {:channel, 3}})
+
+  defp expand_alias(other, _env), do: other
 
   @doc false
   @deprecated "transport/3 in Phoenix.Socket is deprecated and has no effect"
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index d609b3999..2f1d113a4 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -353,7 +353,7 @@ defmodule Phoenix.Socket.Transport do
       is_nil(origin) or check_origin == false ->
         conn
 
-      origin_allowed?(check_origin, URI.parse(origin), endpoint) ->
+      origin_allowed?(check_origin, URI.parse(origin), endpoint, conn) ->
         conn
 
       true ->
@@ -567,8 +567,11 @@ defmodule Phoenix.Socket.Transport do
           {module, function, arguments} ->
             {module, function, arguments}
 
+          :conn ->
+            :conn
+
           invalid ->
-            raise ArgumentError, ":check_origin expects a boolean, list of hosts, or MFA tuple, got: #{inspect(invalid)}"
+            raise ArgumentError, ":check_origin expects a boolean, list of hosts, :conn, or MFA tuple, got: #{inspect(invalid)}"
         end
 
       {:cache, check_origin}
@@ -588,13 +591,15 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
-  defp origin_allowed?({module, function, arguments}, uri, _endpoint),
+  defp origin_allowed?({module, function, arguments}, uri, _endpoint, _conn),
     do: apply(module, function, [uri | arguments])
-  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint),
+  defp origin_allowed?(:conn, uri, _endpoint, %Plug.Conn{} = conn),
+    do: uri.host == conn.host and uri.scheme == conn.scheme and uri.port == conn.port
+  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint, _conn),
     do: false
-  defp origin_allowed?(true, uri, endpoint),
+  defp origin_allowed?(true, uri, endpoint, _conn),
     do: compare?(uri.host, host_to_binary(endpoint.config(:url)[:host]))
-  defp origin_allowed?(check_origin, uri, _endpoint) when is_list(check_origin),
+  defp origin_allowed?(check_origin, uri, _endpoint, _conn) when is_list(check_origin),
     do: origin_allowed?(uri, check_origin)
 
   defp origin_allowed?(uri, allowed_origins) do
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index 7fd3de623..551405a6f 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -197,8 +197,8 @@ defmodule Phoenix.Token do
   ## Options
 
     * `:max_age` - verifies the token only if it has been generated
-      "max age" ago in seconds. A reasonable value is 1 day (86400
-      seconds)
+      "max age" ago in seconds. Defaults to the max age signed in the
+      token (86400)
     * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
       when generating the encryption and signing keys. Defaults to 1000
     * `:key_length` - option passed to `Plug.Crypto.KeyGenerator`


Test patch:
diff --git a/lib/phoenix/test/channel_test.ex b/lib/phoenix/test/channel_test.ex
index 057402695..68558af26 100644
--- a/lib/phoenix/test/channel_test.ex
+++ b/lib/phoenix/test/channel_test.ex
@@ -206,7 +206,7 @@ defmodule Phoenix.ChannelTest do
 
   """
   defmacro socket(socket_module) do
-    build_socket(socket_module, nil, [], __CALLER__)
+    socket(socket_module, nil, [], __CALLER__)
   end
 
   @doc """
@@ -218,22 +218,18 @@ defmodule Phoenix.ChannelTest do
 
   """
   defmacro socket(socket_module, socket_id, socket_assigns) do
-    build_socket(socket_module, socket_id, socket_assigns, __CALLER__)
+    socket(socket_module, socket_id, socket_assigns, __CALLER__)
   end
 
-  defp build_socket(socket, id, assigns, caller) do
+  defp socket(module, id, assigns, caller) do
     if endpoint = Module.get_attribute(caller.module, :endpoint) do
       quote do
-        %Socket{
-          assigns: Enum.into(unquote(assigns), %{}),
-          endpoint: unquote(endpoint),
-          handler: unquote(socket || first_socket!(endpoint)),
-          id: unquote(id),
-          pubsub_server: unquote(endpoint).config(:pubsub_server),
-          serializer: NoopSerializer,
-          transport: :channel_test,
-          transport_pid: self()
-        }
+        unquote(__MODULE__).__socket__(
+          unquote(module),
+          unquote(id),
+          unquote(assigns),
+          unquote(endpoint)
+        )
       end
     else
       raise "module attribute @endpoint not set for socket/2"
@@ -241,15 +237,17 @@ defmodule Phoenix.ChannelTest do
   end
 
   @doc false
-  @deprecated "Phoenix.ChannelTest.socket/0 is deprecated, please call socket/1 instead"
-  defmacro socket() do
-    build_socket(nil, nil, [], __CALLER__)
-  end
-
-  @doc false
-  @deprecated "Phoenix.ChannelTest.socket/2 is deprecated, please call socket/3 instead"
-  defmacro socket(id, assigns) do
-    build_socket(nil, id, assigns, __CALLER__)
+  def __socket__(socket, id, assigns, endpoint) do
+    %Socket{
+      assigns: Enum.into(assigns, %{}),
+      endpoint: endpoint,
+      handler: socket || first_socket!(endpoint),
+      id: id,
+      pubsub_server: endpoint.config(:pubsub_server),
+      serializer: NoopSerializer,
+      transport: {__MODULE__, fetch_test_supervisor!()},
+      transport_pid: self()
+    }
   end
 
   defp first_socket!(endpoint) do
@@ -259,6 +257,34 @@ defmodule Phoenix.ChannelTest do
     end
   end
 
+  defp fetch_test_supervisor!() do
+    case ExUnit.OnExitHandler.get_supervisor(self()) do
+      {:ok, nil} ->
+        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
+        {:ok, sup} = Supervisor.start_link([], opts)
+        ExUnit.OnExitHandler.put_supervisor(self(), sup)
+        sup
+
+      {:ok, sup} ->
+        sup
+
+      :error ->
+        raise ArgumentError, "socket/1-3 can only be invoked from the test process"
+    end
+  end
+
+  @doc false
+  @deprecated "Phoenix.ChannelTest.socket/0 is deprecated, please call socket/1 instead"
+  defmacro socket() do
+    socket(nil, nil, [], __CALLER__)
+  end
+
+  @doc false
+  @deprecated "Phoenix.ChannelTest.socket/2 is deprecated, please call socket/3 instead"
+  defmacro socket(id, assigns) do
+    socket(nil, id, assigns, __CALLER__)
+  end
+
   @doc """
   Initiates a transport connection for the socket handler.
 
@@ -279,7 +305,7 @@ defmodule Phoenix.ChannelTest do
   def __connect__(endpoint, handler, params, connect_info) do
     map = %{
       endpoint: endpoint,
-      transport: :channel_test,
+      transport: {__MODULE__, fetch_test_supervisor!()},
       options: [serializer: [{NoopSerializer, "~> 1.0.0"}]],
       params: __stringify__(params),
       connect_info: connect_info
@@ -383,9 +409,11 @@ defmodule Phoenix.ChannelTest do
         match_topic_to_channel!(socket, topic)
       end
 
+    %Socket{transport: {__MODULE__, sup}} = socket
+
     starter =
       fn _, _, _, spec ->
-        Supervisor.start_child(fetch_test_supervisor!(), %{spec | id: make_ref()})
+        Supervisor.start_child(sup, %{spec | id: make_ref()})
       end
 
     case Server.join(socket, channel, message, [starter: starter] ++ opts) do
@@ -397,23 +425,6 @@ defmodule Phoenix.ChannelTest do
     end
   end
 
-  # TODO: replace with ExUnit.Case.fetch_test_supervisor!() when we require Elixir v1.11.
-  defp fetch_test_supervisor!() do
-    case ExUnit.OnExitHandler.get_supervisor(self()) do
-      {:ok, nil} ->
-        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
-        {:ok, sup} = Supervisor.start_link([], opts)
-        ExUnit.OnExitHandler.put_supervisor(self(), sup)
-        sup
-
-      {:ok, sup} ->
-        sup
-
-      :error ->
-        raise ArgumentError, "fetch_test_supervisor!/0 can only be invoked from the test process"
-    end
-  end
-
   @doc """
   Pushes a message into the channel.
 
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index c4af2c7ea..da8c15b70 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -51,11 +51,13 @@ defmodule Phoenix.Socket.TransportTest do
   ## Check origin
 
   describe "check_origin/4" do
-    defp check_origin(origin, opts) do
-      conn = conn(:get, "/") |> put_req_header("origin", origin)
+    defp check_origin(%Plug.Conn{} = conn, origin, opts) do
+      conn = put_req_header(conn, "origin", origin)
       Transport.check_origin(conn, make_ref(), Endpoint, opts)
     end
 
+    defp check_origin(origin, opts), do: check_origin(conn(:get, "/"), origin, opts)
+
     test "does not check origin if disabled" do
       refute check_origin("/", check_origin: false).halted
     end
@@ -131,6 +133,22 @@ defmodule Phoenix.Socket.TransportTest do
       refute check_origin("", check_origin: mfa).halted
     end
 
+    test "checks origin against :host" do
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "http", port: 80}
+      refute check_origin(conn, "http://example.com", check_origin: :conn).halted
+
+      assert check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "ws://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "wss://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.another.com", check_origin: :conn).halted
+
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "https", port: 443}
+      refute check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "https://example.com:4000", check_origin: :conn).halted
+    end
+
     test "does not halt invalid URIs when check_origin is disabled" do
       refute check_origin("file://", check_origin: false).halted
       refute check_origin("null", check_origin: false).halted
diff --git a/test/phoenix/test/channel_test.exs b/test/phoenix/test/channel_test.exs
index ad5508047..ae4627201 100644
--- a/test/phoenix/test/channel_test.exs
+++ b/test/phoenix/test/channel_test.exs
@@ -187,27 +187,23 @@ defmodule Phoenix.Test.ChannelTest do
   ## socket
 
   test "socket/1" do
-    assert socket(UserSocket) == %Socket{
+    assert %Socket{
              endpoint: @endpoint,
              handler: UserSocket,
              pubsub_server: Phoenix.Test.ChannelTest.PubSub,
-             transport: :channel_test,
-             transport_pid: self(),
              serializer: Phoenix.ChannelTest.NoopSerializer
-           }
+           } = socket(UserSocket)
   end
 
-  test "socket/2" do
-    assert socket(UserSocket, "user:id", %{hello: :world}) == %Socket{
+  test "socket/3" do
+    assert %Socket{
              id: "user:id",
              assigns: %{hello: :world},
              endpoint: @endpoint,
              pubsub_server: Phoenix.Test.ChannelTest.PubSub,
-             transport: :channel_test,
-             transport_pid: self(),
              serializer: Phoenix.ChannelTest.NoopSerializer,
              handler: UserSocket
-           }
+           } = socket(UserSocket, "user:id", %{hello: :world})
   end
 
   ## join
@@ -220,7 +216,7 @@ defmodule Phoenix.Test.ChannelTest do
     assert socket.endpoint == @endpoint
     assert socket.pubsub_server == Phoenix.Test.ChannelTest.PubSub
     assert socket.topic == "foo:socket"
-    assert socket.transport == :channel_test
+    assert {Phoenix.ChannelTest, _} = socket.transport
     assert socket.transport_pid == self()
     assert socket.serializer == Phoenix.ChannelTest.NoopSerializer
     assert socket.assigns == %{hello: :world, original: :assign}
@@ -233,6 +229,18 @@ defmodule Phoenix.Test.ChannelTest do
     assert dictionary[:"$callers"] == [self()]
   end
 
+  test "join/3 from another process" do
+    socket = socket(UserSocket, "id", original: :assign)
+
+    assert {:ok, socket, client} =
+             Task.async(fn ->
+               join(socket, Channel, "foo:socket")
+              end)
+              |> Task.await()
+
+    assert %{socket | joined: true} == client
+  end
+
   test "join/3 with error reply" do
     assert {:error, %{reason: "mybad"}} =
              join(socket(UserSocket), Channel, "foo:error", %{"error" => "mybad"})