Skip to content

Commit efa9e1d

Browse files
committed
Use LinkedHashMap for CORS configurations in CorsGatewayFilterApplicationListener to preserve insertion order. Fixes GH-3805.
Signed-off-by: Yavor Chamov <[email protected]>
1 parent 010a1a1 commit efa9e1d

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.cors;
18+
19+
import java.time.Duration;
20+
import java.util.LinkedHashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import org.awaitility.Awaitility;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.mockito.ArgumentCaptor;
29+
import org.mockito.Captor;
30+
import org.mockito.Mock;
31+
import org.mockito.junit.jupiter.MockitoExtension;
32+
import reactor.core.publisher.Flux;
33+
34+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
35+
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
36+
import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent;
37+
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
38+
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
39+
import org.springframework.cloud.gateway.route.Route;
40+
import org.springframework.cloud.gateway.route.RouteLocator;
41+
import org.springframework.web.cors.CorsConfiguration;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.mockito.Mockito.verify;
45+
import static org.mockito.Mockito.when;
46+
47+
/**
48+
* Tests for {@link CorsGatewayFilterApplicationListener}.
49+
*
50+
* <p>This test verifies that the merged CORS configurations composed of global and
51+
* per-route metadata maintain insertion order as defined by the use of {@link LinkedHashMap}.
52+
* Preserving insertion order helps for predictable and deterministic CORS behavior
53+
* when resolving multiple matching path patterns.
54+
* </p>
55+
*
56+
* <p>The test builds actual {@link Route} instances with {@code Path} predicates and verifies
57+
* that the resulting configuration map passed to {@link RoutePredicateHandlerMapping#setCorsConfigurations(Map)}
58+
* respects the declared order of:
59+
* <ul>
60+
* <li>Global CORS configurations (in insertion order)</li>
61+
* <li>Route-specific CORS configurations (in the order the routes are discovered)</li>
62+
* </ul>
63+
* </p>
64+
*
65+
* @author Yavor Chamov
66+
*/
67+
@ExtendWith(MockitoExtension.class)
68+
class CorsGatewayFilterApplicationListenerTests {
69+
70+
private static final String GLOBAL_PATH_1 = "/global1";
71+
private static final String GLOBAL_PATH_2 = "/global2";
72+
private static final String ROUTE_PATH_1 = "/route1";
73+
private static final String ROUTE_PATH_2 = "/route2";
74+
private static final String ORIGIN_GLOBAL_1 = "https://global1.com";
75+
private static final String ORIGIN_GLOBAL_2 = "https://global2.com";
76+
private static final String ORIGIN_ROUTE_1 = "https://route1.com";
77+
private static final String ORIGIN_ROUTE_2 = "https://route2.com";
78+
private static final String ROUTE_ID_1 = "route1";
79+
private static final String ROUTE_ID_2 = "route2";
80+
private static final String ROUTE_URI = "https://spring.io";
81+
private static final String METADATA_KEY = "cors";
82+
private static final String ALLOWED_ORIGINS_KEY = "allowedOrigins";
83+
84+
@Mock
85+
private RoutePredicateHandlerMapping handlerMapping;
86+
87+
@Mock
88+
private RouteLocator routeLocator;
89+
90+
@Captor
91+
private ArgumentCaptor<Map<String, CorsConfiguration>> corsConfigurations;
92+
93+
private GlobalCorsProperties globalCorsProperties;
94+
95+
private CorsGatewayFilterApplicationListener listener;
96+
97+
@BeforeEach
98+
void setUp() {
99+
globalCorsProperties = new GlobalCorsProperties();
100+
listener = new CorsGatewayFilterApplicationListener(globalCorsProperties,
101+
handlerMapping, routeLocator);
102+
}
103+
104+
@Test
105+
void testOnApplicationEvent_preservesInsertionOrder_withRealRoutes() {
106+
107+
globalCorsProperties.getCorsConfigurations().put(GLOBAL_PATH_1, createCorsConfig(ORIGIN_GLOBAL_1));
108+
globalCorsProperties.getCorsConfigurations().put(GLOBAL_PATH_2, createCorsConfig(ORIGIN_GLOBAL_2));
109+
110+
WebFluxProperties webFluxProperties = new WebFluxProperties();
111+
112+
Route route1 = buildRoute(ROUTE_ID_1, ROUTE_PATH_1, ORIGIN_ROUTE_1, webFluxProperties);
113+
Route route2 = buildRoute(ROUTE_ID_2, ROUTE_PATH_2, ORIGIN_ROUTE_2, webFluxProperties);
114+
115+
when(routeLocator.getRoutes()).thenReturn(Flux.just(route1, route2));
116+
117+
listener.onApplicationEvent(new RefreshRoutesResultEvent(this));
118+
119+
Awaitility.await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> {
120+
121+
verify(handlerMapping).setCorsConfigurations(corsConfigurations.capture());
122+
123+
Map<String, CorsConfiguration> mergedCorsConfigurations = corsConfigurations.getValue();
124+
assertThat(mergedCorsConfigurations.keySet()).containsExactly(GLOBAL_PATH_1,
125+
GLOBAL_PATH_2, ROUTE_PATH_1, ROUTE_PATH_2);
126+
assertThat(mergedCorsConfigurations.get(GLOBAL_PATH_1).getAllowedOrigins())
127+
.containsExactly(ORIGIN_GLOBAL_1);
128+
assertThat(mergedCorsConfigurations.get(GLOBAL_PATH_2).getAllowedOrigins())
129+
.containsExactly(ORIGIN_GLOBAL_2);
130+
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_1).getAllowedOrigins())
131+
.containsExactly(ORIGIN_ROUTE_1);
132+
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_2).getAllowedOrigins())
133+
.containsExactly(ORIGIN_ROUTE_2);
134+
});
135+
}
136+
137+
private CorsConfiguration createCorsConfig(String origin) {
138+
139+
CorsConfiguration config = new CorsConfiguration();
140+
config.setAllowedOrigins(List.of(origin));
141+
return config;
142+
}
143+
144+
private Route buildRoute(String id, String path, String allowedOrigin, WebFluxProperties webFluxProperties) {
145+
146+
return Route.async()
147+
.id(id)
148+
.uri(ROUTE_URI)
149+
.predicate(new PathRoutePredicateFactory(webFluxProperties)
150+
.apply(config -> config.setPatterns(List.of(path))))
151+
.metadata(METADATA_KEY, Map.of(ALLOWED_ORIGINS_KEY, List.of(allowedOrigin)))
152+
.build();
153+
}
154+
155+
}

0 commit comments

Comments
 (0)