From 637b1d5a9aefd8d2190d9f264f567c5a6007c332 Mon Sep 17 00:00:00 2001 From: juicew <491852417@qq.com> Date: Wed, 19 Aug 2026 21:42:46 +0800 Subject: [PATCH] fix: avoid NPE when mock rule omits httpStatusCode (#6881) MockHandle.httpStatusCode has no default, so when a mock rule's handle JSON omits or nulls it, Gson leaves the field null and MockPlugin passes it to HttpStatus.valueOf(int), whose auto-unboxing throws NPE and makes every matching request return 500. Fall back to 200 (OK) via Optional.ofNullable(...).orElse(200) in MockPlugin.doExecute, so the mock response is returned instead. Add MockPluginTest covering: null httpStatusCode -> 200 mock response, an explicitly specified status code being honored, and a missing cached handle falling through to the plugin chain. --- .../apache/shenyu/plugin/mock/MockPlugin.java | 3 +- .../shenyu/plugin/mock/MockPluginTest.java | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 shenyu-plugin/shenyu-plugin-mock/src/test/java/org/apache/shenyu/plugin/mock/MockPluginTest.java diff --git a/shenyu-plugin/shenyu-plugin-mock/src/main/java/org/apache/shenyu/plugin/mock/MockPlugin.java b/shenyu-plugin/shenyu-plugin-mock/src/main/java/org/apache/shenyu/plugin/mock/MockPlugin.java index ce886acaa838..9a8eada8a84b 100644 --- a/shenyu-plugin/shenyu-plugin-mock/src/main/java/org/apache/shenyu/plugin/mock/MockPlugin.java +++ b/shenyu-plugin/shenyu-plugin-mock/src/main/java/org/apache/shenyu/plugin/mock/MockPlugin.java @@ -38,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; +import java.util.Optional; /** * MockPlugin. @@ -53,7 +54,7 @@ protected Mono doExecute(final ServerWebExchange exchange, final ShenyuPlu return chain.execute(exchange); } exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); - exchange.getResponse().setStatusCode(HttpStatus.valueOf(mockHandle.getHttpStatusCode())); + exchange.getResponse().setStatusCode(HttpStatus.valueOf(Optional.ofNullable(mockHandle.getHttpStatusCode()).orElse(200))); return DataBufferUtils.join(exchange.getRequest().getBody()) .switchIfEmpty(Mono.just(DefaultDataBufferFactory.sharedInstance.allocateBuffer(0))) diff --git a/shenyu-plugin/shenyu-plugin-mock/src/test/java/org/apache/shenyu/plugin/mock/MockPluginTest.java b/shenyu-plugin/shenyu-plugin-mock/src/test/java/org/apache/shenyu/plugin/mock/MockPluginTest.java new file mode 100644 index 000000000000..06f0e89d8d1e --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-mock/src/test/java/org/apache/shenyu/plugin/mock/MockPluginTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.mock; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.dto.convert.rule.MockHandle; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.base.utils.CacheKeyUtils; +import org.apache.shenyu.plugin.mock.handler.MockPluginHandler; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * test case for {@link MockPlugin}. + */ +@ExtendWith(MockitoExtension.class) +public final class MockPluginTest { + + private static final String SELECTOR_ID = "mock-selector"; + + private static final String RULE_ID = "mock-rule"; + + private final MockPlugin mockPlugin = new MockPlugin(); + + @Mock + private ShenyuPluginChain chain; + + private MockServerWebExchange exchange; + + @BeforeEach + public void setUp() { + exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localhost").build()); + MockPluginHandler.CACHED_HANDLE.get().removeHandle(CacheKeyUtils.INST.getKey(SELECTOR_ID, RULE_ID)); + } + + @AfterEach + public void clean() { + MockPluginHandler.CACHED_HANDLE.get().removeHandle(CacheKeyUtils.INST.getKey(SELECTOR_ID, RULE_ID)); + } + + /** + * issue #6881: a mock rule whose handle omits {@code httpStatusCode} must not NPE + * and should fall back to a 200 response with the mock content. + */ + @Test + public void testDoExecuteWithNullHttpStatusCode() { + RuleData ruleData = buildRuleData(); + MockHandle mockHandle = new MockHandle(); + mockHandle.setHttpStatusCode(null); + mockHandle.setResponseContent("{\"user\":\"test\"}"); + MockPluginHandler.CACHED_HANDLE.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData), mockHandle); + + StepVerifier.create(mockPlugin.doExecute(exchange, chain, mock(SelectorData.class), ruleData)) + .expectSubscription() + .verifyComplete(); + + assertThat(exchange.getResponse().getStatusCode(), is(HttpStatus.OK)); + assertThat(exchange.getResponse().getBodyAsString().block(), is("{\"user\":\"test\"}")); + verify(chain, never()).execute(any()); + } + + @Test + public void testDoExecuteWithSpecifiedHttpStatusCode() { + RuleData ruleData = buildRuleData(); + MockHandle mockHandle = new MockHandle(); + mockHandle.setHttpStatusCode(HttpStatus.NOT_FOUND.value()); + mockHandle.setResponseContent("not found"); + MockPluginHandler.CACHED_HANDLE.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData), mockHandle); + + StepVerifier.create(mockPlugin.doExecute(exchange, chain, mock(SelectorData.class), ruleData)) + .expectSubscription() + .verifyComplete(); + + assertThat(exchange.getResponse().getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(exchange.getResponse().getBodyAsString().block(), is("not found")); + verify(chain, never()).execute(any()); + } + + @Test + public void testDoExecuteWithoutCachedHandle() { + RuleData ruleData = buildRuleData(); + when(chain.execute(any())).thenReturn(Mono.empty()); + + StepVerifier.create(mockPlugin.doExecute(exchange, chain, mock(SelectorData.class), ruleData)) + .expectSubscription() + .verifyComplete(); + + verify(chain).execute(any()); + } + + @Test + public void testGetOrder() { + final int result = mockPlugin.getOrder(); + assertThat(PluginEnum.MOCK.getCode(), Matchers.is(result)); + } + + @Test + public void testNamed() { + final String result = mockPlugin.named(); + assertThat(PluginEnum.MOCK.getName(), Matchers.is(result)); + } + + private RuleData buildRuleData() { + return new RuleData() + .setSelectorId(SELECTOR_ID) + .setId(RULE_ID); + } +}