This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 80fbc73e05 test(ai-proxy): verify proxy key isolation over real HTTP 
(#7943)
80fbc73e05 is described below

commit 80fbc73e05eeecce7ebf84fda1409bcb8c70f751
Author: redyyang <[email protected]>
AuthorDate: Fri Oct 23 11:17:45 2026 +0800

    test(ai-proxy): verify proxy key isolation over real HTTP (#7943)
    
    * test(ai-proxy): verify proxy key isolation over real HTTP
    
    * test(ai-proxy): reject invalid proxy keys before contacting upstream
    
    * test(ci): align cryptor failures and wait for usable SDK routes
---
 .../enhanced/AiProxyHttpCompatibilityTest.java     | 91 ++++++++++++++++++++++
 1 file changed, 91 insertions(+)

diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
index 5fec678283..5bdd6d3a9d 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
@@ -21,10 +21,12 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.shenyu.common.constant.Constants;
 import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.ProxyApiKeyData;
 import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
 import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.handler.AiProxyPluginHandler;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.cache.AiProxyApiKeyCache;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocolFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiChat;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiResponses;
@@ -43,6 +45,7 @@ import 
org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.web.reactive.function.client.WebClient;
 import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 import reactor.netty.DisposableServer;
 import reactor.netty.http.server.HttpServer;
 import reactor.test.StepVerifier;
@@ -117,6 +120,94 @@ class AiProxyHttpCompatibilityTest {
         }
     }
 
+    @Test
+    void proxyKeysRemainIsolatedAcrossSequentialAndConcurrentRequests() {
+        // The engine replaced cached ChatClient instances; keep isolation 
covered at the HTTP boundary.
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) ->
+                response.header("Content-Type", "application/json")
+                        .header("X-Upstream-Account", 
request.requestHeaders().get("Authorization"))
+                        .sendString(Flux.just(CHAT_REPLY))).bindNow(TIMEOUT);
+        final AiProxyPluginHandler handler = new AiProxyPluginHandler();
+        AiProxyHandle handle = new AiProxyHandle();
+        handle.setProvider("OpenAI");
+        handle.setBaseUrl("http://127.0.0.1:"; + server.port());
+        handle.setApiKey("selector-default");
+        handle.setStream(false);
+        handle.setProxyEnabled("true");
+        handle.setProtocol(OpenAiChat.NAME);
+        handle.setModel("model");
+        
handler.getSelectorCachedHandle().cachedHandle(CacheKeyUtils.INST.getKey("http-selector",
 Constants.DEFAULT_RULE), handle);
+        AiProxyPlugin plugin = plugin(handler);
+        for (String account : List.of("account-a", "account-b")) {
+            
AiProxyApiKeyCache.getInstance().cache(ProxyApiKeyData.builder().selectorId("http-selector")
+                    .proxyApiKey("proxy-" + 
account).realApiKey(account).enabled(true).build());
+        }
+        try {
+            for (String account : List.of("account-a", "account-b", 
"account-a")) {
+                StepVerifier.create(executeForAccount(plugin, 
account)).verifyComplete();
+            }
+            StepVerifier.create(Flux.range(0, 32)
+                    .flatMap(index -> executeForAccount(plugin, index % 2 == 0 
? "account-a" : "account-b"), 8)
+                    .then()).expectComplete().verify(Duration.ofSeconds(15));
+            assertEquals("selector-default", handle.getApiKey());
+        } finally {
+            server.disposeNow(TIMEOUT);
+            for (String account : List.of("account-a", "account-b")) {
+                
AiProxyApiKeyCache.getInstance().remove(ProxyApiKeyData.builder().selectorId("http-selector")
+                        .proxyApiKey("proxy-" + account).build());
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void unknownOrDisabledProxyKeysCannotUseTheSelectorCredential(final 
boolean disabled) {
+        AtomicBoolean called = new AtomicBoolean();
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+            called.set(true);
+            return response.sendString(Mono.just(CHAT_REPLY));
+        }).bindNow(TIMEOUT);
+        final AiProxyPluginHandler handler = new AiProxyPluginHandler();
+        AiProxyHandle handle = new AiProxyHandle();
+        handle.setProvider("OpenAI");
+        handle.setBaseUrl("http://127.0.0.1:"; + server.port());
+        handle.setApiKey("selector-default");
+        handle.setProxyEnabled("true");
+        handle.setStream(false);
+        handle.setProtocol(OpenAiChat.NAME);
+        handle.setModel("model");
+        
handler.getSelectorCachedHandle().cachedHandle(CacheKeyUtils.INST.getKey("http-selector",
 Constants.DEFAULT_RULE), handle);
+        ProxyApiKeyData mapping = 
ProxyApiKeyData.builder().selectorId("http-selector")
+                
.proxyApiKey("rejected-proxy-key").realApiKey("disabled-real-key").enabled(false).build();
+        if (disabled) {
+            AiProxyApiKeyCache.getInstance().cache(mapping);
+        }
+        try {
+            MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/chat")
+                    .header(Constants.X_API_KEY, mapping.getProxyApiKey())
+                    
.body("{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":false}"));
+            StepVerifier.create(plugin(handler).doExecute(exchange, 
mock(ShenyuPluginChain.class), selector(), new RuleData()))
+                    .expectComplete().verify(TIMEOUT);
+            assertEquals(401, exchange.getResponse().getStatusCode().value());
+            assertTrue(!called.get(), "rejected proxy keys must not open an 
upstream connection");
+            assertEquals("selector-default", handle.getApiKey());
+        } finally {
+            AiProxyApiKeyCache.getInstance().remove(mapping);
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
+    private Mono<Void> executeForAccount(final AiProxyPlugin plugin, final 
String account) {
+        MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/chat")
+                .header(Constants.X_API_KEY, "proxy-" + account)
+                
.body("{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":false}"));
+        return plugin.doExecute(exchange, mock(ShenyuPluginChain.class), 
selector(), new RuleData())
+                .then(Mono.fromRunnable(() -> {
+                    assertEquals(200, 
exchange.getResponse().getStatusCode().value());
+                    assertEquals("Bearer " + account, 
exchange.getResponse().getHeaders().getFirst("X-Upstream-Account"));
+                }));
+    }
+
     @Test
     void rejectsResponsesStateBeforeOpeningAnUpstreamConnection() {
         AtomicBoolean called = new AtomicBoolean();

Reply via email to