Until now, the Cookbook app you generated in [Chapter 1](/guide/setting-up-minitest/#generate-the-cookbook-app) lets any visitor create, edit, and delete recipes. That is not how production apps work and the delay was intentional so each chapter stayed focused. Adding authentication from chapter one would have stacked too many new concepts on top of testing ideas that were still new.

If you worked through the guide in order, sign-in is not a brand-new product idea either. In [Chapter 4](/guide/how-to-approach-testing/), you already signed in to the reference Cookbook clone for scenario drills. That clone already had nearly every feature your app is building towards.

This chapter adds authentication: who is logged in? Authentication answers "can this person establish a session with a valid email and password?" [Chapter 13](/guide/authorization-testing/) adds authorization: given a session, may this user edit this recipe?

<%= render Guide::ChapterProgress.new(
  feature: "Rails 8 authentication generator, read-only routes and UI for guests, session helpers for login during tests, password reset.",
  why: "You know the basic testing habits now, so it is the right time to add authentication. Real apps authenticate earlier; this chapter catches the Cookbook app up while those habits stay the same.",
  test: "Integration: sign in and sign out; guest create/edit/destroy denials; guest can view list and show; sign in before create, update, and destroy; password reset flow.
  System: sign-in form; sign in before create, update, and destroy; password reset smoke.",
  later: "[Chapter 13](/guide/authorization-testing/) adds ownership so only the recipe owner can edit or destroy."
) %>

## Why you waited until now

Early chapters already stack plenty: Minitest folders, TDD concept (red/green), fixtures, system tests, model validations, nested forms. Authentication adds sessions, user fixtures, and different actors in the same tests.

Letting guests use full create, update, and destroy in Chapters 5 through 11 keeps each chapter small and focused. You ask "does the form work?" without having to also ask "who is allowed?" every time. Production apps usually authenticate much earlier but for teaching, the delay is a feature.

From this chapter on, guests browse only. Any signed-in user can still create, edit, update, or destroy any recipe. That is enough for authentication, but not yet for the ownership (authorization). Production apps usually let only the recipe owner edit or destroy. Those rules land in [Chapter 13](/guide/authorization-testing/) so this chapter stays focused on authentication: "is the user signed in?"

## What you will do in this chapter

1. Run `bin/rails generate authentication`, review the generated files, then migrate new tables.
2. Add a fixture file for user and add Alice as a user with a password.
3. Add a `current_user` helper on the `Authentication` concern.
4. Sign in: skim generated controller code, write integration coverage, then red/green the sign-in system smoke.
5. Review generated session helper for integration and add a new session helper for system tests.
6. Sign out: integration and system tests using `sign_in_*` helpers.
7. Guest read-only access: write denials, allow index/show pages to guests, then prove signed-in users see New, Edit, Destroy, and Remove buttons.
8. TDD recipe create, update, and destroy tests (integration, then system): fail without a session (red), then sign in and go green.
9. Password reset: skim generated controller, then integration and system coverage.
10. Delete the scaffold `recipes_controller_test.rb` (it drifts under auth; integration already owns Recipes HTTP).
11. Run `bin/rails test:all` to ensure all tests pass, then commit.

## Scenarios to automate

These are the outcomes you will prove by the end of the chapter. Each feature follows the same shape: a main section, then integration tests, then a thin system smoke when the browser path matters. You start with sign in, then session helpers, then sign out, then guest access, then signed-in recipe writes, then password reset.

| Scenario | Expected |
| --- | --- |
| Sign in | Valid email and password establish a session |
| Sign in failure | Wrong password does not establish a session |
| Sign out | Session ends after DELETE |
| Guest list | GET recipes list succeeds without a session (already in `recipes_integration_test.rb`) |
| Guest show | GET recipe show succeeds without a session (already in `recipes_integration_test.rb`) |
| Guest new | Open new recipe while signed out; land on sign-in |
| Guest create | Create without a session adds no row; land on sign-in |
| Guest update | Update without a session leaves the title alone; land on sign-in |
| Guest destroy | Destroy without a session leaves the row; land on sign-in |
| Guest UI | No New or Destroy on the list; no Edit, Destroy, or ingredient/step Remove on show |
| Signed-in UI | Signed-in user sees New and Destroy on the list; Edit, Destroy, and Remove on show |
| Signed-in CRUD | Chapter 7 create, update, and destroy still work after the introduction of authentication |
| Password reset request | Known email enqueues reset mail and redirects to sign-in; unknown email sends no mail |
| Password reset update | Valid token + matching passwords change the digest and redirect to sign-in |
| Password reset UI | Forgot-password form, set a new password, sign in with the new password |

## Generate authentication

Rails 8 shipped with a lot of goodies and one of them was the built-in authentication generator (finally, bye bye Devise!). A single command `bin/rails generate authentication` gives you users, sessions, sign-in, and password reset. It also ships with an `Authentication` concern that ensures user is logged in before accessing any action inside a controller. The generator is deliberately simple so you can customize it to your needs.

Run the following command from your app root to add authentication to the app:

```bash
bin/rails generate authentication
```

### What the generator added

These are the main files and edits you get in the Cookbook app after running the generator. You do not need to memorize every path, just skim the list so you know where files related to authentication live and how they are wired together.

| Path | What it does |
| --- | --- |
| `app/models/user.rb` | Responsible for user accounts in the app. Uses `has_secure_password` that adds methods to set and authenticate against a BCrypt password, `has_many :sessions`, and normalizes `email_address` (strip + downcase) before save. |
| `app/models/session.rb` | One database row per signed-in browser. Belongs to a user. The signed cookie points at this row. |
| `app/models/current.rb` | Responsible for holding the current user for the request. Controllers and views reach the signed-in user through `Current.user`. |
| `app/controllers/concerns/authentication.rb` | Code to enforce authentication lives here. Requires a session by default, lets controllers opt-out of authentication for guests with `allow_unauthenticated_access`, resumes a session from the cookie, and exposes helpers like `authenticated?` to check if the user is signed in or not. |
| `app/controllers/application_controller.rb` | Includes the `Authentication` concern so every controller inherits the before-action for enforcing authentication (edited file). |
| `app/controllers/sessions_controller.rb` | Controller responsible for logging in and out of the app. |
| `app/controllers/passwords_controller.rb` | Responsible for handling the forgot password flow. Request a reset email, then set a new password from the token link. |
| `app/mailers/passwords_mailer.rb` | Responsible for sending the password reset email with a signed token link. |
| `app/views/sessions/*.html.erb` | Views for session controller. |
| `app/views/passwords/*.html.erb` | Views for password controller. |
| `app/views/passwords_mailer/*.html.erb` | Views for password reset email. |
| `db/migrate/*_create_users.rb` | Creates the `users` table with columns `email_address`, `password_digest`, and timestamps. |
| `db/migrate/*_create_sessions.rb` | Creates the `sessions` table with columns `user_id`, optional `ip_address` and `user_agent`. |
| `test/test_helpers/session_test_helper.rb` | Session helpers for integration tests. `sign_in_as(user)` sets the signed session cookie; `sign_out` clears it. Loaded only for controller/integration tests, not available in system tests. |
| `test/fixtures/users.yml` | Starter user fixture from the generator. You will replace it with custom user record in this chapter. |

### Review new migration files

Open the two migrations the generator added `db/migrate/*_create_users.rb` and `db/migrate/*_create_sessions.rb` and take a quick look at the columns for each table.

A production app often grows more columns on `users` table (things like `first_name`, `last_name`, `address`, etc.). But you will stay on the generator defaults for the Cookbook app to keep the setup simple.

#### `users`

| Column | Purpose |
| --- | --- |
| `email_address` | Unique login identity (`null: false`, unique index) |
| `password_digest` | bcrypt digest for `has_secure_password` (`null: false`) |
| `created_at` / `updated_at` | Timestamps |

#### `sessions`

| Column | Purpose |
| --- | --- |
| `user_id` | Foreign key to the signed-in user (`null: false`) |
| `ip_address` | Optional client IP stored when the session starts |
| `user_agent` | Optional browser / client string stored when the session starts |
| `created_at` / `updated_at` | Timestamps |

Once you are comfortable with the columns, migrate the new tables with the following command:

```bash
bin/rails db:migrate
```

## Change root url for the app

Successful sign-in redirects to `root_url` that defaults to the welcome page for a brand new Rails app. Until you set a root route manually, redirects will keep landing on the default Rails welcome page. Point root at the recipes list so sign-in drops you on Cookbook home instead of the unused welcome page. In `config/routes.rb`, find the commented `root "posts#index"` example, uncomment it, and point it at the recipes list:

```ruby
# config/routes.rb
root "recipes#index"
```

From here on, the recipes list will be the app home after sign-in.

## User fixtures

Authentication generator ships with a starter user fixture at `test/fixtures/users.yml`. It declares users as `one` and `two` by default, I am not a fan of that setup so update it to use a real name instead. Open the `test/fixtures/users.yml` file and replace the `one:` with the following:

```yaml
# test/fixtures/users.yml
alice:
  email_address: alice@example.com
  password_digest: <%%= password_digest %>
```

You can leave the `two:` entry as it is. You will replace it with another user in the [next chapter](/guide/authorization-testing/).

## Add a `current_user` helper

The generator stores the signed-in user on `Current.user` (attribute on the `Current` model). Because I come from Devise background I like `current_user` to access the signed-in user instead of `Current.user` which I reads and writes longer. You can use `Current.user` in the view if you want, it's just a matter of preference.

Find the existing `helper_method :authenticated?` line and include `:current_user`. Then add the `current_user` method just below the `authenticated?` method at `app/controllers/concerns/authentication.rb`:

```ruby
# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :authenticated?, :current_user
  end

  # ... class_methods and other private methods unchanged ...

  private
    def authenticated?
      resume_session
    end

    def current_user
      return nil unless authenticated?
      return @current_user if defined?(@current_user)

      @current_user = Current.user
    end

  # ... rest of the concern unchanged ...
end
```

This is what's happening in the code above:

- `helper_method :current_user` exposes this helper method to views and controllers.
- `current_user` returns `nil` with no session. With a session it memoizes `Current.user` once per request.

Going forward, you will use the `current_user` helper in the view and controller to access the signed-in user instead of `Current.user`.

## Sign in to the app

With the authentication generator in place, you can now sign users in to the app. In this section, you will test the actual sign in flow over HTTP and in the browser.

### Controller tests

The generator adds a session test at `test/controllers/sessions_controller_test.rb`. Skim it for an idea of what is covered. We will not dig into controller tests for authentication here and focus on integration and system tests like we did in the previous chapters. You can delete the generated controller test file if you want. That will not break the test suite nor the app.

### Integration tests

With integration tests, you prove the sign-in flow works over HTTP.

Before you write the test, skim the session controller generated by the authentication generator so you know what to test.

```ruby
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end
```

This is what's happening in the code above:

- `allow_unauthenticated_access only: %i[ new create ]` skips the authentication check for the `new` and `create` actions, allowing guests to reach the sign-in form and submit it. `allow_unauthenticated_access` comes from the `Authentication` concern at `app/controllers/concerns/authentication.rb`.
- `rate_limit` caps sign-in attempts, then redirects back to sign-in with an alert. This is a security measure to prevent brute force attacks.
- `new` renders the sign-in form.
- `create` authenticates with `User.authenticate_by`, starts a session, and redirects to `after_authentication_url` (defaults to `root_url`). If the email or password is incorrect, it redirects back to the sign-in page with an alert.
- `destroy` terminates the session (signs out the user) and redirects to the sign-in page.

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Valid sign in | Open sign-in form, POST correct password, redirect to `root_url` |
| Invalid password | POST wrong password, redirect to `new_session_url`, sign-in form still present |

#### Add scenarios to the test file

Create a session integration test file with `nano test/integration/session_integration_test.rb` and add the following scenarios:

```ruby
# test/integration/session_integration_test.rb
  # Actor: visitor with an account
  # Starting point: not signed in
  # Action: GET sign-in form, then POST valid email and password
  # Expected outcome: redirect to root_url; session established
  # test "signs in" do
  # end

  # Actor: visitor with an account
  # Starting point: not signed in
  # Action: POST wrong password
  # Expected outcome: redirect to new_session_url; form still present
  # test "rejects invalid password" do
  # end
```

#### Add the integration tests

Replace the test body with the following:

```ruby
# test/integration/session_integration_test.rb
require "test_helper"

class SessionIntegrationTest < ActionDispatch::IntegrationTest
  test "signs in" do
    get new_session_url
    assert_response :success

    post session_url, params: {
      email_address: users(:alice).email_address,
      password: "password"
    }
    assert_redirected_to root_url
    follow_redirect!
    assert_response :success
  end

  test "rejects invalid password" do
    post session_url, params: {
      email_address: users(:alice).email_address,
      password: "wrong"
    }
    assert_redirected_to new_session_url
    follow_redirect!
    assert_select "form"
  end
end
```

This is what's happening in the code above:

- `signs in` opens the form, posts Alice's fixture email and the shared test password, then follows the redirect to `root_url`.
- `rejects invalid password` posts a bad password and expects a redirect back to `new_session_url`, then checks the form is still there.

Run the session integration test, you should see 0 failures and 0 errors.

```bash
bin/rails test test/integration/session_integration_test.rb
```

### System tests

Integration tests proved the sign-in over HTTP. One system test proves the real form and the UI used by the human to sign in to the app is working correctly.

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Sign in to the app | Fill Alice credentials, land on recipes list, see Sign out |

#### Add scenarios to the test file

Create a system test file with `nano test/system/sessions_test.rb` and add the following scenarios:

```ruby
# test/system/sessions_test.rb
  # Actor: visitor with an account
  # Starting point: signed out
  # Action: visit sign-in, fill Alice credentials, click Sign in
  # Expected outcome: recipes list heading and Sign out
  # test "signs in" do
  # end
```

#### Red: add the system test

Replace the test body with the following:

```ruby
# test/system/sessions_test.rb
require "application_system_test_case"

class SessionsTest < ApplicationSystemTestCase
  test "signs in" do
    visit new_session_url
    fill_in "Enter your email address", with: users(:alice).email_address
    fill_in "Enter your password", with: "password"
    click_on "Sign in"

    assert_selector "h1", text: "Recipes"
    assert_text "Sign out"
  end
end
```

Run the system test:

```bash
bin/rails test test/system/sessions_test.rb
```

You want a failure on `assert_text "Sign out"`. The failure is expected since the view does not yet have the button to sign out of the app.

```bash
F

Failure:
SessionsTest#test_signs_in [test/system/sessions_test.rb]:
expected to find text "Sign out" in "Recipes..."
```

#### Green: add Sign in and Sign out to the application layout

Update the `app/views/layouts/application.html.erb` file to include the sign in link and sign out button. Add the following code inside `<body>` tag and before the `yield` statement:

```erb
<%%# app/views/layouts/application.html.erb %>
<html>
  <!-- Other code -->

  <body>
    <%% if authenticated? %>
      <p>Signed in as <%%= current_user.email_address %></p>
      <%%= button_to "Sign out", session_path, method: :delete %>
    <%% else %>
      <%%= link_to "Sign in", new_session_path if request.path != new_session_path %>
    <%% end %>

    <%%= yield %>
  </body>
</html>
```

This is what's happening in the code above:

- `authenticated?` is a helper exposed by the `Authentication` concern that checks if the user is signed in or not.
- `button_to "Sign out"` signs the user out of the app.
- Guests see Sign in unless they are either already on the sign-in page or signed in to the app.

Run the system test again, you want 0 failures and 0 errors.

```bash
bin/rails test test/system/sessions_test.rb
```

## Sign-in helper for tests

You just proved the real sign-in form. For sign out and other protected routes, you will need a way to sign in the user first. That can be done in two ways:

1. By driving the sign-in form on every request.
2. By setting the signed `session_id` directly before every request (this is how Rails knows the user is signed in).

Reserve the first approach for testing the sign-in form itself (the section you just finished). Prefer the second for sign out, protected routes for the recipe, and other tests that only need a session already in place. Second approach is more efficient and avoids the overhead of driving the sign-in form on every request while still ensuring the user is signed in.

<%= render Shared::Tip.new(
  title: "Protected routes",
  markdown: <<~MD
    A **protected route** is any action that requires a signed-in session before Rails runs it. After `generate authentication`, `ApplicationController` includes the `Authentication` concern, so every controller action is protected by default. Guests who hit a protected route get redirected to sign-in.
  MD
) %>

### For Integration tests

The authentication generator already ships with a session helper required for integration tests to sign in the user. This is what the session helper at `test/test_helpers/session_test_helper.rb` looks like:

```ruby
# test/test_helpers/session_test_helper.rb
module SessionTestHelper
  def sign_in_as(user)
    Current.session = user.sessions.create!

    ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
      cookie_jar.signed[:session_id] = Current.session.id
      cookies["session_id"] = cookie_jar[:session_id]
    end
  end

  def sign_out
    Current.session&.destroy!
    cookies.delete("session_id")
  end
end

ActiveSupport.on_load(:action_dispatch_integration_test) do
  include SessionTestHelper
end
```

This is what's happening in the code above:

- `sign_in_as(user)` creates a real `Session` row and sets the signed cookie the Authentication concern reads. You can use this helper when a test needs a signed-in user without going through the sign in flow via the form.
- `sign_out` destroys the current session and deletes the cookie.
- `ActiveSupport.on_load` includes the module on every `ActionDispatch::IntegrationTest`, so session and recipe tests can call `sign_in_as` without having to `include SessionTestHelper` in each test file.

### For System tests

System tests are not generated by default in recent Rails versions, which also means Rails doesn't generate a session helper required for system tests. You need to add it manually.

Replace the content of `test/application_system_test_case.rb` with the following to include the session helper for signing in the user to the app:

```ruby
# test/application_system_test_case.rb
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driver = ENV["HEADFUL"] == "1" ? :chrome : :headless_chrome
  driven_by :selenium, using: driver, screen_size: [ 1400, 1400 ]

  teardown { Capybara.reset_sessions! }

  def sign_in_to_ui_as(user)
    Current.session = user.sessions.create!

    ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
      cookie_jar.signed[:session_id] = Current.session.id

      visit new_session_url

      page.driver.browser.manage.add_cookie(
        name: :session_id,
        value: cookie_jar[:session_id],
        sameSite: :Lax,
        httpOnly: true
      )
    end
  end
end
```

This is what's happening in the code above:

- `sign_in_to_ui_as(user)` builds a real `Session` row, visits the sign-in page so the browser has a host, then injects the signed `session_id` cookie to the browser.
- `teardown { Capybara.reset_sessions! }` clears browser cookies between tests so a leftover session does not leak.
- The method lives in `ApplicationSystemTestCase` which is the base class for all system tests, so every system test can call it directly.

## Sign out of the app

To exercise the sign out path, you need an existing session first. With the `sign_in_as` helper in place, you can easily sign in to the app with a single line of code. You can then use the `delete session_url` to sign out the user.

### Integration tests

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Sign out | After `sign_in_as`, DELETE session redirects to `new_session_url` |

#### Add the scenario

Append the following scenario to `test/integration/session_integration_test.rb` (same file as sign in):

```ruby
# test/integration/session_integration_test.rb
  # Actor: signed-in user
  # Starting point: alice session from sign_in_as
  # Action: DELETE session
  # Expected outcome: redirect to new_session_url
  # test "signs out" do
  # end
```

#### Add the integration test

Replace the body of `test "signs out"` with the following:

```ruby
# test/integration/session_integration_test.rb
test "signs out" do
  sign_in_as users(:alice)

  delete session_url
  assert_redirected_to new_session_url
  assert_empty cookies["session_id"]
end
```

This is what's happening in the code above:

- `sign_in_as users(:alice)` establishes the session cookie without driving the form again.
- `delete session_url` hits `SessionsController#destroy`, which terminates the session and redirects the user to the sign-in page.
- `assert_empty cookies["session_id"]` checks that the session cookie is deleted after signing out.

Run the full session integration test:

```bash
bin/rails test test/integration/session_integration_test.rb
```

You want 0 failures and 0 errors across sign in, bad password, and sign out.

### System tests

The sign-in system test already requires a visible Sign out button after a successful form login. Now, prove that the user is signed out after clicking the Sign out button and ensure they land on the sign-in page:

#### Add the scenario

Append the following scenario to `test/system/sessions_test.rb`:

```ruby
# test/system/sessions_test.rb
  # Actor: Alice, signed in via UI helper
  # Starting point: recipes list
  # Action: click Sign out
  # Expected outcome: land on sign-in path; Sign in button visible
  # test "signs out" do
  # end
```

#### Add the system test

Replace the body of `test "signs out"` with the following:

```ruby
# test/system/sessions_test.rb
test "signs out" do
  sign_in_to_ui_as users(:alice)
  visit recipes_url
  click_on "Sign out"
  assert_current_path new_session_path
  assert_button "Sign in"
end
```

<%= render Shared::Tip.new(
  title: "New assertion",
  markdown: <<~MD
    **`assert_button "Sign in"`** asks "is there a button (or submit) with this exact text?"
    Syntax: `assert_button` plus the visible label.
  MD
) %>

This is what's happening in the code above:

- `sign_in_to_ui_as users(:alice)` signs in the user to the app.
- `visit recipes_url` visits the recipes list page.
- `click_on "Sign out"` clicks the Sign out button.
- `assert_current_path new_session_path` checks that the user is redirected to the sign-in page.

Run the full system test, you want 0 failures and 0 errors.

```bash
bin/rails test test/system/sessions_test.rb
```

<%= render Guide::LoadDevelopmentFixtures.new(
  after_load: "Sign in as Alice (`alice@example.com` / `password`), confirm Sign out and the recipes list, then sign out and land on the Sign in form again."
) %>

Next, quickly surf in the browser to confirm sign in and sign out:

1. Start `bin/dev` if the server is not running.
2. Visit `http://localhost:3000/session/new` (or click the Sign in link in the page).
3. Sign in as Alice with email `alice@example.com` and password `password`.
4. Confirm you land on the recipes list, see fixture titles, and see Sign out button.
5. Click Sign out. Confirm you land on the sign-in page and see the Sign in button.

With sign in, helpers, and sign out in place, you can now lock the recipe write paths so guests can only browse while signed in users can create, edit and destroy recipes.

## Guest can only read recipes

Until now, guests can browse the list, open a recipe, and create, edit, or destroy recipes. That changes from this point forward: you lock write actions so guests can only read. They should not reach new, create, edit, update, or destroy: any section that modifies the database.

### Integration tests

`ApplicationController` already includes the `Authentication` concern, so every recipe action requires a session (logged in user) by default. This is why the denials paths for recipe modification actions for guests will be true and tests for those will pass once you write them.

The red step you need to fix is that your older list and show tests in `recipes_integration_test.rb` still expect guests to open those pages and they will now fail because these actions require users to be signed in to the app.

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Guest list | Open the recipes list without a session; recipe titles are visible; no New recipe or Destroy this recipe. Covered in `recipes_integration_test.rb`. |
| Guest show | Open pancakes show without a session; recipe title is visible; no Edit or Destroy. Nested Remove is covered with ingredients and steps. Covered in `recipes_integration_test.rb`. |
| Guest new | Open new recipe while signed out; redirected to sign-in |
| Guest create | Submit a create without a session; no new row; redirected to sign-in |
| Guest update | Submit an update without a session; title unchanged; redirected to sign-in |
| Guest destroy | Submit a destroy without a session; recipe count unchanged in the database; redirected to sign-in |
| Signed-in list UI | User signed in; New recipe and Destroy this recipe are present on the list |
| Signed-in show UI | User signed in; Edit, Destroy, and ingredient/step Remove are present on show |

#### Add scenarios to the test file for guest access restrictions

Create a new test file with `nano test/integration/recipe_access_integration_test.rb` and add the following scenarios:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: guest
  # Starting point: recipe count known
  # Action: open new form, then POST create without session
  # Expected outcome: both redirect to sign-in; no new row
  # test "guest cannot create a recipe" do
  # end

  # Actor: guest
  # Starting point: recipes(:pancakes) with known title
  # Action: open edit form, then PATCH update without session
  # Expected outcome: both redirect to sign-in; title unchanged
  # test "guest cannot edit a recipe" do
  # end

  # Actor: guest
  # Starting point: recipes(:lentil_soup) exists
  # Action: submit destroy without session
  # Expected outcome: row count unchanged; redirect to sign-in
  # test "guest cannot destroy a recipe" do
  # end
```

#### Add the access integration tests

Fill in the test bodies with the following test cases:

```ruby
# test/integration/recipe_access_integration_test.rb
require "test_helper"

class RecipeAccessIntegrationTest < ActionDispatch::IntegrationTest
  test "guest cannot create a recipe" do
    get new_recipe_url
    assert_redirected_to new_session_url

    assert_no_difference("Recipe.count") do
      post recipes_url, params: { recipe: { title: "Sneaky soup" } }
    end
    assert_redirected_to new_session_url
  end

  test "guest cannot edit a recipe" do
    recipe = recipes(:pancakes)
    original_title = recipe.title

    get edit_recipe_url(recipe)
    assert_redirected_to new_session_url

    patch recipe_url(recipe), params: { recipe: { title: "Hacked title" } }
    assert_redirected_to new_session_url
    assert_equal original_title, recipe.reload.title
  end

  test "guest cannot destroy a recipe" do
    recipe = recipes(:lentil_soup)

    assert_no_difference("Recipe.count") do
      delete recipe_url(recipe)
    end
    assert_redirected_to new_session_url
  end
end
```

This is what's happening in the code above:

- Each test stays signed out on purpose. Guests must bounce to sign-in.
- Create and destroy use `assert_no_difference("Recipe.count")` because those actions add or remove a row.
- Edit is a title-only `patch`. Row count would stay the same even if the hack succeeded, so the test keeps `original_title` and asserts `recipe.reload.title` instead.

Run the integration test for guest access restrictions, you want 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

#### Red: guest browse in `recipes_integration_test`

List and show still live in `test/integration/recipes_integration_test.rb`. After authentication, those older tests fail because guests can no longer open index or show pages.

Update the list, show, and nested-detail tests so they assert that create, edit, destroy and remove buttons are not present for guests.

```ruby
# test/integration/recipes_integration_test.rb
test "visits the list" do
  # ... existing code unchanged ...
  assert_select "a", text: "New recipe", count: 0
  assert_select "button", text: "Destroy this recipe", count: 0
end

test "shows a recipe" do
  # ... existing code unchanged ...
  assert_select "a", text: "Edit this recipe", count: 0
  assert_select "button", text: "Destroy this recipe", count: 0
end

test "shows ingredients and steps on the recipe page" do
  # ... existing code unchanged ...
  assert_select "button", text: "Remove", count: 0
end
```

<%= render Shared::Tip.new(
  title: "New assertions",
  markdown: <<~MD
    1. **`assert_select "a", text: "New recipe", count: 0`**
      Asks "how many links with this exact text are in the response HTML?"
      Syntax: CSS-ish selector, optional `text:`, then `count: 0` when the control must be absent. Same idea for Edit this recipe.

    2. **`assert_select "button", text: "Destroy this recipe", count: 0`**
      Same as `assert_select "a"` but for `<button>` tags.

    3. **`assert_select "button", text: "Remove", count: 0`**
      Ingredient and step Remove buttons also use `button_to`. Keep this assert in `shows ingredients and steps on the recipe page`, next to the nested Salt / Flour / Steps checks from Chapter 9.
  MD
) %>

Run the recipe integration tests for list, show, and nested details:

```bash
bin/rails test test/integration/recipes_integration_test.rb -i "/test_visits_the_list|test_shows_a_recipe|test_shows_ingredients_and_steps_on_the_recipe_page/"
```

You want failures. Guests still hit the global `require_authentication` on index and show, so the response is a redirect to sign-in instead of `:success`.

```bash
FFF

Failure:
RecipesIntegrationTest#test_visits_the_list [test/integration/recipes_integration_test.rb:7]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/session/new?utm_source=minitestrails.com>
Response body: 

Failure:
RecipesIntegrationTest#test_shows_a_recipe [test/integration/recipes_integration_test.rb:16]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/session/new?utm_source=minitestrails.com>
Response body: 

Failure:
RecipesIntegrationTest#test_shows_ingredients_and_steps_on_the_recipe_page [test/integration/recipes_integration_test.rb:24]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/session/new?utm_source=minitestrails.com>
Response body: 
```

That is your red step for guest browsing the recipe list and show pages.

#### Green: allow guest browse and hide buttons to modify a recipe

Update the `RecipesController` to allow guests to browse the recipe list and show pages. Add the `allow_unauthenticated_access` helper to the controller:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_recipe, only: %i[show edit update destroy]

  # ... existing actions unchanged
end
```

`allow_unauthenticated_access only: %i[index show]` skips `require_authentication` for browsing. Every other action still needs a session. The helper lives in the `Authentication` concern and has the following code:

```ruby
# app/controllers/concerns/authentication.rb
def allow_unauthenticated_access(**options)
  skip_before_action :require_authentication, **options
end
```

Next, you need to hide the buttons to modify a recipe for guests: New recipe button in the index view and Edit and Destroy buttons in the show view, plus ingredient and step Remove buttons in the show view.

Update the index view at `app/views/recipes/index.html.erb` and wrap the New recipe link in an `if` block to only show it to signed in users:

```erb
<%%# app/views/recipes/index.html.erb %>
<%% if authenticated? %>
  <%%= link_to "New recipe", new_recipe_path %>
<%% end %>
```

Update the recipe partial at `app/views/recipes/_recipe.html.erb` and wrap the Destroy button in an `if` block to only show it to signed in users:

```erb
<%%# app/views/recipes/_recipe.html.erb %>
  <%% if authenticated? %>
    <%%= button_to "Destroy this recipe", destroy_url, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
  <%% end %>
```

Update the show view at `app/views/recipes/show.html.erb` and wrap the Edit this recipe link, Remove buttons for each ingredient and step in an `if` block to only show them to signed in users. Replace the content of the file with the following:

```erb
<%%# app/views/recipes/show.html.erb (bottom links) %>
<p style="color: green"><%%= notice %></p>

<%%= render @recipe, for_show: true %>

<%% if @recipe.ingredients.any? %>
  <h2>Ingredients</h2>
  <ul id="ingredients">
    <%% @recipe.ingredients.each do |ingredient| %>
      <li id="<%%= dom_id(ingredient) %>">
        <%%= ingredient.name %><%% if ingredient.quantity.present? %> (<%%= number_with_precision(ingredient.quantity, precision: 2, strip_insignificant_zeros: true) %><%% if ingredient.unit.present? %> <%%= ingredient.unit %><%% end %>)<%% end %>
        <%% if authenticated? %>
          <%%= button_to "Remove",
                recipe_ingredient_path(@recipe, ingredient),
                method: :delete,
                data: { turbo_confirm: "Remove this ingredient?" } %>
        <%% end %>
      </li>
    <%% end %>
  </ul>
<%% end %>

<%% if @recipe.steps.any? %>
  <h2>Steps</h2>
  <ol id="steps">
    <%% @recipe.steps.order(:position).each do |step| %>
      <li id="<%%= dom_id(step) %>">
        <%%= step.instruction %>
        <%% if authenticated? %>
          <%%= button_to "Remove",
                recipe_step_path(@recipe, step),
                method: :delete,
                data: { turbo_confirm: "Remove this step?" } %>
        <%% end %>
      </li>
    <%% end %>
  </ol>
<%% end %>

<div>
  <%% if authenticated? %>
    <%%= link_to "Edit this recipe", edit_recipe_path(@recipe) %>
  <%% end %>

  <%%= link_to "Back to recipes", recipes_path %>
</div>
```

`authenticated?` hides New, Destroy, Edit, and Remove from guests while signed-in users still see them.

Run list, show, and nested-detail tests again, you want 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipes_integration_test.rb -i "/test_visits_the_list|test_shows_a_recipe|test_shows_ingredients_and_steps_on_the_recipe_page/"
```

#### Prove signed-in users still see change buttons

Guest list and show prove the controls are gone when signed out. You still need the other side: a signed-in user must see them. Update the `recipe_access_integration_test.rb` file with the following scenarios:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Alice, signed in
  # Starting point: session from sign_in_as
  # Action: open recipes list
  # Expected outcome: New recipe and Destroy this recipe present
  # test "signed-in user sees new recipe on the list" do
  # end

  # Actor: Alice, signed in
  # Starting point: session from sign_in_as
  # Action: open pancakes show
  # Expected outcome: Edit, Destroy, and Remove present
  # test "signed-in user sees edit, destroy, and remove on show" do
  # end
```

Replace the test bodies with the following test cases:

```ruby
# test/integration/recipe_access_integration_test.rb
test "signed-in user sees new recipe on the list" do
  sign_in_as users(:alice)

  get recipes_url
  assert_response :success
  assert_select "a", text: "New recipe"
  assert_select "button", text: "Destroy this recipe"
end

test "signed-in user sees edit, destroy, and remove on show" do
  sign_in_as users(:alice)

  get recipe_url(recipes(:pancakes))
  assert_response :success
  assert_select "a", text: "Edit this recipe"
  assert_select "button", text: "Destroy this recipe"
  assert_select "button", text: "Remove"
end
```

This is what's happening in the code above:

- `sign_in_as users(:alice)` establishes a session before the GET.
- These tests mirror the guest `count: 0` asserts in `recipes_integration_test.rb`. Guests must not see the controls. Signed-in users must.

Run the full access test file, you want 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

<%= render Guide::LoadDevelopmentFixtures.new(
  after_load: "Signed out, confirm New and Destroy are gone on the list, and Edit / Destroy / Remove are gone on Fluffy pancakes. Sign in as Alice with password `password`, then confirm those controls return."
) %>

Next, quickly surf in the browser to confirm the changes:

1. Start `bin/dev` if the server is not running.
2. Visit `http://localhost:3000/recipes` while signed out. Confirm recipe titles appear and buttons for New recipe and Destroy this recipe are gone.
3. Open Fluffy pancakes. Confirm the title appears and Edit this recipe, Destroy this recipe, and Remove are gone.
4. Sign in as Alice with email `alice@example.com` and password `password`. Confirm New and Destroy return on the list, and Edit, Destroy, and Remove return on the recipe show page.

### System tests

You do not need a system test for guest restrictions or signed-in button visibility. The integration tests above already cover both sides.

<%= render Guide::SupportCta.new(
  variant: :mid_chapter,
  site_metadata: site.data.site_metadata,
  milestone_hook: "Guest read-only HTTP and UI now match: redirects, unchanged counts and hidden buttons for guests.",
  headline: "Guests browse, recipe modification buttons are shown for signed in users, and the tests prove both.",
  body_text: "You locked guest create, edit, and destroy, hid buttons for guests, and proved signed in users still see New, Edit, Destroy, and Remove. If that habit is sticking, fund the next chapter and help keep the guide free."
) %>

## Signed-in users can still change recipes

Guests are locked out of create, update, and destroy while signed-in users still see New, Edit, Destroy, and Remove. But your old tests for create, update, and destroy still assume a guest can change recipes, so they fail until you add a session. Fix that next.

### Integration tests

#### Red: tests for create, update, and destroy recipe fail without a session

Run existing integration tests for create, update, and destroy recipe to see failures due to missing session.

```bash
bin/rails test test/integration/recipes_integration_test.rb -i "/test_creates_a_recipe|test_updates_a_recipe|test_destroys_a_recipe/"
```

Without a session, create hits `require_authentication` on `get new_recipe_url` and redirects to sign-in instead of `:success`. You will see failures like this for each test:

```bash
FFFF

Failure:
RecipesIntegrationTest#test_creates_a_recipe [test/integration/recipes_integration_test.rb:28]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/session/new?utm_source=minitestrails.com>
Response body: 
```

#### Green: sign in before create, update, and destroy

Add `sign_in_as users(:alice)` at the start of every test for create, update, and destroy recipe.

```ruby
# test/integration/recipes_integration_test.rb
test "creates a recipe" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end

test "creates a recipe with an ingredient and a step" do
  sign_in_as users(:alice)

  get new_recipe_url
  assert_response :success
  # ... rest unchanged from Chapter 9
end

test "does not create a recipe with invalid data" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end

test "does not create a recipe with invalid ingredient" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end

test "does not create a recipe with invalid step" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end

test "updates a recipe" do
  sign_in_as users(:alice)
  recipe = recipes(:pancakes)

  # ... rest of the test remains unchanged
end

test "adds an ingredient and a step when editing a recipe" do
  sign_in_as users(:alice)
  recipe = recipes(:pancakes)

  # ... rest of the test remains unchanged
end

test "destroys a recipe" do
  sign_in_as users(:alice)
  recipe = recipes(:lentil_soup)

  # ... rest of the test remains unchanged
end

test "removes existing ingredient and step when updating a recipe" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end

test "removes ingredients and steps from the detail page" do
  sign_in_as users(:alice)

  # ... rest of the test remains unchanged
end
```

This is what's happening in the code above:

- `sign_in_as users(:alice)` runs first so the session cookie is set before `get`, `post`, `patch`, or `delete`.
- Invalid-data and nested tests follow the same pattern: sign in, then the same params you already used in earlier chapters.
- List, show, and filter tests stay unchanged since they are not affected by the authentication changes and work without user signing in.

Run the full integration file. You want 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

### System tests

Same as integration tests, system tests for create, update, and destroy recipe fail without a session. List and show tests stay unchanged since they are not affected by the authentication changes and work without user signing in.

#### Red: system tests for create, update, and destroy recipe fail without sign-in

Run the system tests for create, update, and destroy recipe to see failures due to missing session.

```bash
bin/rails test test/system/recipes_test.rb -i "/test_creates_a_recipe|test_updates_a_recipe|test_destroys_a_recipe/"
```

You want failures. The browser lands on the sign-in page, so Capybara cannot find the Title field or the Destroy this recipe button.

```bash
EEEE

Error:
RecipesTest#test_creates_a_recipe:
Capybara::ElementNotFound: Unable to find field "Title" that is not disabled

Error:
RecipesTest#test_destroys_a_recipe:
Capybara::ElementNotFound: Unable to find link or button "Destroy this recipe"
```

#### Green: sign in before create, update, and destroy recipe

Add `sign_in_to_ui_as users(:alice)` as the first line of each test for create, update, and destroy recipe while keeping other lines unchanged.

```ruby
# test/system/recipes_test.rb
test "creates a recipe" do
  sign_in_to_ui_as users(:alice)

  # ... rest of the test remains unchanged
end

test "updates a recipe" do
  sign_in_to_ui_as users(:alice)
  
  # ... rest of the test remains unchanged
end

test "destroys a recipe" do
  sign_in_to_ui_as users(:alice)

  # ... rest of the test remains unchanged
end

test "destroys a recipe from the list" do
  sign_in_to_ui_as users(:alice)

  # ... rest of the test remains unchanged
end

test "removes ingredients and steps from the detail page" do
  sign_in_to_ui_as users(:alice)

  # ... rest of the test remains unchanged
end
```

This is what's happening in the code above:

- Create, update, destroy and remove ingredients and steps call `sign_in_to_ui_as users(:alice)` first so Capybara signs in the user before performing the action.

Run the full system test. You want 0 failures and 0 errors.

```bash
bin/rails test test/system/recipes_test.rb
```

## Reset a forgotten password

The authentication generator also ships password reset: request a reset email, open the token link, set a new password. Skim the generated controller before writing tests:

```ruby
# app/controllers/passwords_controller.rb
class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end
```

Here is what's happening in the code above:

- `allow_unauthenticated_access` allows the controller to be accessed without a session.
- `before_action :set_user_by_token, only: %i[ edit update ]` sets the user by the password reset token.
- `rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }` limits the number of password reset requests to 10 tries per 3 minutes.
- `def new` renders the new password reset form.
- `def create` finds the user by email address and sends a password reset email if the user exists.
- `def edit` renders the edit password form.
- `def update` updates the user's password and redirects to the sign-in page.

### Controller tests

The generator also adds `test/controllers/passwords_controller_test.rb` similar to how it does for sessions. You can skim it if you want but we will ignore it for now as we don't prefer writing integration tests for authentication over controller tests.

You can keep the file, or delete it if you prefer one home for password-reset HTTP in `test/integration/`. The integration tests below are the ones you will maintain as Cookbook coverage.

### Integration tests

`PasswordsController#create` always redirects to sign-in with the same notice and only enqueues mail when the email matches a user. `PasswordsController#update` uses `password_reset_token` in the URL, clears all sessions on success, and redirects back to edit when confirmation does not match.

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Known email | POST reset request enqueues `PasswordsMailer#reset` and redirects to sign-in |
| Unknown email | Same redirect and notice shape; no email enqueued (do not leak whether the account exists) |
| Valid token update | Matching password + confirmation change `password_digest` and redirect to sign-in |
| Mismatched password confirmation | Digest unchanged; redirect back to the edit form |

#### Add scenarios to the test file

Create a new test file with `nano test/integration/password_integration_test.rb` and add the following scenarios:

```ruby
# test/integration/password_integration_test.rb
  # Actor: guest with an account
  # Action: POST passwords with Alice's email
  # Expected outcome: PasswordsMailer#reset enqueued; redirect to sign-in
  # test "requests a password reset for a known email" do
  # end

  # Actor: guest
  # Action: POST passwords with an unknown email
  # Expected outcome: no email; redirect to sign-in
  # test "does not send a password reset email for an unknown email" do
  # end

  # Actor: guest with a valid reset token
  # Action: PUT new matching passwords
  # Expected outcome: password_digest changes; redirect to sign-in
  # test "updates the password with a valid token" do
  # end

  # Actor: guest with a valid reset token
  # Action: PUT mismatched password confirmation
  # Expected outcome: digest unchanged; redirect to edit password
  # test "rejects a mismatched password confirmation" do
  # end
```

#### Add the integration tests

Replace the test bodies with the following test cases:

```ruby
# test/integration/password_integration_test.rb
require "test_helper"

class PasswordIntegrationTest < ActionDispatch::IntegrationTest
  test "requests a password reset for a known email" do
    assert_enqueued_email_with PasswordsMailer, :reset, args: [users(:alice)] do
      post passwords_url, params: { email_address: users(:alice).email_address }
    end
    assert_redirected_to new_session_url
  end

  test "does not send a password reset email for an unknown email" do
    assert_no_enqueued_emails do
      post passwords_url, params: { email_address: "missing@example.com" }
    end
    assert_redirected_to new_session_url
  end

  test "updates the password with a valid token" do
    user = users(:alice)
    token = user.password_reset_token

    assert_changes -> { user.reload.password_digest } do
      put password_url(token), params: {
        password: "new-password",
        password_confirmation: "new-password"
      }
    end
    assert_redirected_to new_session_url
  end

  test "rejects a mismatched password confirmation" do
    user = users(:alice)
    token = user.password_reset_token

    assert_no_changes -> { user.reload.password_digest } do
      put password_url(token), params: {
        password: "new-password",
        password_confirmation: "different"
      }
    end
    assert_redirected_to edit_password_url(token)
  end
end
```

<%= render Shared::Tip.new(
  title: "New assertions",
  markdown: <<~MD
    1. **`assert_enqueued_email_with PasswordsMailer, :reset, args: [users(:alice)]`**
      Asks "did this block enqueue the reset mail for Alice?"
      The generator delivers with `deliver_later`, so you assert on the job queue. You don't need to test email content in this file, we will cover all mailer tests later in [Chapter 14](/guide/testing-mailers/).

    2. **`assert_no_enqueued_emails`**
      Asks "did this block enqueue zero emails?"
      Use it for the unknown-address path so a missing account does not send mail.

    3. **`assert_changes -> { user.reload.password_digest }`**
      Asks "did this value change inside the block?"
      Syntax: `assert_changes` plus a lambda that reads the value after reload. Pair with `assert_no_changes` when a bad confirmation must leave the digest alone.
  MD
) %>

This is what's happening in the code above:

- Known and unknown emails share the same redirect with only a difference in enqueue side. This matches the generator's privacy-minded notice.
- `user.password_reset_token` builds the signed token the edit/update URLs expect.
- A successful update changes the password digest. A mismatched confirmation leaves it alone.

Run the test:

```bash
bin/rails test test/integration/password_integration_test.rb
```

You want 0 failures and 0 errors. The generator already implemented the controller, so this suite should be green once the tests match the redirects and mail enqueue behavior above.

### System tests

One browser smoke covers the path a human uses: Forgot password from the sign-in page, set a new password from the token URL, then sign in with the new password.

#### What counts as working?

| Flow | Expected |
| --- | --- |
| Request reset | From sign-in, open Forgot password?, submit Alice's email, see the sent notice |
| Set new password | Open edit URL with a fresh token, save matching passwords, see reset notice |
| Sign in with new password | Alice signs in with `new-password` and sees Sign out |

#### Add scenarios to the test file

Create a new test file with `nano test/system/password_test.rb` and add the following scenario:

```ruby
# test/system/password_test.rb
require "application_system_test_case"

class PasswordTest < ApplicationSystemTestCase
  # Actor: guest with Alice's account
  # Starting point: sign-in page
  # Action: Forgot password?, submit Alice's email, open edit URL with a fresh token, save matching passwords, sign in with new-password
  # Expected outcome: reset-instructions notice, password-reset notice, Recipes page with Sign out
  # test "resets password" do
  # end
end
```

#### Add the system test

Replace the test body for "resets password" with the following test cases:

```ruby
# test/system/password_test.rb
require "application_system_test_case"

class PasswordTest < ApplicationSystemTestCase
  test "resets password" do
    user = users(:alice)

    visit new_session_url
    click_on "Forgot password?"
    fill_in "Enter your email address", with: user.email_address
    click_on "Email reset instructions"
    assert_text /reset instructions sent/i

    visit edit_password_url(user.password_reset_token)
    fill_in "Enter new password", with: "new-password"
    fill_in "Repeat new password", with: "new-password"
    click_on "Save"
    assert_text /Password has been reset/i

    fill_in "Enter your email address", with: user.email_address
    fill_in "Enter your password", with: "new-password"
    click_on "Sign in"
    assert_selector "h1", text: "Recipes"
    assert_text "Sign out"
  end
end
```

This is what's happening in the code above:

- Test fills in the detail and clicks through buttons in the password reset flow (`Enter your email address`, `Enter new password`, `Repeat new password`).
- You use `visit edit_password_url(user.password_reset_token)` to visit the edit password page with a fresh token instead of clicking the mailer link. The integration tests already proved enqueue behavior of mailer for the known email so you don't need to test that again.

Run the test, you want 0 failures and 0 errors.

```bash
bin/rails test test/system/password_test.rb
```

## Delete the scaffold Recipes controller test

[Chapter 5](/guide/your-first-test/#http-tests-controller-and-integration) left `test/controllers/recipes_controller_test.rb` in place after you ran it once. I had left it alone thinking it wouldn't pose an issue in future but now with the introduction of Authentication, it breaks that file. Write actions still call `get` / `post` / `patch` / `delete` with no session, so they redirect to sign-in instead of changing recipes.

Run the controller test once:

```bash
bin/rails test test/controllers/recipes_controller_test.rb
```

You should see failures like this:

```bash
Failure:
RecipesControllerTest#test_should_destroy_recipe [test/controllers/recipes_controller_test.rb:42]:
`Recipe.count` didn't change by -1, but by 0.
```

You are not going to grow that scaffold file. Guest denials, signed-in writes, and HTML outcomes already live in `test/integration/recipes_integration_test.rb` and `test/integration/recipe_access_integration_test.rb`. Delete the drifted controller test instead of fixing the tests by sprinkling `sign_in_as` into generator-shaped tests:

```bash
rm test/controllers/recipes_controller_test.rb
```

Deleting the authentication generator's own `sessions_controller_test.rb` and `passwords_controller_test.rb` stays optional if you still have them.

## Commit your work

Run the full suite:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors across model, integration, and system tests. Then commit changes:

```bash
git add .
git commit -m "Add authentication, guest access, and password reset tests"
```

Small commits make it easier to roll back, bisect a regression, or pick up on another machine.

## Want registration tests too?

Rails `generate authentication` does not ship a sign-up form. This chapter stays on sessions and guest read-only access. When you add registration yourself and want Minitest coverage for it, see [How to Test User Registration with Minitest Rails](/blog/testing-user-registration-rails-minitest/).

## What is next

In this chapter any signed-in user can edit or destroy a recipe. Chapter 13 introduces authorization concepts and locks edit/destroy to the owner while signed-in users can still create and guests can still read recipes.

Continue to **[Authorization testing](/guide/authorization-testing/)**.
