Files
portal-api/tests/Feature/Api/Admin/RememberLoginTest.php
T
baodev0 0b08689ea4 feat: harden admin auth and ticket workflows
- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
2026-10-16 17:00:55 +07:00

154 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Admin;
use App\Models\AdminRefreshToken;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
use Tymon\JWTAuth\Facades\JWTAuth;
class RememberLoginTest extends TestCase
{
use RefreshDatabase;
private const REFRESH_COOKIE = 'admin_refresh_token';
private User $admin;
protected function setUp(): void
{
parent::setUp();
$this->admin = User::factory()->create([
'username' => 'remember-admin',
'password' => Hash::make('Password123!'),
]);
}
public function test_login_only_returns_refresh_token_when_remember_is_enabled(): void
{
$credentials = [
'username' => $this->admin->username,
'password' => 'Password123!',
];
$this->postJson('/api/v1/administrator/login', $credentials)
->assertOk()
->assertJsonMissingPath('data.refresh_token')
->assertCookieExpired(self::REFRESH_COOKIE);
$response = $this->postJson('/api/v1/administrator/login', [...$credentials, 'remember' => true])
->assertOk()
->assertJsonPath('data.refresh_expires_in', 2592000)
->assertJsonMissingPath('data.refresh_token')
->assertCookie(self::REFRESH_COOKIE)
->assertCookieNotExpired(self::REFRESH_COOKIE);
$cookie = $response->getCookie(self::REFRESH_COOKIE, false);
$this->assertTrue($cookie->isHttpOnly());
$this->assertTrue($cookie->isSecure());
$this->assertSame('strict', $cookie->getSameSite());
$this->assertSame('/api/v1/administrator', $cookie->getPath());
$this->assertNotSame($response->json('data.token'), $cookie->getValue());
$this->assertNull(JWTAuth::setToken($response->json('data.token'))->getPayload()->get('remember_until'));
}
public function test_remembered_session_can_refresh_without_a_valid_access_token(): void
{
$login = $this->postJson('/api/v1/administrator/login', [
'username' => $this->admin->username,
'password' => 'Password123!',
'remember' => true,
])->assertOk();
$refreshToken = $login->getCookie(self::REFRESH_COOKIE, false)->getValue();
$this->withCredentials()
->withUnencryptedCookie(self::REFRESH_COOKIE, $refreshToken)
->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())
->assertOk()
->assertJsonMissingPath('data.refresh_token')
->assertJsonStructure(['data' => ['token', 'expires_in']])
->assertCookie(self::REFRESH_COOKIE);
}
public function test_remembered_session_can_refresh_before_thirty_days(): void
{
$login = $this->postJson('/api/v1/administrator/login', [
'username' => $this->admin->username,
'password' => 'Password123!',
'remember' => true,
])->assertOk();
$this->travelTo(now()->addDays(29));
$this->withCredentials()
->withUnencryptedCookie(
self::REFRESH_COOKIE,
$login->getCookie(self::REFRESH_COOKIE, false)->getValue(),
)->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())->assertOk();
}
public function test_remembered_session_cannot_refresh_after_thirty_days(): void
{
$login = $this->postJson('/api/v1/administrator/login', [
'username' => $this->admin->username,
'password' => 'Password123!',
'remember' => true,
])->assertOk();
$this->travelTo(now()->addDays(30)->addSecond());
$this->withCredentials()
->withUnencryptedCookie(
self::REFRESH_COOKIE,
$login->getCookie(self::REFRESH_COOKIE, false)->getValue(),
)->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())
->assertUnauthorized()
->assertCookieExpired(self::REFRESH_COOKIE);
}
public function test_refresh_rejects_body_tokens_and_non_ajax_requests(): void
{
$this->postJson('/api/v1/administrator/refresh', [
'refresh_token' => 'exposed-token',
])->assertForbidden();
$this->postJson('/api/v1/administrator/refresh', [
'refresh_token' => 'exposed-token',
], $this->refreshHeaders())->assertUnprocessable();
}
public function test_logout_revokes_the_http_only_refresh_token(): void
{
$login = $this->postJson('/api/v1/administrator/login', [
'username' => $this->admin->username,
'password' => 'Password123!',
'remember' => true,
])->assertOk();
$refreshToken = $login->getCookie(self::REFRESH_COOKIE, false)->getValue();
$this->assertTrue(AdminRefreshToken::where('token', hash('sha256', $refreshToken))->exists());
$this->withCredentials()
->withToken($login->json('data.token'))
->withUnencryptedCookie(self::REFRESH_COOKIE, $refreshToken)
->postJson('/api/v1/administrator/logout')
->assertOk()
->assertCookieExpired(self::REFRESH_COOKIE);
$this->assertFalse(AdminRefreshToken::where('token', hash('sha256', $refreshToken))->exists());
}
private function refreshHeaders(): array
{
return ['X-Requested-With' => 'XMLHttpRequest'];
}
}