Pest-Driven Development: Testing Routes For Authenticated Users in Laravel Jetstream
Tags: laravel, pest, testing, opensource
Published: 2023-09-18 17:15:45

_I'm blogging along with the laracasts series [Pest Driven Laravel](https://laracasts.com/series/pest-driven-laravel) created by [Christoph Rumpel](https://twitter.com/christophrumpel) as I work on a project I hope to open source. I'm just documenting how I'm working, the resources I'm using, the problems I run into and some cool things I'm reminded of as I develop._

I'm moving forward with a new understanding of my goal, and already I can tell that my goalpost has shifted. This past week involved a lot of failing. I'm excited about that because while I do enjoy all the resources I wrote about in [my last blog post](https://dev.to/sifrious/pest-driven-development-learning-iterating-and-project-planning-50e0), I always learn the most by breaking things in new ways and paying a lot of attention while I fix it. It's easier to pay attention when I'm trying to fix something. This year's Laracon had an excellent talk by Joel  Clermont about [getting unstuck](https://www.youtube.com/watch?v=23McCP-_9BE) which had a particularly tricky debugging situation as its final example. No spoilers, but I knew exactly what the problem was because I've done it in Python. 

I also moved to a new blog writing method now that I've put out all the fires I can find so everything's going to be in past tense this time.

I made a new test file just for `task.show`. This is the named route we're going to be talking about today. It displays a navigation bar and the details of one task that's already been created.

```php
<?php

use App\Models\User;
use App\Models\Task;

use function Pest\Laravel\{get};

it('gives back successful redirect response for unauthenticated users for the tasks.show page', function () {
    $task = Task::factory()->create(['task_description' => 'Test Task 1', 'sheets_id' => 1, 'public' => false]);
    get(route('tasks.show', ['user' => $user, 'task' => $task]))->assertStatus(302);
});
```

The test ran and it passed.

## Failing With A Default `Factory`

The next step was to actually test the routes with a logged-in user. Building on the principles I laid out for myself, I decided to test the smallest thing I can: the `show` method for a single task. So I wrote this based on the progress I've made in the course so far:

```php
it('can be accessed by verified user', function () {
    // Arrange
    $user = User::factory()->create();
    $task = Task::factory()->create();

    // Act & Assert
    $this->actingAs($user)
        ->get(route('tasks.show'))
        ->assertOk();
});
```

It failed. Normally that's step 1, but in this case I already had a task view written and I was expecting it to pass. This test has some new stuff going on:

- **`$user = User::factory()->create();`**: Here, we return a facade which uses a factory to create and store a new instance of `User` in the testing database. These records are created during the test instantiation and destroyed when the test is complete, so they'll look different every time pest is run. Pest manages the creation and deletion of these records for you. It relies on [php faker](https://fakerphp.github.io/) to seed random data so you don't have to write seeders. There's also a [python faker](https://faker.readthedocs.io/en/master/) for my fellow polygots who may or may not have spent a bunch of hours writing their own seeder only to be told Laravel already has a good one. Turns out Python has a good one too. Oops.
- **`$this->actingAs($user)`...**: I still cannot tell you what `$this` is with confidence (is it the testing context??), but I can tell you that `actingAs($user)` is a chained method which accepts our temporary `$user` as a parameter and allows us to make requests to a route as that fake user. This comes from [Laravel Passport](https://laravel.com/docs/10.x/passport#testing), the dependency jetstream uses to spin up an OAuth2 server implementation for us. I've been working through [a book about OAuth2](https://learning.oreilly.com/library/view/oauth-2-in/9781617293276/) for almost 3 years. It is so refreshing to bump into OAuth2 while searching docs as opposed to bending to its will.
- `$this->actingAs($user)`**`->get(route(tasks.show))`...**: Now we've come to same old version of our previous get request. This time it's being called with more contextual information stored in `$this`.

The error comes from eloquent, the built-in Laravel dbms. Our `create()` method tried to commit a `Task` to the database without required attributes. It's calling out the `sheets_id` attribute because it's listed first, but there's going to be a few more problems we can fix by writing a `[Factory]()` right now.

## Task Factory

When I created my `task` model, I specified I wanted a factory to be automatically created for me. It's already there in `{{root}}\database\factories\TaskFactory`.

I also already have a working `user` factory provided by Laravel. It's in `/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php`. More general code for factories is also already in your app at `/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php`.

In general, we can use [`faker` generators](https://fakerphp.github.io/#create-fake-data); you can find the generator class in your `./vendor/fakerphp/faker/src/Faker/generator.php` directory if you want to see a list real quick. In this case, a few attributes I'm going to keep consistent just so I'm seeing consistent data across a test if I need to see it with `dump()` later.

Here's the factory I ended up with:

```php
class TaskFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // get a date sometime in the next two months
        $currentDateTime = Carbon::now();
        $dateTime = $currentDateTime->addDays(rand(2, 60))->toDateString();
        $currentDateTime = $currentDateTime->toDateString();
        $name = $this->faker->firstName();
        $sheetsId = $name . $currentDateTime;
        return [
            'sheets_created_at' => "",
            'sheets_id' => $sheetsId,
            'name' => $this->faker->randomDigitNotNull(),
            'author' => $this->faker->firstName(),
            'start_date' => $dateTime,
            'start_time' => $dateTime,
            'public' => rand(0, 1) == 1,
            'client_address' => $this->faker->streetAddress(),
            'task_description' => $this->faker->text(),
            'destination' => $this->faker->streetAddress(),
            'volunteer' => "",
            'status' => "unassigned",
            'contact_information' => $this->faker->text(),
        ];
    }
}
```

The other line to notice here is `$currentDateTime = Carbon::now()` and `$dateTime = $currentDateTime->addDays(rand(2,60))->toDateString();`. I wanted to create a `start_date` and `start_time` between now and 2 months for now, because that's about the amount of time I'm seeing most tasks scheduled. If that was all I wanted I could use `$dateTime = Carbon::now()->addDays(rand(2,60))->toDateString();`, but I also want to see a `sheets_created_at` with a really similar timestamp to the `created_at` in my database timestamps.

I'm using carbon to get my epic `$currentDateTime` timestamp ([pun intended](https://www.epoch101.com/PHP) this [time](http://www.quickmeme.com/img/f5/f55393d22cd9449ff1fed73933768ce840796a9c8528e816963516960db93bb5.jpg)). I know of Carbon from [Laravel Up And Running](https://www.amazon.com/Laravel-Running-Framework-Building-Modern/dp/109815326X), so I used these handy digitalocean tutorials on [Getting an Easy Datetime in Laravel](https://www.digitalocean.com/community/tutorials/easier-datetime-in-laravel-and-php-with-carbon) and [Manipulating The Date And Time](https://www.digitalocean.com/community/tutorials/easier-datetime-in-laravel-and-php-with-carbon#manipulating-the-date-and-time) to copy and paste. Then I googled again to [assign it to my attributes as a string](https://stackoverflow.com/questions/50692147/how-to-convert-a-carbon-into-string-to-take-the-date-only). I'm going to format this output later but right now I have my test set up and nothing is `null` that shouldn't be `nullable`.

So far I:

1. wrote a failing test to hit the route as authenticated
2. wrote a blade template
3. added my route to `web.php`
4. told `TaskController.php` what to do when a request hits the route
5. modified my test to include `$actingAs` before the `get` and include `User` and `Task` factories through facades
6. wrote a `Task` factory and used `dump()` to check that my creates a complete, random `$task` I can pass to my `route`

That's everything the course teaches about showing detail, I think. It should pass now, right? Right?

## Failing Without a User Group

No, it didn't pass. Now I'm getting an error from my navigation bar template in blade that wants the name and id of the organization associated with my user. I have a fake user, a fake task, and no fake organization. Not going to lie, it took me a while to realize what this error meant. There's not much we can do in jetstream without running into this issue because the navigation bar is on every page and users really should be associated with teams at all times.

This is a problem I signed up for. I'm coding along with the course instead of going ahead through it because I want to make all of these mistakes now. I'm VERY tempted to reach out and ask for help (or thank people for being awesome and making course content), but I can be pretty thick-headed and I know that if I don't understand why I'm doing these things and fix them as I break them I'll learn them for ever ever. If I don't, I'll lose time (potentially for years) forgetting things I don't rememeber learning. That's why I'm not a copy and paste from stack overflow person and never will be. Unless it's a super elegant one-liner like `public`: [rand(0, 1) == 1](https://stackoverflow.com/questions/19222716/get-random-boolean-true-false-in-php) which I'm sure I understand fully before I use it. Should I compromise? Yes. Would I compromise if I was being compensated for my time? Also yes. Moving on.

In order to display my template, I need to create a user organization, but it can't just be any user organization. They have to have a relationship or the test will still fail. My `$user` is going to have a [belongs to relationship](https://laravel.com/docs/10.x/eloquent-factories#belongs-to-relationships) to the `personal_team` provided by jetstream. I will sometimes refer to this `personal_team` it a team because that is what jetstream named it. I also sometimes call it an organization, because that is what it represents in my problem domain. There's this [great solution on laracasts.com](https://laracasts.com/discuss/channels/laravel/user-factory-with-jetstream-team?page=1&replyId=660679), which helped me find the [Laravel Documentation addressing the problem](https://laravel.com/docs/10.x/http-tests#session-and-authentication).

Now my test looks like this:

```php
it('returns 200 on GET request for authenticated user @ tasks.show', function () {
    // Arrange
    $user = User::factory()
        ->hasAttached(
            Team::factory()
                ->state(function (array $attributes, User $user) {
                    return ['user_id' => $user->id, 'personal_team' => true];
                }),
        )
        ->create();
    $task = Task::factory()->create();

    // Act & Assert
    $this->actingAs($user)
        ->get(route('tasks.show', ['task' => $task, 'user' => $user]))
        ->assertOk();
});
```

And it passes. There's NOTHING like a passing test case.

![One Passing Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ynrs4s9m1d24rdhmf5ac.png)

## Conclusion
After getting `tasks.show` to pass for authenticated users, I added new files for each view that already existed in my previous `SuccessfulGetResponse2.php` except `/home`. Then I used variations of the method described above to produce this output:

![Suite of Passing Tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jlnhspz2m2mdzd3eq7k.png)