Repository: phoenixframework/phoenix
Pull request #3924 (2020-07-02 14:42:45)

Configurable static compressors
Following on the work in #3836 and #3840, I wanted to have a quick go at a more robust feature described in https://github.com/phoenixframework/phoenix/issues/3836#issuecomment-629623791

I added `compress_file?` to the behaviour to be able to not compress some assets with certain compressors if the content is too small (or similar).

I haven't written any docs/tests yet, and the naming/structure could use some updates, but figured I'd start the discussion with this 🙂

josevalim: I like it! :+1:

Patch:
diff --git a/assets/package-lock.json b/assets/package-lock.json
index a70e86d56..3826b4a2f 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "phoenix",
-  "version": "1.4.0",
+  "version": "1.5.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -4689,7 +4689,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4710,12 +4711,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4730,17 +4733,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4857,7 +4863,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4869,6 +4876,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4883,6 +4891,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -4890,12 +4899,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -4914,6 +4925,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4994,7 +5006,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5006,6 +5019,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5091,7 +5105,8 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5127,6 +5142,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5146,6 +5162,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5189,12 +5206,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
diff --git a/guides/contexts.md b/guides/contexts.md
index 5baf7d17f..0201e9fbc 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -45,7 +45,7 @@ Generated hello app
 The database for Hello.Repo has been dropped
 The database for Hello.Repo has been created
 
-14:38:37.418 [info]  Already up
+14:38:37.418 [info]  Migrations already up
 ```
 
 Now we're ready to create our accounts context. We'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting users, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root:
@@ -996,7 +996,7 @@ And it works! We now have two isolated contexts responsible for user accounts an
 
 ## Adding CMS functions
 
-Just like we extended our `Accounts` context with new application-specific functions like `Accounts.authenticate_by_email_password/2`, let's extend our generated `CMS` context with new functionality. For any CMS system, the ability to track how many times a page has been viewed is essential for popularity ranks. While we could try to use the existing `CMS.update_page` function, along the lines of `CMS.update_page(user, page, %{views: page.views + 1})`, this would not only be prone to race conditions, but it would also require the caller to know too much about our CMS system. To see why the race condition exists, let's walk through the possible execution of events:
+Just like we extended our `Accounts` context with new application-specific functions like `Accounts.authenticate_by_email_password/2`, let's extend our generated `CMS` context with new functionality. For any CMS system, the ability to track how many times a page has been viewed is essential for popularity ranks. While we could try to use the existing `CMS.update_page` function, along the lines of `CMS.update_page(page, %{views: page.views + 1})`, this would not only be prone to race conditions, but it would also require the caller to know too much about our CMS system. To see why the race condition exists, let's walk through the possible execution of events:
 
 Intuitively, you would assume the following events:
 
diff --git a/guides/controllers.md b/guides/controllers.md
index a6f4bff9d..7a7e0cc30 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -427,7 +427,7 @@ The flash functionality is handy when mixed with redirects. Perhaps you want to
   end
 ```
 
-Now if you reload the Welcome page, you will be redirect and the flash messages will be shown once more.
+Now if you reload the Welcome page, you will be redirected and the flash messages will be shown once more.
 
 Besides `put_flash/3` and `get_flash/2`, the `Phoenix.Controller` module has another useful function worth knowing about. `clear_flash/1` takes only `conn` and removes any flash messages which might be stored in the session.
 
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index 33af365a4..e5fb6cefd 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -26,7 +26,7 @@ Heroku is a great platform and Elixir performs well on it. However, you may run
 
 If you are just getting started or you don't expect to use the features above, Heroku should be enough for your needs. For instance, if you are migrating an existing application running on Heroku to Phoenix, keeping a similar set of features, Elixir will perform just as well or even better than your current stack.
 
-If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider [Distillery](https://github.com/bitwalker/distillery).
+If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider using `mix release`.
 
 ## Steps
 
diff --git a/guides/ecto.md b/guides/ecto.md
index c2dd6dfae..f9b1adf76 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -454,7 +454,7 @@ This will set up all the correct dependencies and configuration for us automatic
 
 If we have an existing application, all we need to do is switch adapters and make some small configuration changes.
 
-To switch adapters, we need to remove the Postgrex dependency and add a new one for Mariaex instead.
+To switch adapters, we need to remove the Postgrex dependency and add a new one for Myxql instead.
 
 Let's open up our `mix.exs` file and do that now.
 
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index 7c01a2698..c8c1968a6 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -46,6 +46,4 @@ There are a number of places to connect with community members at all experience
 
   * [LearnPhoenix.tv: Learn how to Build Fast, Dependable Web Apps with Phoenix (Feb 2017)](https://www.learnphoenix.tv/)
 
-  * [Build Real-Time Web Apps with Phoenix (Oct 2016)](https://pragprog.com/screencast/v-bhphnx/build-real-time-web-apps-with-phoenix)
-
   * [LearnPhoenix.io: Build Scalable, Real-Time Apps with Phoenix, React, and React Native (Aug 2016)](https://www.learnphoenix.io/)
diff --git a/guides/mix_tasks.md b/guides/mix_tasks.md
index 4c45d56d8..323a44eb1 100644
--- a/guides/mix_tasks.md
+++ b/guides/mix_tasks.md
@@ -585,13 +585,7 @@ The `--step` option will behave the same way.
 mix ecto.migrate --step 2
 ```
 
-We can also specify an individual migration we would like to run with the `-v` option.
-
-```console
-mix ecto.migrate -v 20150317170448
-```
-
-The `--to` option will behave the same way.
+The `--to` option will run all migrations up to and including given version.
 
 ```console
 mix ecto.migrate --to 20150317170448
@@ -690,4 +684,4 @@ If you want to make your new mix task to use your application's infrastructure,
     Mix.shell().info("Now I have access to Repo and other goodies!")
   end
   . . .
-```
\ No newline at end of file
+```
diff --git a/guides/plug.md b/guides/plug.md
index d044fc59f..d18c88bff 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -228,17 +228,16 @@ defmodule HelloWeb.MessageController do
   use HelloWeb, :controller
 
   def show(conn, params) do
-    case authenticate(conn) do
+    case Authenticator.find_user(conn) do
       {:ok, user} ->
         case find_message(params["id"]) do
           nil ->
             conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/")
           message ->
-            case authorize_message(conn, params["id"]) do
-              :ok ->
-                render(conn, :show, page: find_message(params["id"]))
-              :error ->
-                conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/")
+            if Authorizer.can_access?(user, message) do
+              render(conn, :show, page: message)
+            else
+              conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/")
             end
         end
       :error ->
@@ -259,7 +258,7 @@ defmodule HelloWeb.MessageController do
   plug :authorize_message
 
   def show(conn, params) do
-    render(conn, :show, page: find_message(params["id"]))
+    render(conn, :show, page: conn.assigns[:message])
   end
 
   defp authenticate(conn, _) do
diff --git a/guides/realtime/channels.md b/guides/realtime/channels.md
index a6296f1ae..982b5666d 100644
--- a/guides/realtime/channels.md
+++ b/guides/realtime/channels.md
@@ -156,7 +156,7 @@ The `Phoenix.Socket.Message` module defines a struct with the following keys whi
 PubSub consists of the `Phoenix.PubSub` module and a variety of modules for different adapters and their `GenServer`s.
 These modules contain functions which are the nuts and bolts of organizing Channel communication - subscribing to topics, unsubscribing from topics, and broadcasting messages on a topic.
 PubSub is used internally by Phoenix.
-It's also useful in application development in any case where you want to notify interested processes of an event; for instance, letting all connected [live views](https://github.com/phoenixframework/phoenix_live_view) know that a new comment has been added to a post.
+It's also useful in application development in any case where you want to notify interested processes of an event; for instance, letting all connected [live views](https://github.com/phoenixframework/phoenix_live_view) to know that a new comment has been added to a post.
 
 The PubSub system takes care of getting messages from one node to another so that they can be sent to all subscribers across the cluster.
 By default, this is done using [Phoenix.PubSub.PG2](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.PG2.html), which uses native BEAM messaging.
@@ -439,7 +439,7 @@ socket.connect()
 
 Now that we are connected, we can join channels with a topic:
 
-```elixir
+```javascript
 let channel = socket.channel("topic:subtopic", {})
 channel.join()
   .receive("ok", resp => { console.log("Joined successfully", resp) })
diff --git a/guides/routing.md b/guides/routing.md
index b418489a9..bea13811e 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -318,6 +318,8 @@ scope "/", HelloWeb do
 end
 
 scope "/admin", HelloWeb.Admin do
+  pipe_through :browser
+
   resources "/reviews", ReviewController
 end
 ```
@@ -352,6 +354,8 @@ We can fix this problem by adding an `as: :admin` option to our admin scope:
 ```elixir
 
 scope "/admin", HelloWeb.Admin, as: :admin do
+  pipe_through :browser
+
   resources "/reviews", ReviewController
 end
 ```
diff --git a/guides/views.md b/guides/views.md
index 3e1ecd7e8..14023537f 100644
--- a/guides/views.md
+++ b/guides/views.md
@@ -122,7 +122,7 @@ iex(1)> Phoenix.View.render(HelloWeb.PageView, "test.html", message: "Hello from
 {:safe, ["This is the message: ", "Hello from IEx!"]}
 ```
 
-As we can see, we're calling `render/3` with the individual view responsible for our test template, the name of our test template, and an empty map representing any assigns we might have wanted to pass in. The return value is a tuple beginning with the atom `:safe` and the resultant io list of the interpolated template. "Safe" here means that Phoenix has escaped the contents of our rendered template to avoid XSS injection attacks.
+As we can see, we're calling `render/3` with the individual view responsible for our test template, the name of our test template, and a set of assigns we might have wanted to pass in. The return value is a tuple beginning with the atom `:safe` and the resultant io list of the interpolated template. "Safe" here means that Phoenix has escaped the contents of our rendered template to avoid XSS injection attacks.
 
 Let's test out the HTML escaping, just for fun:
 
@@ -131,7 +131,7 @@ iex(2)> Phoenix.View.render(HelloWeb.PageView, "test.html", message: "<script>ba
 {:safe, ["This is the message: ", "&lt;script&gt;badThings();&lt;/script&gt;"]}
 ```
 
-If we need only the rendered string, without the whole tuple, we can use the `render_to_iodata/3`.
+If we need only the rendered string, without the whole tuple, we can use `render_to_string/3`.
 
 ```elixir
 iex(5)> Phoenix.View.render_to_string(HelloWeb.PageView, "test.html", message: "Hello from IEx!")
@@ -247,7 +247,7 @@ And the `render/2` matching `"show.json"`:
 
 This works because both "index.json" and "show.json" builds themselves on top of an internal "page.json" clause.
 
-The `render_many/3` function takes the data we want to respond with (`pages`), a view, and a string to pattern match on the `render/2` function defined on view. It will map over each item in `pages`, and call `PageView.render("page.json", %{page: page})`. `render_one/3` follows, the same signature, ultimately using the `render/2` matching `page.json` to specify what each `page` looks like. 
+The `render_many/3` function takes the data we want to respond with (`pages`), a view, and a string to pattern match on the `render/2` function defined on view. It will map over each item in `pages`, and call `PageView.render("page.json", %{page: page})`. `render_one/3` follows, the same signature, ultimately using the `render/2` matching `page.json` to specify what each `page` looks like.
 
 It's useful to build our views like this so they can be composable. Imagine a situation where our `Page` has a `has_many` relationship with `Author`, and depending on the request, we may want to send back `author` data with the `page`. We can easily accomplish this with a new `render/2`:
 
diff --git a/lib/mix/tasks/phx.digest.ex b/lib/mix/tasks/phx.digest.ex
index 9b5ae270a..970d2b9d8 100644
--- a/lib/mix/tasks/phx.digest.ex
+++ b/lib/mix/tasks/phx.digest.ex
@@ -37,6 +37,7 @@ defmodule Mix.Tasks.Phx.Digest do
 
   @doc false
   def run(all_args) do
+    Mix.Task.run "compile", all_args
     {opts, args, _} = OptionParser.parse(all_args, switches: [output: :string], aliases: [o: :output])
     input_path = List.first(args) || @default_input_path
     output_path = opts[:output] || input_path
diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex
index 15c89ee3f..46d47de7b 100644
--- a/lib/mix/tasks/phx.gen.json.ex
+++ b/lib/mix/tasks/phx.gen.json.ex
@@ -141,7 +141,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do
 
       Add the resource to your #{schema.web_namespace} :api scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
 
-          scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)} do
+          scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)}, as: :#{schema.web_path} do
             pipe_through :api
             ...
             resources "/#{schema.plural}", #{inspect schema.alias}Controller
diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex
index 26c898e7d..559bcb73b 100644
--- a/lib/phoenix/digester.ex
+++ b/lib/phoenix/digester.ex
@@ -118,8 +118,11 @@ defmodule Phoenix.Digester do
   defp manifest_join(path, filename), do: Path.join(path, filename)
 
   defp compiled_file?(file_path) do
+    compressors = Application.fetch_env!(:phoenix, :static_compressors)
+    compressed_extensions = Enum.flat_map(compressors, &(&1.file_extensions))
+
     Regex.match?(@digested_file_regex, Path.basename(file_path)) ||
-      Path.extname(file_path) in ~w(.gz .br) ||
+      Path.extname(file_path) in compressed_extensions ||
       Path.basename(file_path) == "cache_manifest.json"
   end
 
@@ -148,18 +151,25 @@ defmodule Phoenix.Digester do
     path = Path.join(output_path, file.relative_path)
     File.mkdir_p!(path)
 
-    # compressed files
-    if compress_file?(file) do
-      File.write!(
-        Path.join(path, file.digested_filename <> ".gz"),
-        :zlib.gzip(file.digested_content)
-      )
-
-      File.write!(
-        Path.join(path, file.filename <> ".gz"),
-        :zlib.gzip(file.content)
-      )
-    end
+    compressors = Application.fetch_env!(:phoenix, :static_compressors)
+
+    Enum.each(compressors, fn(compressor) ->
+      [file_extension | _] = compressor.file_extensions
+
+      with {:ok, compressed_digested} <- compressor.compress_file(file.digested_filename, file.digested_content) do
+        File.write!(
+          Path.join(path, file.digested_filename <> file_extension),
+          compressed_digested
+        )
+      end
+
+      with {:ok, compressed} <- compressor.compress_file(file.filename, file.content) do
+        File.write!(
+          Path.join(path, file.filename <> file_extension),
+          compressed
+        )
+      end
+    end)
 
     # uncompressed files
     File.write!(Path.join(path, file.digested_filename), file.digested_content)
@@ -168,10 +178,6 @@ defmodule Phoenix.Digester do
     file
   end
 
-  defp compress_file?(file) do
-    Path.extname(file.filename) in Application.fetch_env!(:phoenix, :gzippable_exts)
-  end
-
   defp digested_contents(file, latest) do
     ext = Path.extname(file.filename)
 
diff --git a/lib/phoenix/digester/compressor.ex b/lib/phoenix/digester/compressor.ex
new file mode 100644
index 000000000..d93c24a28
--- /dev/null
+++ b/lib/phoenix/digester/compressor.ex
@@ -0,0 +1,44 @@
+defmodule Phoenix.Digester.Compressor do
+  @moduledoc ~S"""
+  Defines the `Phoenix.Digester.Compressor` behaviour for
+  implementing static file compressors.
+
+  A custom compressor expects 2 functions to be implemented.
+
+  By default, Phoenix uses only `Phoenix.Digester.Gzip` to compress
+  static files, but additional compressors can be defined and added
+  to the digest process.
+
+  ## Example
+
+  If you wanted to compress files using an external brotli compression
+  library, you could define a new module implementing the behaviour and add the
+  module to the list of configured Phoenix static compressors.
+
+      defmodule MyApp.BrotliCompressor do
+        @behaviour Phoenix.Digester.Compressor
+
+        def compress_file(file_path, content) do
+          valid_extension = Path.extname(file_path) in Application.fetch_env!(:phoenix, :gzippable_exts)
+          compressed_content = :brotli.encode(content)
+
+          if valid_extension && byte_size(compressed_content) < byte_size(content) do
+            {:ok, compressed_content}
+          else
+            :error
+          end
+        end
+
+        def file_extensions do
+          [".br"]
+        end
+      end
+
+      # config/config.exs
+      config :phoenix,
+        static_compressors: [Phoenix.Digester.Gzip, MyApp.BrotliCompressor],
+        # ...
+  """
+  @callback compress_file(Path.t(), binary()) :: {:ok, binary()} | :error
+  @callback file_extensions() :: nonempty_list(String.t())
+end
diff --git a/lib/phoenix/digester/gzip.ex b/lib/phoenix/digester/gzip.ex
new file mode 100644
index 000000000..6f86b4638
--- /dev/null
+++ b/lib/phoenix/digester/gzip.ex
@@ -0,0 +1,18 @@
+defmodule Phoenix.Digester.Gzip do
+  @moduledoc ~S"""
+  Gzip compressor for Phoenix.Digester
+  """
+  @behaviour Phoenix.Digester.Compressor
+
+  def compress_file(file_path, content) do
+    if Path.extname(file_path) in Application.fetch_env!(:phoenix, :gzippable_exts) do
+      {:ok, :zlib.gzip(content)}
+    else
+      :error
+    end
+  end
+
+  def file_extensions do
+    [".gz"]
+  end
+end
diff --git a/lib/phoenix/presence.ex b/lib/phoenix/presence.ex
index 349afa02d..7d30fc529 100644
--- a/lib/phoenix/presence.ex
+++ b/lib/phoenix/presence.ex
@@ -100,7 +100,7 @@ defmodule Phoenix.Presence do
         users = presences |> Map.keys() |> Accounts.get_users_map()
 
         for {key, %{metas: metas}} <- presences, into: %{} do
-          {key, %{metas: metas, user: users[key]}}
+          {key, %{metas: metas, user: users[String.to_integer(key)]}}
         end
       end
 
diff --git a/mix.exs b/mix.exs
index 64508d104..5c09b3468 100644
--- a/mix.exs
+++ b/mix.exs
@@ -55,6 +55,7 @@ defmodule Phoenix.MixProject do
         filter_parameters: ["password"],
         serve_endpoints: false,
         gzippable_exts: ~w(.js .css .txt .text .html .json .svg .eot .ttf),
+        static_compressors: [Phoenix.Digester.Gzip],
         trim_on_html_eex_engine: true
       ]
     ]


Test patch:
diff --git a/guides/testing/testing.md b/guides/testing/testing.md
index 3604d5460..b1a681e45 100644
--- a/guides/testing/testing.md
+++ b/guides/testing/testing.md
@@ -68,7 +68,7 @@ Our test files simply define modules. At the top of each module, you will find a
 use HelloWeb.ConnCase
 ```
 
-If you were to write an Elixir library, outside of Phoenix, instead of `use HelloWeb.ConnCase`, you would write `use ExUnit.Case` instead. However, Phoenix already ships with a bunch of functionality for testing controllers and `HelloWeb.ConnCase` builds on top of `ExUnit.Case` to bring these functionalities in. We will explore the `HelloWeb.ConnCase` module soon.
+If you were to write an Elixir library, outside of Phoenix, instead of `use HelloWeb.ConnCase` you would write `use ExUnit.Case`. However, Phoenix already ships with a bunch of functionality for testing controllers and `HelloWeb.ConnCase` builds on top of `ExUnit.Case` to bring these functionalities in. We will explore the `HelloWeb.ConnCase` module soon.
 
 Then we define each test using the `test/3` macro. The `test/3` macro receives three arguments: the test name, the testing context that we are pattern matching on, and the contents of the test. In this test, we access the root page of our application by a "GET" HTTP request on the path "/" with the `get/2` macro. Then we **assert** that the rendered page contains the string "Welcome to Phoenix!".
 
@@ -113,7 +113,7 @@ There is a lot to unpack here.
 
 The second line says this is a case template. This is a ExUnit feature that allows developers to replace the built-in `use ExUnit.Case` by their own case. This line is pretty much what allows us to write `use HelloWeb.ConnCase` at the top of our controller tests.
 
-Now that we have made this module a case template, when can define callbacks that are invoked on certain occasions. The `using` callback defines code to be injected on every module that calls `use HelloWeb.ConnCase`. In this case, we import [`Plug.Conn`](https://hexdocs.pm/plug/Plug.Conn.html), so all of the connection helpers available in controllers are also available in tests, and then imports [`Phoenix.ConnTest`](https://hexdocs.pm/phoenix/Phoenix.ConnTest.html). You can consult these modules to learn all functionality available.
+Now that we have made this module a case template, we can define callbacks that are invoked on certain occasions. The `using` callback defines code to be injected on every module that calls `use HelloWeb.ConnCase`. In this case, we import [`Plug.Conn`](https://hexdocs.pm/plug/Plug.Conn.html), so all of the connection helpers available in controllers are also available in tests, and then imports [`Phoenix.ConnTest`](https://hexdocs.pm/phoenix/Phoenix.ConnTest.html). You can consult these modules to learn all functionality available.
 
 Then it aliases the module with all path helpers, so we can easily generate URLs in our tests. Finally, it sets the `@endpoint` module attribute with the name of our endpoint.
 
@@ -528,7 +528,7 @@ Remember to update your repository by running migrations:
 
 Now let's follow the directions and add the new resources route to our `lib/hello_web/router.ex` file and run the migrations.
 
-When we run `mix test` again, we see that we now have twenty tests!
+When we run `mix test` again, we see that we now have nineteen tests!
 
 ```console
 $ mix test
diff --git a/guides/testing/testing_contexts.md b/guides/testing/testing_contexts.md
index 5f76ccbfe..03495a96e 100644
--- a/guides/testing/testing_contexts.md
+++ b/guides/testing/testing_contexts.md
@@ -4,7 +4,7 @@
 
 > **Requirement**: This guide expects that you have gone through [the Introduction to Testing guide](testing.html).
 
-> **Requirement**: This guide expects that you have gone through [the Context guide](context.html).
+> **Requirement**: This guide expects that you have gone through [the Contexts guide](contexts.html).
 
 At the end of the Introduction to Testing guide, we generated an HTML resource for posts using the following command:
 
@@ -66,13 +66,13 @@ As the top of the file we import `Hello.DataCase`, which as we will see soon, it
 
 Next we define an alias, so we can refer to `Hello.Blog` simply as `Blog`.
 
-Then we start a `describe "posts"` block. A `describe` block is a feature in ExUnit that allows us to group similar tests. The reason why we have grouped all post related tests together is because contexts in Phoenix are capable of grouping multiple schemas together. For example, if you ran this command:
+Then we start a `describe "posts"` block. A `describe` block is a feature in ExUnit that allows us to group similar tests. The reason why we have grouped all post related tests together is because contexts in Phoenix are capable of grouping multiple schemas together. For example, if we ran this command:
 
 ```console
 $ mix phx.gen.html Blog Comment comments post_id:references:posts body:text
 ```
 
-We will would get a bunch of new functions in the `Hello.Blog` context plus a whole new `describe "comments"` block in our test file.
+We will get a bunch of new functions in the `Hello.Blog` context plus a whole new `describe "comments"` block in our test file.
 
 The tests defined for our context are very straight-forward. They call the functions in our context and assert on their results. As you can see, some of those tests even create entries in the database:
 
@@ -158,7 +158,7 @@ defmodule Hello.Blog.PostTest do
   alias Hello.Blog.Post
 
   test "title must be at least two characters long" do
-    changeset = Post.changeset(%User{}, %{title: "I"})
+    changeset = Post.changeset(%Post{}, %{title: "I"})
     assert %{title: ["should be at least 2 character(s)"]} = errors_on(changeset)
   end
 end
diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md
index d3e813582..eedaf0d8b 100644
--- a/guides/testing/testing_controllers.md
+++ b/guides/testing/testing_controllers.md
@@ -238,7 +238,7 @@ You may have noticed that this time the scaffold controller has generated fewer
 resources "/articles", ArticleController, except: [:new, :edit]
 ```
 
-`new` and `edit` are only necessary for HTML because they basically exist to assist users in creating and updating resources. Besides having less actions, we will the controller and view tests and implementations for JSON are drastically different from the HTML ones.
+`new` and `edit` are only necessary for HTML because they basically exist to assist users in creating and updating resources. Besides having less actions, we will notice the controller and view tests and implementations for JSON are drastically different from the HTML ones.
 
 The only thing that is pretty much the same between HTML and JSON is the contexts and the schema, which, once you think about it, it makes total sense. After all, your business logic should remain the same, regardless if you are exposing it as HTML or JSON.
 
@@ -386,7 +386,7 @@ The `action_fallback` can be extremely useful to reduce boilerplate when designi
 
 ### The `delete` action
 
-Finally, the last action we will stidy is the `delete` action for JSON. Its implementation looks like this:
+Finally, the last action we will study is the `delete` action for JSON. Its implementation looks like this:
 
 ```elixir
 def delete(conn, %{"id" => id}) do
diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex
index 64779cce9..7b06ed8b1 100644
--- a/lib/phoenix/test/conn_test.ex
+++ b/lib/phoenix/test/conn_test.ex
@@ -273,13 +273,13 @@ defmodule Phoenix.ConnTest do
   @doc """
   Gets the whole flash storage.
   """
-  @spec get_flash(Conn.t) :: Conn.t
+  @spec get_flash(Conn.t) :: map
   defdelegate get_flash(conn), to: Phoenix.Controller
 
   @doc """
   Gets the given key from the flash storage.
   """
-  @spec get_flash(Conn.t, term) :: Conn.t
+  @spec get_flash(Conn.t, term) :: term
   defdelegate get_flash(conn, key), to: Phoenix.Controller
 
   @doc """
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index f3fe7cd89..39dec3351 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -131,7 +131,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
 
       Add the resource to your Blog :api scope in lib/phoenix_web/router.ex:
 
-          scope "/blog", PhoenixWeb.Blog do
+          scope "/blog", PhoenixWeb.Blog, as: :blog do
             pipe_through :api
             ...
             resources "/posts", PostController
diff --git a/test/phoenix/digester/gzip_test.exs b/test/phoenix/digester/gzip_test.exs
new file mode 100644
index 000000000..76c08e1eb
--- /dev/null
+++ b/test/phoenix/digester/gzip_test.exs
@@ -0,0 +1,13 @@
+defmodule Phoenix.Digester.GzipTest do
+  use ExUnit.Case, async: true
+  alias Phoenix.Digester.Gzip
+
+  test "compress_file/2 compresses file" do
+    file_path = "test/fixtures/digest/priv/static/css/app.css"
+    content = File.read!(file_path)
+
+    {:ok, compressed} = Gzip.compress_file(file_path, content)
+
+    assert is_binary(compressed)
+  end
+end
diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs
index 37df1b8d6..12cccaf8b 100644
--- a/test/phoenix/digester_test.exs
+++ b/test/phoenix/digester_test.exs
@@ -91,7 +91,6 @@ defmodule Phoenix.DigesterTest do
 
       json = Path.join(@output_path, "cache_manifest.json") |> json_read!()
       refute json["latest"]["precompressed.js.gz"]
-      refute json["latest"]["precompressed.js.br"]
 
       assert :ok = Phoenix.Digester.compile(@output_path, @output_path)
       assert output_files == assets_files(@output_path)