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', ]); $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', ]); $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', ]); $cookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull($cookie); $this->assertEquals('strict', strtolower($cookie->getSameSite())); } public function test_login_sets_app_cookie_regardless_of_origin(): void { // Post-WS-3 PR-B2b: there is no per-app cookie resolution. Whatever // Origin (or no Origin) the request carries, the auth cookie issued // is always crewli_app_token. The request body alone determines auth. $cases = [ 'no Origin header' => [], 'app Origin' => ['Origin' => 'http://localhost:5174'], 'unknown Origin' => ['Origin' => 'http://localhost:9999'], 'foreign Origin' => ['Origin' => 'https://elsewhere.example.com'], ]; foreach ($cases as $label => $headers) { $user = User::factory()->create(); $response = $this->postJson('/api/v1/auth/login', [ 'email' => $user->email, 'password' => 'password', ], $headers); $response->assertOk(); $cookie = $this->findCookie($response, 'crewli_app_token'); $this->assertNotNull( $cookie, "crewli_app_token must be set for case: {$label}", ); } } // --- 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'); $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'); $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(); } // --- 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; } }