request('POST', '/api/password/forgot', [ 'json' => ['email' => 'test@example.com'], 'headers' => [ 'Host' => 'localhost', ], ]); // Should NOT return 401 Unauthorized // It should return 200 (success) or 429 (rate limited), but never 401 $status = $response->getStatusCode(); self::assertNotEquals(401, $status, 'Password forgot endpoint should be accessible without JWT'); self::assertContains($status, [200, 201, 429], 'Expected 200/201 (success) or 429 (rate limited)'); } #[Test] public function passwordResetEndpointIsAccessibleWithoutAuthentication(): void { $client = static::createClient(); $response = $client->request('POST', '/api/password/reset', [ 'json' => [ 'token' => 'invalid-token-for-test', 'password' => 'NewSecurePassword123!', ], 'headers' => [ 'Host' => 'localhost', ], ]); // Should NOT return 401 Unauthorized // It should return 400 (invalid token) or 410 (expired), but never 401 self::assertNotEquals(401, $response->getStatusCode(), 'Password reset endpoint should be accessible without JWT'); // With an invalid token, we expect a 400 Bad Request self::assertResponseStatusCodeSame(400); } #[Test] public function passwordForgotValidatesEmailFormat(): void { $client = static::createClient(); $response = $client->request('POST', '/api/password/forgot', [ 'json' => ['email' => 'not-an-email'], 'headers' => [ 'Host' => 'localhost', ], ]); // Invalid email format returns validation error (422) // This is acceptable because it doesn't reveal user existence // (format validation happens before checking if user exists) self::assertResponseStatusCodeSame(422); } #[Test] public function passwordResetValidatesPasswordRequirements(): void { $client = static::createClient(); // Password too short $response = $client->request('POST', '/api/password/reset', [ 'json' => [ 'token' => 'some-token', 'password' => 'short', ], 'headers' => [ 'Host' => 'localhost', ], ]); // Should return 422 Unprocessable Entity for validation errors self::assertResponseStatusCodeSame(422); } }