initialize(); function assertSecretSafe(array $result, string $secret, string $message): void { $serialized = json_encode($result, JSON_UNESCAPED_UNICODE) ?: ''; if (str_contains($serialized, $secret)) { fwrite(STDERR, "FAIL: {$message}\n"); exit(1); } } $qwenSecret = 'unit-test-qwen-sensitive-placeholder'; $openAiSecret = 'unit-test-openai-sensitive-placeholder'; $baseConfig = [ 'enable' => false, 'base_url' => 'https://ai.example.test/v1', 'timeout' => 90, 'models' => [ 'qwen' => ['name' => 'qwen-test', 'label' => 'Qwen', 'api_key' => $qwenSecret], 'openai' => ['name' => 'openai-test', 'label' => 'OpenAI', 'api_key' => $openAiSecret], ], ]; function assertAllSecretsSafe(array $result, array $secrets, string $message): void { foreach ($secrets as $secret) { assertSecretSafe($result, $secret, $message); } } $secrets = [$qwenSecret, $openAiSecret]; $resolveProfile = (new ReflectionClass(DifyChatService::class))->getMethod('resolveProfileConfig'); $resolvedQwen = $resolveProfile->invoke(null, $baseConfig, 'qwen'); $resolvedOpenAi = $resolveProfile->invoke(null, $baseConfig, 'openai'); if ( !is_array($resolvedQwen) || !is_array($resolvedOpenAi) || ($resolvedQwen['api_key'] ?? null) !== $qwenSecret || ($resolvedOpenAi['api_key'] ?? null) !== $openAiSecret ) { fwrite(STDERR, "FAIL: each model key must resolve only its own server credential\n"); exit(1); } if ($resolveProfile->invoke(null, $baseConfig, 'other') !== null) { fwrite(STDERR, "FAIL: non-whitelisted profile must not resolve server configuration\n"); exit(1); } config($baseConfig, 'prescription_ai'); $disabled = DifyChatService::chat('qwen', [], 'test', 'test-user'); assertAllSecretsSafe($disabled, $secrets, 'disabled response must not expose credentials'); $enabledConfig = $baseConfig; $enabledConfig['enable'] = true; config($enabledConfig, 'prescription_ai'); foreach (['other', 'QWEN', ' openai', 'gpt-5.6-sol'] as $invalidProfile) { $invalid = DifyChatService::chat($invalidProfile, [], 'test', 'test-user'); if (($invalid['error_code'] ?? '') !== 'INVALID_PROFILE') { fwrite(STDERR, "FAIL: invalid profile must be rejected before upstream work\n"); exit(1); } assertAllSecretsSafe($invalid, $secrets, 'invalid-profile response must not expose credentials'); } $invalidUrlConfig = $baseConfig; $invalidUrlConfig['enable'] = true; $invalidUrlConfig['base_url'] = 'file:///not-allowed'; config($invalidUrlConfig, 'prescription_ai'); $invalidUrl = DifyChatService::chat('qwen', [], 'test', 'test-user'); assertAllSecretsSafe($invalidUrl, $secrets, 'invalid URL response must not expose credentials'); $headerInjectionConfig = $baseConfig; $headerInjectionConfig['enable'] = true; $headerInjectionConfig['models']['qwen']['api_key'] = $qwenSecret . "\r\nInjected: value"; config($headerInjectionConfig, 'prescription_ai'); $headerInjection = DifyChatService::chat('qwen', [], 'test', 'test-user'); assertAllSecretsSafe($headerInjection, $secrets, 'invalid credential response must not expose credentials'); echo "Prescription AI secret safety: OK\n";