getUserBadgeFrom('token-b'); self::assertSame('claude-desktop', $badge->getUserIdentifier()); self::assertSame(['ROLE_CHATBOT_MCP'], $badge->getUser()->getRoles()); self::assertSame('n8n', $handler->getUserBadgeFrom('token-a')->getUserIdentifier()); } public function testATokenMayContainColons(): void { self::assertSame('n8n', (new McpAccessTokenHandler('n8n:a:b:c'))->getUserBadgeFrom('a:b:c')->getUserIdentifier()); } public function testAnUnknownTokenIsRejected(): void { $this->expectException(BadCredentialsException::class); (new McpAccessTokenHandler('n8n:token-a'))->getUserBadgeFrom('token-b'); } public function testAClientNameIsNotAToken(): void { $this->expectException(BadCredentialsException::class); (new McpAccessTokenHandler('n8n:token-a'))->getUserBadgeFrom('n8n'); } public function testAnEmptyListRejectsEverything(): void { $this->expectException(BadCredentialsException::class); (new McpAccessTokenHandler(' '))->getUserBadgeFrom(''); } /** * @return iterable */ public static function malformedLists(): iterable { yield 'token without name' => ['token-a']; yield 'empty token' => ['n8n:']; yield 'invalid name' => ['n 8 n:token-a']; yield 'duplicate name' => ['n8n:token-a,n8n:token-b']; yield 'shared token' => ['n8n:token-a,claude-desktop:token-a']; } #[DataProvider('malformedLists')] public function testAMalformedListFailsLoudly(string $tokens): void { $this->expectException(\InvalidArgumentException::class); new McpAccessTokenHandler($tokens); } }