. */ declare(strict_types=1); namespace App\Tests\Services\OAuth; use App\Entity\UserSystem\ApiTokenLevel; use App\Entity\UserSystem\OAuthClientGrantPreference; use App\Services\OAuth\OAuthClientGrantPreferenceManager; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; final class OAuthClientGrantPreferenceManagerTest extends KernelTestCase { public function testSaveUpsertsAllFields(): void { self::bootKernel(); $manager = static::getContainer()->get(OAuthClientGrantPreferenceManager::class); $userIdentifier = 'test-user-'.bin2hex(random_bytes(8)); $clientIdentifier = 'test-client-'.bin2hex(random_bytes(8)); self::assertNull($manager->find($userIdentifier, $clientIdentifier)); $manager->save($userIdentifier, $clientIdentifier, ApiTokenLevel::EDIT, 'First Name', 7); $preference = $manager->find($userIdentifier, $clientIdentifier); self::assertInstanceOf(OAuthClientGrantPreference::class, $preference); self::assertSame(ApiTokenLevel::EDIT, $preference->getScopeLevel()); self::assertSame('First Name', $preference->getFriendlyName()); self::assertSame(7, $preference->getRefreshTokenTtlDays()); // A second save() for the same (user, client) pair must update the existing row, not create // a second one. $manager->save($userIdentifier, $clientIdentifier, ApiTokenLevel::READ_ONLY, null, null); $updated = $manager->find($userIdentifier, $clientIdentifier); self::assertInstanceOf(OAuthClientGrantPreference::class, $updated); self::assertSame($preference->getId(), $updated->getId()); self::assertSame(ApiTokenLevel::READ_ONLY, $updated->getScopeLevel()); self::assertNull($updated->getFriendlyName()); self::assertNull($updated->getRefreshTokenTtlDays()); } public function testRecordUsageSetsLastUsedAndSelfHealsMissingPreference(): void { self::bootKernel(); $manager = static::getContainer()->get(OAuthClientGrantPreferenceManager::class); $userIdentifier = 'test-user-'.bin2hex(random_bytes(8)); $clientIdentifier = 'test-client-'.bin2hex(random_bytes(8)); self::assertNull($manager->find($userIdentifier, $clientIdentifier)); // No preference exists yet - recordUsage() must create one, inferring the scope level from the // token's own scopes rather than silently dropping the usage record. $manager->recordUsage($userIdentifier, $clientIdentifier, ['read_only', 'edit']); $preference = $manager->find($userIdentifier, $clientIdentifier); self::assertInstanceOf(OAuthClientGrantPreference::class, $preference); self::assertSame(ApiTokenLevel::EDIT, $preference->getScopeLevel()); self::assertNotNull($preference->getLastUsedAt()); } public function testRecordUsageDoesNotOverwriteExistingPreferenceFields(): void { self::bootKernel(); $manager = static::getContainer()->get(OAuthClientGrantPreferenceManager::class); $userIdentifier = 'test-user-'.bin2hex(random_bytes(8)); $clientIdentifier = 'test-client-'.bin2hex(random_bytes(8)); $manager->save($userIdentifier, $clientIdentifier, ApiTokenLevel::ADMIN, 'My Device', 30); $manager->recordUsage($userIdentifier, $clientIdentifier, ['read_only']); $preference = $manager->find($userIdentifier, $clientIdentifier); self::assertInstanceOf(OAuthClientGrantPreference::class, $preference); // Untouched by recordUsage() - only lastUsedAt changes. self::assertSame(ApiTokenLevel::ADMIN, $preference->getScopeLevel()); self::assertSame('My Device', $preference->getFriendlyName()); self::assertSame(30, $preference->getRefreshTokenTtlDays()); self::assertNotNull($preference->getLastUsedAt()); } }