Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Traits/ChecksStoreRole.php
@@
     protected function currentStoreId(): ?int
     {
         if (! app()->bound('current_store')) {
             return null;
         }
@@

         return $store instanceof Store ? $store->getKey() : null;
     }
+
+    protected function storeIdForModel(object $model): ?int
+    {
+        if (method_exists($model, 'getAttribute')) {
+            $storeId = $model->getAttribute('store_id');
+
+            return is_numeric($storeId) ? (int) $storeId : null;
+        }
+
+        if (property_exists($model, 'store_id')) {
+            return is_numeric($model->store_id) ? (int) $model->store_id : null;
+        }
+
+        return null;
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/ProductPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class ProductPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $product): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($product));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $product): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($product));
+    }
+
+    public function delete(User $user, object $product): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($product));
+    }
+
+    public function archive(User $user, object $product): bool
+    {
+        return $this->delete($user, $product);
+    }
+
+    public function restore(User $user, object $product): bool
     {
-        //
+        return $this->delete($user, $product);
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/OrderPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class OrderPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $order): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($order));
+    }
+
+    public function update(User $user, object $order): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($order));
+    }
+
+    public function cancel(User $user, object $order): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($order));
+    }
+
+    public function createFulfillment(User $user, object $order): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($order));
+    }
+
+    public function createRefund(User $user, object $order): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($order));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/CollectionPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class CollectionPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $collection): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($collection));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $collection): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($collection));
+    }
+
+    public function delete(User $user, object $collection): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($collection));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/DiscountPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class DiscountPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $discount): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($discount));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $discount): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($discount));
+    }
+
+    public function delete(User $user, object $discount): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($discount));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/CustomerPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class CustomerPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $customer): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($customer));
+    }
+
+    public function update(User $user, object $customer): bool
     {
-        //
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($customer));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/StorePolicy.php
@@
 namespace App\Policies;

+use App\Models\Store;
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class StorePolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function view(User $user, Store $store): bool
+    {
+        return $this->isAnyRole($user, $store->getKey());
+    }
+
+    public function update(User $user, Store $store): bool
+    {
+        return $this->isOwnerOrAdmin($user, $store->getKey());
+    }
+
+    public function delete(User $user, Store $store): bool
     {
-        //
+        return $this->isOwnerOnly($user, $store->getKey());
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/PagePolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class PagePolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function view(User $user, object $page): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($page));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $page): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($page));
+    }
+
+    public function delete(User $user, object $page): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($page));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/ThemePolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class ThemePolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function view(User $user, object $theme): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function update(User $user, object $theme): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
+    }
+
+    public function publish(User $user, object $theme): bool
+    {
+        return $this->update($user, $theme);
+    }
+
+    public function delete(User $user, object $theme): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/FulfillmentPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class FulfillmentPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $fulfillment): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($fulfillment));
+    }
+
+    public function cancel(User $user, object $fulfillment): bool
     {
-        //
+        return $this->update($user, $fulfillment);
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Policies/RefundPolicy.php
@@
 namespace App\Policies;

 use App\Models\User;
+use App\Traits\ChecksStoreRole;

 class RefundPolicy
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function view(User $user, object $refund): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($refund));
     }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Traits/ChecksStoreRole.php
M /Users/fabianwesner/Herd/shop/app/Policies/ProductPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/OrderPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/CollectionPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/DiscountPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/CustomerPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/StorePolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/PagePolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/ThemePolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/FulfillmentPolicy.php
M /Users/fabianwesner/Herd/shop/app/Policies/RefundPolicy.php

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,27 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $discount): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($discount));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $discount): bool
     {
-        //
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($discount));
+    }
+
+    public function delete(User $user, object $discount): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($discount));
     }

@@ -61,2 +61,17 @@
     }
+
+    protected function storeIdForModel(object $model): ?int
+    {
+        if (method_exists($model, 'getAttribute')) {
+            $storeId = $model->getAttribute('store_id');
+
+            return is_numeric($storeId) ? (int) $storeId : null;
+        }
+
+        if (property_exists($model, 'store_id')) {
+            return is_numeric($model->store_id) ? (int) $model->store_id : null;
+        }
+
+        return null;
+    }
 }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,17 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $fulfillment): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($fulfillment));
+    }
+
+    public function cancel(User $user, object $fulfillment): bool
     {
-        //
+        return $this->update($user, $fulfillment);
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,37 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $product): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($product));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $product): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($product));
+    }
+
+    public function delete(User $user, object $product): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($product));
+    }
+
+    public function archive(User $user, object $product): bool
+    {
+        return $this->delete($user, $product);
+    }
+
+    public function restore(User $user, object $product): bool
     {
-        //
+        return $this->delete($user, $product);
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,17 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $customer): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($customer));
+    }
+
+    public function update(User $user, object $customer): bool
     {
-        //
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($customer));
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,27 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function view(User $user, object $page): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($page));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $page): bool
     {
-        //
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($page));
+    }
+
+    public function delete(User $user, object $page): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($page));
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,12 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function create(User $user): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function view(User $user, object $refund): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($refund));
     }

@@ -4,3 +4,5 @@

+use App\Models\Store;
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +10,17 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function view(User $user, Store $store): bool
     {
-        //
+        return $this->isAnyRole($user, $store->getKey());
+    }
+
+    public function update(User $user, Store $store): bool
+    {
+        return $this->isOwnerOrAdmin($user, $store->getKey());
+    }
+
+    public function delete(User $user, Store $store): bool
+    {
+        return $this->isOwnerOnly($user, $store->getKey());
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,32 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $order): bool
     {
-        //
+        return $this->isAnyRole($user, $this->storeIdForModel($order));
+    }
+
+    public function update(User $user, object $order): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($order));
+    }
+
+    public function cancel(User $user, object $order): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($order));
+    }
+
+    public function createFulfillment(User $user, object $order): bool
+    {
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($order));
+    }
+
+    public function createRefund(User $user, object $order): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($order));
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,32 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function view(User $user, object $theme): bool
     {
-        //
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerOrAdmin($user);
+    }
+
+    public function update(User $user, object $theme): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
+    }
+
+    public function publish(User $user, object $theme): bool
+    {
+        return $this->update($user, $theme);
+    }
+
+    public function delete(User $user, object $theme): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
     }

@@ -5,2 +5,3 @@
 use App\Models\User;
+use App\Traits\ChecksStoreRole;

@@ -8,8 +9,27 @@
 {
-    /**
-     * Create a new policy instance.
-     */
-    public function __construct()
+    use ChecksStoreRole;
+
+    public function viewAny(User $user): bool
+    {
+        return $this->isAnyRole($user);
+    }
+
+    public function view(User $user, object $collection): bool
+    {
+        return $this->isAnyRole($user, $this->storeIdForModel($collection));
+    }
+
+    public function create(User $user): bool
+    {
+        return $this->isOwnerAdminOrStaff($user);
+    }
+
+    public function update(User $user, object $collection): bool
     {
-        //
+        return $this->isOwnerAdminOrStaff($user, $this->storeIdForModel($collection));
+    }
+
+    public function delete(User $user, object $collection): bool
+    {
+        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($collection));
     }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Traits/ChecksStoreRole.php\nM /Users/fabianwesner/Herd/shop/app/Policies/ProductPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/OrderPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/CollectionPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/DiscountPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/CustomerPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/StorePolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/PagePolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/ThemePolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/FulfillmentPolicy.php\nM /Users/fabianwesner/Herd/shop/app/Policies/RefundPolicy.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


The foundation needs evidence, so I’m adding focused Pest feature tests before running the slice. These will cover fresh schema/seed expectations, hostname/session store resolution, store-scoped customer auth, and the owner/staff/support role matrix pieces that are now implemented.


php artisan make:test --pest Foundation/TenancyTest --no-interaction && php artisan make:test --pest Foundation/CustomerAuthTest --no-interaction && php artisan make:test --pest Foundation/AuthorizationTest --no-interaction


Output:
Chunk ID: 6ac0d2
Wall time: 0.4552 seconds
Process exited with code 0
Original token count: 65
Output:

   INFO  Test [tests/Feature/Foundation/TenancyTest.php] created successfully.

   INFO  Test [tests/Feature/Foundation/CustomerAuthTest.php] created successfully.

   INFO  Test [tests/Feature/Foundation/AuthorizationTest.php] created successfully.


php artisan make:test --pest Foundation/TenancyTest --no-interaction && php artisan make:test --pest Foundation/CustomerAuthTest --no-interaction && php artisan make:test --pest Foundation/AuthorizationTest --no-interaction

   INFO  Test [tests/Feature/Foundation/TenancyTest.php] created successfully.

   INFO  Test [tests/Feature/Foundation/CustomerAuthTest.php] created successfully.

   INFO  Test [tests/Feature/Foundation/AuthorizationTest.php] created successfully.


sed -n '1,220p' tests/Pest.php && sed -n '1,120p' tests/Feature/ExampleTest.php


sed -n '1,220p' tests/Pest.php && sed -n '1,120p' tests/Feature/ExampleTest.php

ExampleTest.php

<?php

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/

pest()->extend(Tests\TestCase::class)
 // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/

expect()->extend('toBeOne', function () {
    return $this->toBe(1);
});

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function something()
{
    // ..
}
<?php

it('returns a successful response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: f9a6c7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 429
Output:
<?php

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/

pest()->extend(Tests\TestCase::class)
 // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/

expect()->extend('toBeOne', function () {
    return $this->toBe(1);
});

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function something()
{
    // ..
}
<?php

it('returns a successful response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Store.php
@@
         return $this->belongsToMany(User::class, 'store_users')
             ->using(StoreUser::class)
-            ->withPivot('role')
-            ->withTimestamps();
+            ->withPivot('role', 'created_at');
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/User.php
@@
         return $this->belongsToMany(Store::class, 'store_users')
             ->using(StoreUser::class)
-            ->withPivot('role')
-            ->withTimestamps();
+            ->withPivot('role', 'created_at');
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Models/Store.php
M /Users/fabianwesner/Herd/shop/app/Models/User.php

@@ -53,4 +53,3 @@
             ->using(StoreUser::class)
-            ->withPivot('role')
-            ->withTimestamps();
+            ->withPivot('role', 'created_at');
     }

@@ -51,4 +51,3 @@
             ->using(StoreUser::class)
-            ->withPivot('role')
-            ->withTimestamps();
+            ->withPivot('role', 'created_at');
     }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Models/Store.php\nM /Users/fabianwesner/Herd/shop/app/Models/User.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,80p' tests/Feature/Foundation/TenancyTest.php && sed -n '1,80p' tests/Feature/Foundation/CustomerAuthTest.php && sed -n '1,80p' tests/Feature/Foundation/AuthorizationTest.php


sed -n '1,80p' tests/Feature/Foundation/TenancyTest.php && sed -n '1,80p' tests/Feature/Foundation/CustomerAuthTest.php && sed -n '1,80p' tests/Feature/Foundation/AuthorizationTest.php

AuthorizationTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: 3aac55
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 82
Output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/TenancyTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Store;
+use App\Models\StoreDomain;
+use App\Models\User;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Schema;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+test('phase one tables and seeded tenant records exist', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    expect(Schema::hasColumns('organizations', ['name', 'billing_email']))->toBeTrue()
+        ->and(Schema::hasColumns('stores', ['organization_id', 'handle', 'status', 'default_currency']))->toBeTrue()
+        ->and(Schema::hasColumns('store_domains', ['store_id', 'hostname', 'type', 'is_primary']))->toBeTrue()
+        ->and(Schema::hasColumns('store_users', ['store_id', 'user_id', 'role']))->toBeTrue()
+        ->and(Schema::hasColumns('store_settings', ['store_id', 'settings_json']))->toBeTrue()
+        ->and(StoreDomain::query()->where('hostname', 'shop.test')->exists())->toBeTrue()
+        ->and(User::query()->where('email', 'admin@acme.test')->exists())->toBeTrue();
+});
+
+test('storefront requests resolve a store by hostname', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $this->get('http://shop.test/')
+        ->assertOk();
+});
+
+test('storefront requests reject unknown or suspended stores', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $this->get('http://unknown-shop.test/')
+        ->assertNotFound();
+
+    $store = Store::factory()->suspended()->create();
+    StoreDomain::factory()->create([
+        'store_id' => $store->getKey(),
+        'hostname' => 'suspended-shop.test',
+    ]);
+
+    $this->get('http://suspended-shop.test/')
+        ->assertServiceUnavailable();
+});
+
+test('admin requests resolve the selected session store for authorized staff', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+
+    $this->actingAs($user)
+        ->withSession(['current_store_id' => $store->getKey()])
+        ->get('/admin')
+        ->assertOk();
 });
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/CustomerAuthTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Customer;
+use App\Models\Store;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Auth;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+test('customer authentication is scoped to the current store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+
+    Customer::factory()->create([
+        'store_id' => $firstStore->getKey(),
+        'email' => 'same@example.test',
+        'password' => 'first-password',
+    ]);
+
+    Customer::factory()->create([
+        'store_id' => $secondStore->getKey(),
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'first-password',
+    ]))->toBeTrue();
+
+    Auth::guard('customer')->logout();
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]))->toBeFalse();
+
+    app()->instance('current_store', $secondStore);
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]))->toBeTrue();
 });
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Store;
+use App\Models\User;
+use App\Policies\DiscountPolicy;
+use App\Policies\ProductPolicy;
+use App\Policies\StorePolicy;
+use Illuminate\Foundation\Testing\RefreshDatabase;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+function policySubjectFor(Store $store): object
+{
+    return new class($store->getKey())
+    {
+        public function __construct(public int $store_id) {}
+    };
+}
+
+test('store role helper returns the role for the selected store', function () {
+    $store = Store::factory()->create();
+    $user = User::factory()->create();
+
+    $store->users()->attach($user, ['role' => 'staff']);
+
+    expect($user->roleForStore($store)?->value)->toBe('staff');
+});
+
+test('product policy allows support to view but not mutate products', function () {
+    $store = Store::factory()->create();
+    $support = User::factory()->create();
+    $store->users()->attach($support, ['role' => 'support']);
+
+    app()->instance('current_store', $store);
+    $product = policySubjectFor($store);
+    $policy = new ProductPolicy;
+
+    expect($policy->viewAny($support))->toBeTrue()
+        ->and($policy->view($support, $product))->toBeTrue()
+        ->and($policy->create($support))->toBeFalse()
+        ->and($policy->update($support, $product))->toBeFalse()
+        ->and($policy->delete($support, $product))->toBeFalse();
+});
+
+test('staff can manage discounts but only owner or admin can delete them', function () {
+    $store = Store::factory()->create();
+    $staff = User::factory()->create();
+    $store->users()->attach($staff, ['role' => 'staff']);
+
+    app()->instance('current_store', $store);
+    $discount = policySubjectFor($store);
+    $policy = new DiscountPolicy;
+
+    expect($policy->create($staff))->toBeTrue()
+        ->and($policy->update($staff, $discount))->toBeTrue()
+        ->and($policy->delete($staff, $discount))->toBeFalse();
+});
+
+test('only owners can delete stores', function () {
+    $store = Store::factory()->create();
+    $owner = User::factory()->create();
+    $admin = User::factory()->create();
+
+    $store->users()->attach($owner, ['role' => 'owner']);
+    $store->users()->attach($admin, ['role' => 'admin']);
+
+    $policy = new StorePolicy;
+
+    expect($policy->delete($owner, $store))->toBeTrue()
+        ->and($policy->delete($admin, $store))->toBeFalse()
+        ->and($policy->update($admin, $store))->toBeTrue();
 });
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/TenancyTest.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/CustomerAuthTest.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php

@@ -2,6 +2,56 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Store;
+use App\Models\StoreDomain;
+use App\Models\User;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Schema;
+
+uses(RefreshDatabase::class);
+
+test('phase one tables and seeded tenant records exist', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    expect(Schema::hasColumns('organizations', ['name', 'billing_email']))->toBeTrue()
+        ->and(Schema::hasColumns('stores', ['organization_id', 'handle', 'status', 'default_currency']))->toBeTrue()
+        ->and(Schema::hasColumns('store_domains', ['store_id', 'hostname', 'type', 'is_primary']))->toBeTrue()
+        ->and(Schema::hasColumns('store_users', ['store_id', 'user_id', 'role']))->toBeTrue()
+        ->and(Schema::hasColumns('store_settings', ['store_id', 'settings_json']))->toBeTrue()
+        ->and(StoreDomain::query()->where('hostname', 'shop.test')->exists())->toBeTrue()
+        ->and(User::query()->where('email', 'admin@acme.test')->exists())->toBeTrue();
+});
+
+test('storefront requests resolve a store by hostname', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $this->get('http://shop.test/')
+        ->assertOk();
+});
+
+test('storefront requests reject unknown or suspended stores', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $this->get('http://unknown-shop.test/')
+        ->assertNotFound();
+
+    $store = Store::factory()->suspended()->create();
+    StoreDomain::factory()->create([
+        'store_id' => $store->getKey(),
+        'hostname' => 'suspended-shop.test',
+    ]);
+
+    $this->get('http://suspended-shop.test/')
+        ->assertServiceUnavailable();
+});
+
+test('admin requests resolve the selected session store for authorized staff', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();

-    $response->assertStatus(200);
+    $this->actingAs($user)
+        ->withSession(['current_store_id' => $store->getKey()])
+        ->get('/admin')
+        ->assertOk();
 });

@@ -2,6 +2,45 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Customer;
+use App\Models\Store;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Auth;
+
+uses(RefreshDatabase::class);

-    $response->assertStatus(200);
+test('customer authentication is scoped to the current store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+
+    Customer::factory()->create([
+        'store_id' => $firstStore->getKey(),
+        'email' => 'same@example.test',
+        'password' => 'first-password',
+    ]);
+
+    Customer::factory()->create([
+        'store_id' => $secondStore->getKey(),
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'first-password',
+    ]))->toBeTrue();
+
+    Auth::guard('customer')->logout();
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]))->toBeFalse();
+
+    app()->instance('current_store', $secondStore);
+
+    expect(Auth::guard('customer')->attempt([
+        'email' => 'same@example.test',
+        'password' => 'second-password',
+    ]))->toBeTrue();
 });

@@ -2,6 +2,71 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Store;
+use App\Models\User;
+use App\Policies\DiscountPolicy;
+use App\Policies\ProductPolicy;
+use App\Policies\StorePolicy;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
+function policySubjectFor(Store $store): object
+{
+    return new class($store->getKey())
+    {
+        public function __construct(public int $store_id) {}
+    };
+}
+
+test('store role helper returns the role for the selected store', function () {
+    $store = Store::factory()->create();
+    $user = User::factory()->create();
+
+    $store->users()->attach($user, ['role' => 'staff']);
+
+    expect($user->roleForStore($store)?->value)->toBe('staff');
+});
+
+test('product policy allows support to view but not mutate products', function () {
+    $store = Store::factory()->create();
+    $support = User::factory()->create();
+    $store->users()->attach($support, ['role' => 'support']);

-    $response->assertStatus(200);
+    app()->instance('current_store', $store);
+    $product = policySubjectFor($store);
+    $policy = new ProductPolicy;
+
+    expect($policy->viewAny($support))->toBeTrue()
+        ->and($policy->view($support, $product))->toBeTrue()
+        ->and($policy->create($support))->toBeFalse()
+        ->and($policy->update($support, $product))->toBeFalse()
+        ->and($policy->delete($support, $product))->toBeFalse();
+});
+
+test('staff can manage discounts but only owner or admin can delete them', function () {
+    $store = Store::factory()->create();
+    $staff = User::factory()->create();
+    $store->users()->attach($staff, ['role' => 'staff']);
+
+    app()->instance('current_store', $store);
+    $discount = policySubjectFor($store);
+    $policy = new DiscountPolicy;
+
+    expect($policy->create($staff))->toBeTrue()
+        ->and($policy->update($staff, $discount))->toBeTrue()
+        ->and($policy->delete($staff, $discount))->toBeFalse();
+});
+
+test('only owners can delete stores', function () {
+    $store = Store::factory()->create();
+    $owner = User::factory()->create();
+    $admin = User::factory()->create();
+
+    $store->users()->attach($owner, ['role' => 'owner']);
+    $store->users()->attach($admin, ['role' => 'admin']);
+
+    $policy = new StorePolicy;
+
+    expect($policy->delete($owner, $store))->toBeTrue()
+        ->and($policy->delete($admin, $store))->toBeFalse()
+        ->and($policy->update($admin, $store))->toBeTrue();
 });


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/TenancyTest.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/CustomerAuthTest.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/ExampleTest.php
@@
 <?php

+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
 it('returns a successful response', function () {
-    $response = $this->get('/');
+    $this->seed(DatabaseSeeder::class);
+
+    $response = $this->get('http://shop.test/');

-    $response->assertStatus(200);
+    $response->assertOk();
 });
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Feature/ExampleTest.php

@@ -2,6 +2,13 @@

+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
 it('returns a successful response', function () {
-    $response = $this->get('/');
+    $this->seed(DatabaseSeeder::class);

-    $response->assertStatus(200);
+    $response = $this->get('http://shop.test/');
+
+    $response->assertOk();
 });


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Feature/ExampleTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,180p' tests/Feature/DashboardTest.php


rg -n "get\('/|get\(\"/|post\('/login|assertRedirect\('/dashboard|dashboard" tests/Feature


sed -n '1,180p' tests/Feature/DashboardTest.php

<?php

use App\Models\User;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('guests are redirected to the login page', function () {
    $response = $this->get(route('dashboard'));
    $response->assertRedirect(route('login'));
});

test('authenticated users can visit the dashboard', function () {
    $user = User::factory()->create();
    $this->actingAs($user);

    $response = $this->get(route('dashboard'));
    $response->assertOk();
});


Output:
Chunk ID: 96e235
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 117
Output:
<?php

use App\Models\User;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('guests are redirected to the login page', function () {
    $response = $this->get(route('dashboard'));
    $response->assertRedirect(route('login'));
});

test('authenticated users can visit the dashboard', function () {
    $user = User::factory()->create();
    $this->actingAs($user);

    $response = $this->get(route('dashboard'));
    $response->assertOk();
});


rg -n "get\('/|get\(\"/|post\('/login|assertRedirect\('/dashboard|dashboard" tests/Feature

rg -n "get\\('/|get\\(\"/|post\\('/login|assertRedirect\\('/dashboard|dashboard" tests/Feature

Question: get\('/|get\(\"/|post\('/login|assertRedirect\('/dashboard|dashboard

tests/Feature/DashboardTest.php:8:    $response = $this->get(route('dashboard'));
tests/Feature/DashboardTest.php:12:test('authenticated users can visit the dashboard', function () {
tests/Feature/DashboardTest.php:16:    $response = $this->get(route('dashboard'));
tests/Feature/Foundation/TenancyTest.php:55:        ->get('/admin')
tests/Feature/Auth/AuthenticationTest.php:24:        ->assertRedirect(route('dashboard', absolute: false));
tests/Feature/Auth/EmailVerificationTest.php:34:    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
tests/Feature/Auth/EmailVerificationTest.php:65:        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');
tests/Feature/Auth/RegistrationTest.php:20:        ->assertRedirect(route('dashboard', absolute: false));
tests/Feature/Settings/ProfileUpdateTest.php:12:    $this->get('/settings/profile')->assertOk();


Output:
Chunk ID: 064eb1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 225
Output:
tests/Feature/DashboardTest.php:8:    $response = $this->get(route('dashboard'));
tests/Feature/DashboardTest.php:12:test('authenticated users can visit the dashboard', function () {
tests/Feature/DashboardTest.php:16:    $response = $this->get(route('dashboard'));
tests/Feature/Foundation/TenancyTest.php:55:        ->get('/admin')
tests/Feature/Auth/AuthenticationTest.php:24:        ->assertRedirect(route('dashboard', absolute: false));
tests/Feature/Auth/EmailVerificationTest.php:34:    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
tests/Feature/Auth/EmailVerificationTest.php:65:        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');
tests/Feature/Auth/RegistrationTest.php:20:        ->assertRedirect(route('dashboard', absolute: false));
tests/Feature/Settings/ProfileUpdateTest.php:12:    $this->get('/settings/profile')->assertOk();


sed -n '1,120p' tests/Feature/Auth/AuthenticationTest.php


sed -n '1,100p' tests/Feature/Auth/RegistrationTest.php


sed -n '1,100p' tests/Feature/Auth/EmailVerificationTest.php


sed -n '1,80p' tests/Feature/Settings/ProfileUpdateTest.php


sed -n '1,120p' tests/Feature/Auth/AuthenticationTest.php

<?php

use App\Models\User;
use Laravel\Fortify\Features;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('login screen can be rendered', function () {
    $response = $this->get(route('login'));

    $response->assertOk();
});

test('users can authenticate using the login screen', function () {
    $user = User::factory()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response
        ->assertSessionHasNoErrors()
        ->assertRedirect(route('dashboard', absolute: false));

    $this->assertAuthenticated();
});

test('users can not authenticate with invalid password', function () {
    $user = User::factory()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'wrong-password',
    ]);

    $response->assertSessionHasErrorsIn('email');

    $this->assertGuest();
});

test('users with two factor enabled are redirected to two factor challenge', function () {
    if (! Features::canManageTwoFactorAuthentication()) {
        $this->markTestSkipped('Two-factor authentication is not enabled.');
    }
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
    ]);

    $user = User::factory()->withTwoFactor()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response->assertRedirect(route('two-factor.login'));
    $this->assertGuest();
});

test('users can logout', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)->post(route('logout'));

    $response->assertRedirect(route('home'));
    $this->assertGuest();
});


Output:
Chunk ID: a2abfd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 451
Output:
<?php

use App\Models\User;
use Laravel\Fortify\Features;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('login screen can be rendered', function () {
    $response = $this->get(route('login'));

    $response->assertOk();
});

test('users can authenticate using the login screen', function () {
    $user = User::factory()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response
        ->assertSessionHasNoErrors()
        ->assertRedirect(route('dashboard', absolute: false));

    $this->assertAuthenticated();
});

test('users can not authenticate with invalid password', function () {
    $user = User::factory()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'wrong-password',
    ]);

    $response->assertSessionHasErrorsIn('email');

    $this->assertGuest();
});

test('users with two factor enabled are redirected to two factor challenge', function () {
    if (! Features::canManageTwoFactorAuthentication()) {
        $this->markTestSkipped('Two-factor authentication is not enabled.');
    }
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
    ]);

    $user = User::factory()->withTwoFactor()->create();

    $response = $this->post(route('login.store'), [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response->assertRedirect(route('two-factor.login'));
    $this->assertGuest();
});

test('users can logout', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)->post(route('logout'));

    $response->assertRedirect(route('home'));
    $this->assertGuest();
});


sed -n '1,80p' tests/Feature/Settings/ProfileUpdateTest.php

<?php

use App\Livewire\Settings\Profile;
use App\Models\User;
use Livewire\Livewire;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('profile page is displayed', function () {
    $this->actingAs($user = User::factory()->create());

    $this->get('/settings/profile')->assertOk();
});

test('profile information can be updated', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test(Profile::class)
        ->set('name', 'Test User')
        ->set('email', 'test@example.com')
        ->call('updateProfileInformation');

    $response->assertHasNoErrors();

    $user->refresh();

    expect($user->name)->toEqual('Test User');
    expect($user->email)->toEqual('test@example.com');
    expect($user->email_verified_at)->toBeNull();
});

test('email verification status is unchanged when email address is unchanged', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test(Profile::class)
        ->set('name', 'Test User')
        ->set('email', $user->email)
        ->call('updateProfileInformation');

    $response->assertHasNoErrors();

    expect($user->refresh()->email_verified_at)->not->toBeNull();
});

test('user can delete their account', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test('settings.delete-user-form')
        ->set('password', 'password')
        ->call('deleteUser');

    $response
        ->assertHasNoErrors()
        ->assertRedirect('/');

    expect($user->fresh())->toBeNull();
    expect(auth()->check())->toBeFalse();
});

test('correct password must be provided to delete account', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test('settings.delete-user-form')
        ->set('password', 'wrong-password')
        ->call('deleteUser');

    $response->assertHasErrors(['password']);

    expect($user->fresh())->not->toBeNull();
});


sed -n '1,100p' tests/Feature/Auth/RegistrationTest.php

<?php

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('registration screen can be rendered', function () {
    $response = $this->get(route('register'));

    $response->assertOk();
});

test('new users can register', function () {
    $response = $this->post(route('register.store'), [
        'name' => 'John Doe',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertSessionHasNoErrors()
        ->assertRedirect(route('dashboard', absolute: false));

    $this->assertAuthenticated();
});


sed -n '1,100p' tests/Feature/Auth/EmailVerificationTest.php

<?php

use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('email verification screen can be rendered', function () {
    $user = User::factory()->unverified()->create();

    $response = $this->actingAs($user)->get(route('verification.notice'));

    $response->assertOk();
});

test('email can be verified', function () {
    $user = User::factory()->unverified()->create();

    Event::fake();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1($user->email)]
    );

    $response = $this->actingAs($user)->get($verificationUrl);

    Event::assertDispatched(Verified::class);

    expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
});

test('email is not verified with invalid hash', function () {
    $user = User::factory()->unverified()->create();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1('wrong-email')]
    );

    $this->actingAs($user)->get($verificationUrl);

    expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});

test('already verified user visiting verification link is redirected without firing event again', function () {
    $user = User::factory()->create([
        'email_verified_at' => now(),
    ]);

    Event::fake();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1($user->email)]
    );

    $this->actingAs($user)->get($verificationUrl)
        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');

    expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
    Event::assertNotDispatched(Verified::class);
});


Output:
Chunk ID: ca64fa
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 153
Output:
<?php

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('registration screen can be rendered', function () {
    $response = $this->get(route('register'));

    $response->assertOk();
});

test('new users can register', function () {
    $response = $this->post(route('register.store'), [
        'name' => 'John Doe',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertSessionHasNoErrors()
        ->assertRedirect(route('dashboard', absolute: false));

    $this->assertAuthenticated();
});


Output:
Chunk ID: 24ca49
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 508
Output:
<?php

use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('email verification screen can be rendered', function () {
    $user = User::factory()->unverified()->create();

    $response = $this->actingAs($user)->get(route('verification.notice'));

    $response->assertOk();
});

test('email can be verified', function () {
    $user = User::factory()->unverified()->create();

    Event::fake();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1($user->email)]
    );

    $response = $this->actingAs($user)->get($verificationUrl);

    Event::assertDispatched(Verified::class);

    expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
});

test('email is not verified with invalid hash', function () {
    $user = User::factory()->unverified()->create();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1('wrong-email')]
    );

    $this->actingAs($user)->get($verificationUrl);

    expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});

test('already verified user visiting verification link is redirected without firing event again', function () {
    $user = User::factory()->create([
        'email_verified_at' => now(),
    ]);

    Event::fake();

    $verificationUrl = URL::temporarySignedRoute(
        'verification.verify',
        now()->addMinutes(60),
        ['id' => $user->id, 'hash' => sha1($user->email)]
    );

    $this->actingAs($user)->get($verificationUrl)
        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');

    expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
    Event::assertNotDispatched(Verified::class);
});


Output:
Chunk ID: 2cd63f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 511
Output:
<?php

use App\Livewire\Settings\Profile;
use App\Models\User;
use Livewire\Livewire;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('profile page is displayed', function () {
    $this->actingAs($user = User::factory()->create());

    $this->get('/settings/profile')->assertOk();
});

test('profile information can be updated', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test(Profile::class)
        ->set('name', 'Test User')
        ->set('email', 'test@example.com')
        ->call('updateProfileInformation');

    $response->assertHasNoErrors();

    $user->refresh();

    expect($user->name)->toEqual('Test User');
    expect($user->email)->toEqual('test@example.com');
    expect($user->email_verified_at)->toBeNull();
});

test('email verification status is unchanged when email address is unchanged', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test(Profile::class)
        ->set('name', 'Test User')
        ->set('email', $user->email)
        ->call('updateProfileInformation');

    $response->assertHasNoErrors();

    expect($user->refresh()->email_verified_at)->not->toBeNull();
});

test('user can delete their account', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test('settings.delete-user-form')
        ->set('password', 'password')
        ->call('deleteUser');

    $response
        ->assertHasNoErrors()
        ->assertRedirect('/');

    expect($user->fresh())->toBeNull();
    expect(auth()->check())->toBeFalse();
});

test('correct password must be provided to delete account', function () {
    $user = User::factory()->create();

    $this->actingAs($user);

    $response = Livewire::test('settings.delete-user-form')
        ->set('password', 'wrong-password')
        ->call('deleteUser');

    $response->assertHasErrors(['password']);

    expect($user->fresh())->not->toBeNull();
});


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/DashboardTest.php
@@
 use App\Models\User;
+use Database\Seeders\DatabaseSeeder;

 uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
@@
 test('authenticated users can visit the dashboard', function () {
-    $user = User::factory()->create();
+    $this->seed(DatabaseSeeder::class);
+
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
     $this->actingAs($user);

-    $response = $this->get(route('dashboard'));
+    $response = $this->get(route('admin.dashboard'));
     $response->assertOk();
 });
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Auth/AuthenticationTest.php
@@
     $response
         ->assertSessionHasNoErrors()
-        ->assertRedirect(route('dashboard', absolute: false));
+        ->assertRedirect(route('admin.dashboard', absolute: false));
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Auth/RegistrationTest.php
@@
     $response->assertSessionHasNoErrors()
-        ->assertRedirect(route('dashboard', absolute: false));
+        ->assertRedirect(route('admin.dashboard', absolute: false));
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Auth/EmailVerificationTest.php
@@
-    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+    $response->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1');
@@
     $this->actingAs($user)->get($verificationUrl)
-        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+        ->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1');
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Feature/DashboardTest.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Auth/AuthenticationTest.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Auth/RegistrationTest.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Auth/EmailVerificationTest.php

@@ -19,5 +19,5 @@
     $response->assertSessionHasNoErrors()
-        ->assertRedirect(route('dashboard', absolute: false));
+        ->assertRedirect(route('admin.dashboard', absolute: false));

     $this->assertAuthenticated();
-});
\ No newline at end of file
+});

@@ -3,2 +3,3 @@
 use App\Models\User;
+use Database\Seeders\DatabaseSeeder;

@@ -12,7 +13,9 @@
 test('authenticated users can visit the dashboard', function () {
-    $user = User::factory()->create();
+    $this->seed(DatabaseSeeder::class);
+
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
     $this->actingAs($user);

-    $response = $this->get(route('dashboard'));
+    $response = $this->get(route('admin.dashboard'));
     $response->assertOk();
-});
\ No newline at end of file
+});

@@ -33,3 +33,3 @@
     expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
-    $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+    $response->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1');
 });
@@ -64,3 +64,3 @@
     $this->actingAs($user)->get($verificationUrl)
-        ->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+        ->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1');

@@ -68,2 +68,2 @@
     Event::assertNotDispatched(Verified::class);
-});
\ No newline at end of file
+});

@@ -23,3 +23,3 @@
         ->assertSessionHasNoErrors()
-        ->assertRedirect(route('dashboard', absolute: false));
+        ->assertRedirect(route('admin.dashboard', absolute: false));

@@ -68,2 +68,2 @@
     $this->assertGuest();
-});
\ No newline at end of file
+});


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Feature/DashboardTest.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Auth/AuthenticationTest.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Auth/RegistrationTest.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Auth/EmailVerificationTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,160p' app/Actions/Fortify/ResetUserPassword.php


sed -n '1,160p' app/Actions/Fortify/ResetUserPassword.php

<?php

namespace App\Actions\Fortify;

use App\Concerns\PasswordValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;

class ResetUserPassword implements ResetsUserPasswords
{
    use PasswordValidationRules;

    /**
     * Validate and reset the user's forgotten password.
     *
     * @param  array<string, string>  $input
     */
    public function reset(User $user, array $input): void
    {
        Validator::make($input, [
            'password' => $this->passwordRules(),
        ])->validate();

        $user->forceFill([
            'password' => $input['password'],
        ])->save();
    }
}


Output:
Chunk ID: 15ae08
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 172
Output:
<?php

namespace App\Actions\Fortify;

use App\Concerns\PasswordValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;

class ResetUserPassword implements ResetsUserPasswords
{
    use PasswordValidationRules;

    /**
     * Validate and reset the user's forgotten password.
     *
     * @param  array<string, string>  $input
     */
    public function reset(User $user, array $input): void
    {
        Validator::make($input, [
            'password' => $this->passwordRules(),
        ])->validate();

        $user->forceFill([
            'password' => $input['password'],
        ])->save();
    }
}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Auth/CustomerUserProvider.php
@@
-        $store = app()->bound('current_store') ? app('current_store') : null;
-
-        if ($store instanceof Store) {
-            $query->where('store_id', $store->getKey());
+        $store = app()->bound('current_store') ? app('current_store') : null;
+
+        if (! $store instanceof Store) {
+            return null;
         }
+
+        $query->where('store_id', $store->getKey());

         return $query->first();
     }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Auth/CustomerUserProvider.php

@@ -37,6 +37,8 @@

-        if ($store instanceof Store) {
-            $query->where('store_id', $store->getKey());
+        if (! $store instanceof Store) {
+            return null;
         }

+        $query->where('store_id', $store->getKey());
+
         return $query->first();


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Auth/CustomerUserProvider.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


I’m running the focused Phase 1 tests now. I expect the first run may expose integration issues from changing the starter user password column and adding tenant middleware, so I’ll use the failures to tighten the slice rather than broaden scope.


php artisan test --compact tests/Feature/Foundation/TenancyTest.php tests/Feature/Foundation/CustomerAuthTest.php tests/Feature/Foundation/AuthorizationTest.php


Output:
Chunk ID: 84a775
Wall time: 0.6528 seconds
Process exited with code 2
Original token count: 2357
Output:

  ⨯⨯⨯⨯.⨯⨯⨯⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > phase one…   QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > admin requ…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > stor…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, staff, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:24

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > prod…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, support, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:32

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > staf…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, staff, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:48

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > only…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:64

  Tests:    8 failed, 1 passed (3 assertions)
  Duration: 0.38s


php artisan test --compact tests/Feature/Foundation/TenancyTest.php tests/Feature/Foundation/CustomerAuthTest.php tests/Feature/Foundation/AuthorizationTest.php

  ⨯⨯⨯⨯.⨯⨯⨯⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > phase one…   QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > admin requ…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > stor…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, staff, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:24

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > prod…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, support, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:32

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > staf…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, staff, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:48

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > only…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:37, owner, 2026-05-03 21:26:37))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:64

  Tests:    8 failed, 1 passed (3 assertions)
  Duration: 0.38s


The first test run exposed a pivot nuance: Laravel was still auto-writing `updated_at` for `store_users`. I’m disabling pivot timestamps explicitly and setting `created_at` where we attach seed/test roles, which matches the spec’s single timestamp column.


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/StoreUser.php
@@
     public $incrementing = false;

+    public $timestamps = false;
+
     public const UPDATED_AT = null;
*** Update File: /Users/fabianwesner/Herd/shop/database/seeders/StoreUserSeeder.php
@@
         $store->users()->syncWithoutDetaching([
-            $user->getKey() => ['role' => 'owner'],
+            $user->getKey() => ['role' => 'owner', 'created_at' => now()],
         ]);
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php
@@
-    $store->users()->attach($user, ['role' => 'staff']);
+    $store->users()->attach($user, ['role' => 'staff', 'created_at' => now()]);
@@
-    $store->users()->attach($support, ['role' => 'support']);
+    $store->users()->attach($support, ['role' => 'support', 'created_at' => now()]);
@@
-    $store->users()->attach($staff, ['role' => 'staff']);
+    $store->users()->attach($staff, ['role' => 'staff', 'created_at' => now()]);
@@
-    $store->users()->attach($owner, ['role' => 'owner']);
-    $store->users()->attach($admin, ['role' => 'admin']);
+    $store->users()->attach($owner, ['role' => 'owner', 'created_at' => now()]);
+    $store->users()->attach($admin, ['role' => 'admin', 'created_at' => now()]);
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Models/StoreUser.php
M /Users/fabianwesner/Herd/shop/database/seeders/StoreUserSeeder.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php

@@ -23,3 +23,3 @@

-    $store->users()->attach($user, ['role' => 'staff']);
+    $store->users()->attach($user, ['role' => 'staff', 'created_at' => now()]);

@@ -31,3 +31,3 @@
     $support = User::factory()->create();
-    $store->users()->attach($support, ['role' => 'support']);
+    $store->users()->attach($support, ['role' => 'support', 'created_at' => now()]);

@@ -47,3 +47,3 @@
     $staff = User::factory()->create();
-    $store->users()->attach($staff, ['role' => 'staff']);
+    $store->users()->attach($staff, ['role' => 'staff', 'created_at' => now()]);

@@ -63,4 +63,4 @@

-    $store->users()->attach($owner, ['role' => 'owner']);
-    $store->users()->attach($admin, ['role' => 'admin']);
+    $store->users()->attach($owner, ['role' => 'owner', 'created_at' => now()]);
+    $store->users()->attach($admin, ['role' => 'admin', 'created_at' => now()]);

@@ -11,2 +11,4 @@

+    public $timestamps = false;
+
     public const UPDATED_AT = null;

@@ -20,3 +20,3 @@
         $store->users()->syncWithoutDetaching([
-            $user->getKey() => ['role' => 'owner'],
+            $user->getKey() => ['role' => 'owner', 'created_at' => now()],
         ]);


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Models/StoreUser.php\nM /Users/fabianwesner/Herd/shop/database/seeders/StoreUserSeeder.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Foundation/AuthorizationTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Output:
Chunk ID: bc4fe7
Wall time: 0.5454 seconds
Process exited with code 2
Original token count: 2357
Output:

  ⨯⨯⨯⨯.⨯⨯⨯⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > phase one…   QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, owner, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, owner, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > storefront…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, owner, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\TenancyTest > admin requ…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, owner, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +14 vendor frames
  15  database/seeders/StoreUserSeeder.php:20
      +8 vendor frames
  24  database/seeders/DatabaseSeeder.php:14

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > stor…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, staff, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:24

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > prod…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, support, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:32

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > staf…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, staff, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:48

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Foundation\AuthorizationTest > only…  QueryException
  SQLSTATE[HY000]: General error: 1 table store_users has no column named updated_at (Connection: sqlite, Database: :memory:, SQL: insert into "store_users" ("user_id", "store_id", "created_at", "role", "updated_at") values (1, 1, 2026-05-03 21:26:55, owner, 2026-05-03 21:26:55))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:578
    574▕             if ($this->pretending()) {
    575▕                 return true;
    576▕             }
    577▕
  ➜ 578▕             $statement = $this->getPdo()->prepare($query);
    579▕
    580▕             $this->bindValues($statement, $this->prepareBindings($bindings));
    581▕
    582▕             $this->recordsHaveBeenModified();

      +11 vendor frames
  12  tests/Feature/Foundation/AuthorizationTest.php:64

  Tests:    8 failed, 1 passed (3 assertions)
  Duration: 0.38s