seed(RoleSeeder::class); // Enable cookies for JSON requests (required for cookie-based auth testing) $this->withCredentials(); } // --- Login Cookie Tests --- public function test_login_response_does_not_contain_token_in_json_body(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ]); $response->assertOk(); $response->assertJsonMissing(['token']); $this->assertArrayNotHasKey('token', $response->json('data')); } public function test_login_response_sets_httponly_cookie(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], ['Origin' => 'http://localhost:5174']); $response->assertOk(); $response->assertCookie('crewli_app_token'); } public function test_login_cookie_has_httponly_flag(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], ['Origin' => 'http://localhost:5174']); $cookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull($cookie, 'Cookie crewli_app_token not found'); $this->assertTrue($cookie->isHttpOnly(), 'Cookie must be httpOnly'); } public function test_login_cookie_has_samesite_strict(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], ['Origin' => 'http://localhost:5174']); $cookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull($cookie); $this->assertEquals('strict', strtolower($cookie->getSameSite())); } public function test_login_sets_admin_cookie_for_admin_origin(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], ['Origin' => 'http://localhost:5173']); $response->assertOk(); $response->assertCookie('crewli_admin_token'); } public function test_login_sets_portal_cookie_for_portal_origin(): void { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], ['Origin' => 'http://localhost:5175']); $response->assertOk(); $response->assertCookie('crewli_portal_token'); } // --- Middleware Tests --- public function test_request_with_auth_cookie_is_authenticated(): void { $user = User::factory()->create(); $token = $user->createToken('auth-token')->plainTextToken; $response = $this->withUnencryptedCookie('crewli_app_token', $token) ->getJson('/api/v1/auth/me'); $response->assertOk(); $response->assertJsonPath('data.id', $user->id); } public function test_request_with_invalid_cookie_returns_401(): void { $response = $this->withUnencryptedCookie('crewli_app_token', 'invalid-token-value') ->getJson('/api/v1/auth/me'); $response->assertUnauthorized(); } public function test_request_without_cookie_or_header_returns_401(): void { $response = $this->getJson('/api/v1/auth/me'); $response->assertUnauthorized(); } // --- Logout Tests --- public function test_logout_expires_auth_cookie(): void { $user = User::factory()->create(); $token = $user->createToken('auth-token')->plainTextToken; $response = $this->withUnencryptedCookie('crewli_app_token', $token) ->postJson('/api/v1/auth/logout', [], ['Origin' => 'http://localhost:5174']); $response->assertOk(); $cookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull($cookie, 'Cookie crewli_app_token not found in logout response'); // Expired cookie has a past expiry time $this->assertTrue($cookie->getExpiresTime() < time(), 'Logout cookie must be expired'); } // --- Refresh Tests --- public function test_refresh_revokes_old_token_and_sets_new_cookie(): void { $user = User::factory()->create(); $accessToken = $user->createToken('auth-token'); $token = $accessToken->plainTextToken; $response = $this->withUnencryptedCookie('crewli_app_token', $token) ->postJson('/api/v1/auth/refresh', [], ['Origin' => 'http://localhost:5174']); $response->assertOk(); $response->assertCookie('crewli_app_token'); // New cookie should contain a different token $newCookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull($newCookie); $this->assertNotEquals($token, $newCookie->getValue(), 'New token must differ from old token'); // Old token should be revoked in the database $this->assertNull( \Laravel\Sanctum\PersonalAccessToken::findToken($token), 'Old token must be deleted from database', ); // New token should be valid in the database $this->assertNotNull( \Laravel\Sanctum\PersonalAccessToken::findToken($newCookie->getValue()), 'New token must exist in database', ); } public function test_refresh_with_expired_token_returns_401(): void { $response = $this->withUnencryptedCookie('crewli_app_token', 'expired-or-invalid-token') ->postJson('/api/v1/auth/refresh'); $response->assertUnauthorized(); } // --- Cross-App Isolation Tests --- public function test_app_cookie_does_not_authenticate_portal_requests(): void { $user = User::factory()->create(); $token = $user->createToken('auth-token')->plainTextToken; // App cookie is set, but request comes from portal origin — // middleware should only read crewli_portal_token, not crewli_app_token $response = $this->withUnencryptedCookie('crewli_app_token', $token) ->getJson('/api/v1/auth/me', ['Origin' => 'http://localhost:5175']); $response->assertUnauthorized(); } public function test_portal_cookie_does_not_authenticate_app_requests(): void { $user = User::factory()->create(); $token = $user->createToken('auth-token')->plainTextToken; $response = $this->withUnencryptedCookie('crewli_portal_token', $token) ->getJson('/api/v1/auth/me', ['Origin' => 'http://localhost:5174']); $response->assertUnauthorized(); } public function test_correct_cookie_authenticates_with_matching_origin(): void { $user = User::factory()->create(); $token = $user->createToken('auth-token')->plainTextToken; // Portal cookie + portal origin = authenticated $response = $this->withUnencryptedCookie('crewli_portal_token', $token) ->getJson('/api/v1/auth/me', ['Origin' => 'http://localhost:5175']); $response->assertOk(); $response->assertJsonPath('data.id', $user->id); } // --- Helper --- private function findCookie($response, string $name): ?\Symfony\Component\HttpFoundation\Cookie { foreach ($response->headers->getCookies() as $cookie) { if ($cookie->getName() === $name) { return $cookie; } } return null; } }