'example.invalid', 'port' => '443', 'id' => self::UUID, 'net' => 'ws', 'host' => 'cdn.example.invalid', 'sni' => 'tls.example.invalid', 'tls' => 'tls', 'ps' => 'original']; $node = Codec::parse("\xEF\xBB\xBFvmess://".base64_encode(json_encode($config))); $uri = Codec::uri($node, '中文 + name'); self::assertSame('中文 + name', json_decode(Codec::decode64(substr($uri, 8)), true)['ps']); self::assertSame('tls.example.invalid', Codec::singbox($node, 'x')['tls']['server_name']); } public function testPasswordCharactersAreNotDecodedTwice(): void { $node = Codec::parse('trojan://a%2Bb%40c%23d@example.invalid:443?security=tls#test'); self::assertSame('a+b@c#d', Codec::singbox($node, 'x')['password']); } public function testSsBase64UrlAndPlugin(): void { $auth = rtrim(strtr(base64_encode('aes-128-gcm:a+b'), '+/', '-_'), '='); $node = Codec::parse('ss://'.$auth.'@example.invalid:443?plugin=v2ray-plugin%3Btls%3Bhost%3Dexample.invalid#test'); $out = Codec::singbox($node, 'x'); self::assertSame('a+b', $out['password']); self::assertSame('v2ray-plugin', $out['plugin']); self::assertSame('tls;host=example.invalid', $out['plugin_opts']); self::assertSame($node['id'], Codec::parse(Codec::uri($node, 'new'))['id']); } public function testQueryOrderAndNamesDoNotChangeIdentity(): void { $a = Codec::parse('vless://'.self::UUID.'@example.invalid:443?type=ws&path=%2Fx#first'); $b = Codec::parse('vless://'.self::UUID.'@example.invalid:443?path=%2Fx&type=ws#second'); self::assertSame($a['id'], $b['id']); } public function testUnknownTransportIsNotSilentlyConverted(): void { $node = Codec::parse('vless://'.self::UUID.'@example.invalid:443?type=xhttp#test'); $this->expectException(InvalidArgumentException::class); Codec::singbox($node, 'x'); } public function testInvalidPortIsRejected(): void { $this->expectException(InvalidArgumentException::class); Codec::parse('socks://example.invalid:0#test'); } }