diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 9041a5d8f501..5775ab207147 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -138,7 +138,6 @@ dependencies { api("org.seleniumhq.selenium:selenium-java:4.41.0") api("org.skyscreamer:jsonassert:1.5.3") api("org.testng:testng:7.12.0") - api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-lite:1.1.0") api("org.xmlunit:xmlunit-assertj:2.10.4") api("org.xmlunit:xmlunit-matchers:2.10.4") diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 1b9b668ba0d3..24add7f4f3dd 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -238,7 +238,7 @@ private static MimeType parseMimeTypeInternal(String mimeType) { break; } } - else if (ch == '"') { + else if (ch == '"' && mimeType.charAt(nextIndex - 1) != '\\') { quoted = !quoted; } nextIndex++; diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index b1998ca07958..6346b8ec5405 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -98,7 +98,7 @@ void parseQuotedCharset() { } @Test - void parseQuotedSeparator() { + void parseQuotedParameterValue() { String s = "application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\""; MimeType mimeType = MimeType.valueOf(s); assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application"); @@ -107,6 +107,15 @@ void parseQuotedSeparator() { assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\""); } + @Test + void parseParameterWithQuotedPair() { + String s = "text/plain;twelve=\"1\\\"2\""; + MimeType mimeType = MimeType.valueOf(s); + assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text"); + assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("plain"); + assertThat(mimeType.getParameter("twelve")).isEqualTo("\"1\\\"2\""); + } + @Test void withConversionService() { ConversionService conversionService = new DefaultConversionService(); diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 16c9be3a271f..8de09951ee51 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -61,7 +61,7 @@ dependencies { testRuntimeOnly("org.glassfish:jakarta.el") testRuntimeOnly("org.jruby:jruby") testRuntimeOnly("org.python:jython-standalone") - testRuntimeOnly("org.webjars:underscorejs") + testRuntimeOnly("org.webjars:momentjs:2.29.4") } test { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java index 6983d2fddfae..3fd7b40c4025 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java @@ -90,7 +90,8 @@ protected Mono resolveUrlPathInternal(String resourceUrlPath, .switchIfEmpty(Mono.defer(() -> { String webJarResourcePath = findWebJarResourcePath(resourceUrlPath); if (webJarResourcePath != null) { - return chain.resolveUrlPath(webJarResourcePath, locations); + Mono fallback = (webJarResourcePath.endsWith("/")) ? Mono.just(webJarResourcePath) : Mono.empty(); + return chain.resolveUrlPath(webJarResourcePath, locations).switchIfEmpty(fallback); } else { return Mono.empty(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java index e7a5ce16cff8..fe8d35f87c94 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java @@ -29,6 +29,8 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -80,8 +82,8 @@ void resolveUrlExistingNotInJarFile() { @Test void resolveUrlWebJarResource() { - String file = "underscorejs/underscore.js"; - String expected = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expected = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty()); given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.just(expected)); @@ -92,10 +94,24 @@ void resolveUrlWebJarResource() { verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); } + @Test + void resolveUrlWebJarDirectory() { + String folder = "momentjs/locale/"; + String expected = "momentjs/2.29.4/locale/"; + given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(Mono.empty()); + given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.empty()); + + String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain).block(TIMEOUT); + + assertThat(actual).isEqualTo(expected); + verify(this.chain, times(1)).resolveUrlPath(folder, this.locations); + verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); + } + @Test void resolveUrlWebJarResourceNotFound() { - String file = "something/something.js"; - given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty()); + String file = "momentjs/locale/unknown.js"; + given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(Mono.empty()); String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT); @@ -134,11 +150,11 @@ void resolveResourceNotFound() { @Test void resolveResourceWebJar() { - String file = "underscorejs/underscore.js"; + String file = "momentjs/momentjs.js"; given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty()); Resource expected = mock(); - String expectedPath = "underscorejs/1.8.3/underscore.js"; + String expectedPath = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveResource(this.exchange, expectedPath, this.locations)) .willReturn(Mono.just(expected)); diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 760530e3c87d..194278ad7c07 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -80,5 +80,5 @@ dependencies { testRuntimeOnly("org.glassfish:jakarta.el") testRuntimeOnly("org.jruby:jruby") testRuntimeOnly("org.python:jython-standalone") - testRuntimeOnly("org.webjars:underscorejs") + testRuntimeOnly("org.webjars:momentjs:2.29.4") } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 23550a46e046..53e4d165933f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -17,12 +17,14 @@ package org.springframework.web.servlet; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -42,9 +44,14 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.i18n.LocaleContext; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; @@ -513,7 +520,7 @@ private void initHandlerMappings(ApplicationContext context) { if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); // We keep HandlerMappings in sorted order. - AnnotationAwareOrderComparator.sort(this.handlerMappings); + sortStrategyBeans(context, matchingBeans, this.handlerMappings); } } else { @@ -559,7 +566,7 @@ private void initHandlerAdapters(ApplicationContext context) { if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList<>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. - AnnotationAwareOrderComparator.sort(this.handlerAdapters); + sortStrategyBeans(context, matchingBeans, this.handlerAdapters); } } else { @@ -598,7 +605,7 @@ private void initHandlerExceptionResolvers(ApplicationContext context) { if (!matchingBeans.isEmpty()) { this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); // We keep HandlerExceptionResolvers in sorted order. - AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); + sortStrategyBeans(context, matchingBeans, this.handlerExceptionResolvers); } } else { @@ -663,7 +670,7 @@ private void initViewResolvers(ApplicationContext context) { if (!matchingBeans.isEmpty()) { this.viewResolvers = new ArrayList<>(matchingBeans.values()); // We keep ViewResolvers in sorted order. - AnnotationAwareOrderComparator.sort(this.viewResolvers); + sortStrategyBeans(context, matchingBeans, this.viewResolvers); } } else { @@ -712,6 +719,24 @@ else if (logger.isDebugEnabled()) { } } + /** + * Sort the given strategy beans using {@link AnnotationAwareOrderComparator}, additionally + * consulting each bean's merged {@link BeanDefinition} for an + * {@link AbstractBeanDefinition#ORDER_ATTRIBUTE order attribute}, factory method or + * declared target type. This mirrors the ordering behavior the bean factory uses when + * resolving sorted dependency injections (see + * {@code DefaultListableBeanFactory.FactoryAwareOrderSourceProvider}), so programmatic + * ordering via {@code BeanRegistrar}, {@code GenericApplicationContext.registerBean(..., order)}, + * or a direct {@code ORDER_ATTRIBUTE} on a bean definition is reflected here. + */ + private static void sortStrategyBeans(ApplicationContext context, Map matchingBeans, List beans) { + if (beans.size() <= 1) { + return; + } + beans.sort(AnnotationAwareOrderComparator.INSTANCE.withSourceProvider( + new BeanDefinitionOrderSourceProvider(context, matchingBeans))); + } + /** * Obtain this servlet's MultipartResolver, if any. * @return the MultipartResolver used by this servlet, or {@code null} if none @@ -1405,4 +1430,63 @@ private static String getRequestUri(HttpServletRequest request) { return uri; } + /** + * {@link OrderComparator.OrderSourceProvider} that resolves order metadata for a given + * bean instance from its merged {@link BeanDefinition}: an + * {@link AbstractBeanDefinition#ORDER_ATTRIBUTE order attribute}, a factory method, and + * a declared target type when distinct from the bean's runtime class. Mirrors + * {@code DefaultListableBeanFactory.FactoryAwareOrderSourceProvider} so that + * DispatcherServlet's strategy detection sees the same ordering inputs the bean factory + * uses for sorted dependency injection. Standard {@code @Order} / {@link Ordered} + * fallback handling is left to the comparator. + */ + private static class BeanDefinitionOrderSourceProvider implements OrderComparator.OrderSourceProvider { + + private final @Nullable ApplicationContext context; + + private final Map instancesToBeanNames; + + BeanDefinitionOrderSourceProvider(@Nullable ApplicationContext context, Map matchingBeans) { + this.context = context; + this.instancesToBeanNames = new IdentityHashMap<>(matchingBeans.size()); + matchingBeans.forEach((name, instance) -> this.instancesToBeanNames.put(instance, name)); + } + + @Override + public @Nullable Object getOrderSource(Object obj) { + String beanName = this.instancesToBeanNames.get(obj); + if (beanName == null || !(this.context instanceof ConfigurableApplicationContext cac)) { + return null; + } + try { + BeanDefinition beanDefinition = cac.getBeanFactory().getMergedBeanDefinition(beanName); + List sources = new ArrayList<>(3); + Object orderAttribute = beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + if (orderAttribute != null) { + if (orderAttribute instanceof Integer order) { + sources.add((Ordered) () -> order); + } + else { + throw new IllegalStateException("Invalid value type for attribute '" + + AbstractBeanDefinition.ORDER_ATTRIBUTE + "': " + orderAttribute.getClass().getName()); + } + } + if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { + Method factoryMethod = rootBeanDefinition.getResolvedFactoryMethod(); + if (factoryMethod != null) { + sources.add(factoryMethod); + } + Class targetType = rootBeanDefinition.getTargetType(); + if (targetType != null && targetType != obj.getClass()) { + sources.add(targetType); + } + } + return sources.toArray(); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java index 1486063b0e29..358927ab1e70 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java @@ -88,7 +88,10 @@ public LiteWebJarsResourceResolver(WebJarVersionLocator webJarVersionLocator) { if (path == null) { String webJarResourcePath = findWebJarResourcePath(resourceUrlPath); if (webJarResourcePath != null) { - return chain.resolveUrlPath(webJarResourcePath, locations); + path = chain.resolveUrlPath(webJarResourcePath, locations); + if (path == null && webJarResourcePath.endsWith("/")) { + path = webJarResourcePath; + } } } return path; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java index 1bb17460bacb..818cdfd2b279 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -30,15 +31,19 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; @@ -920,6 +925,96 @@ void shouldResetContentHeadersIfNotCommitted() throws Exception { assertThat(response.getHeaderNames()).doesNotContain(HttpHeaders.CONTENT_DISPOSITION); } + @Test // gh-36637 + void detectsHandlerMappingsOrderedByBeanDefinitionOrderAttribute() throws Exception { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.setServletContext(getServletContext()); + + RootBeanDefinition first = new RootBeanDefinition(StubHandlerMapping.class); + first.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 1); + context.registerBeanDefinition("first", first); + + RootBeanDefinition second = new RootBeanDefinition(StubHandlerMapping.class); + second.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 2); + context.registerBeanDefinition("second", second); + + DispatcherServlet servlet = new DispatcherServlet(context); + servlet.init(servletConfig); + + List mappings = servlet.getHandlerMappings(); + assertThat(mappings).isNotNull().hasSize(2); + assertThat(mappings.get(0)).isSameAs(context.getBean("first")); + assertThat(mappings.get(1)).isSameAs(context.getBean("second")); + } + + @Test // gh-36637 + void beanDefinitionOrderAttributeOverridesAnnotationOrderForHandlerMappings() throws Exception { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.setServletContext(getServletContext()); + + // Without an ORDER_ATTRIBUTE override this bean's @Order(1) would put it first. + RootBeanDefinition annotated = new RootBeanDefinition(LowOrderAnnotatedHandlerMapping.class); + annotated.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 100); + context.registerBeanDefinition("annotatedButOverridden", annotated); + + RootBeanDefinition viaAttribute = new RootBeanDefinition(StubHandlerMapping.class); + viaAttribute.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 1); + context.registerBeanDefinition("orderedByAttribute", viaAttribute); + + DispatcherServlet servlet = new DispatcherServlet(context); + servlet.init(servletConfig); + + List mappings = servlet.getHandlerMappings(); + assertThat(mappings).isNotNull().hasSize(2); + assertThat(mappings.get(0)).isSameAs(context.getBean("orderedByAttribute")); + assertThat(mappings.get(1)).isSameAs(context.getBean("annotatedButOverridden")); + } + + @Test // gh-36637 + void nonIntegerOrderAttributeIsRejected() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.setServletContext(getServletContext()); + + RootBeanDefinition invalid = new RootBeanDefinition(StubHandlerMapping.class); + invalid.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, "not-an-integer"); + context.registerBeanDefinition("invalid", invalid); + + // A second bean is required to actually trigger sorting. + RootBeanDefinition valid = new RootBeanDefinition(StubHandlerMapping.class); + valid.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 1); + context.registerBeanDefinition("valid", valid); + + DispatcherServlet servlet = new DispatcherServlet(context); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> servlet.init(servletConfig)) + .withMessageContaining("Invalid value type for attribute 'order'"); + } + + @Test // gh-36637 + void handlerMappingOrderFromBeanDefinitionInheritsAcrossParentContext() throws Exception { + StaticWebApplicationContext parent = new StaticWebApplicationContext(); + parent.setServletContext(getServletContext()); + RootBeanDefinition fromParent = new RootBeanDefinition(StubHandlerMapping.class); + fromParent.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 5); + parent.registerBeanDefinition("fromParent", fromParent); + parent.refresh(); + + StaticWebApplicationContext child = new StaticWebApplicationContext(); + child.setServletContext(getServletContext()); + child.setParent(parent); + RootBeanDefinition fromChild = new RootBeanDefinition(StubHandlerMapping.class); + fromChild.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, 1); + child.registerBeanDefinition("fromChild", fromChild); + + DispatcherServlet servlet = new DispatcherServlet(child); + servlet.init(servletConfig); + + List mappings = servlet.getHandlerMappings(); + assertThat(mappings).isNotNull().hasSize(2); + assertThat(mappings.get(0)).isSameAs(child.getBean("fromChild")); + assertThat(mappings.get(1)).isSameAs(parent.getBean("fromParent")); + } + public static class ControllerFromParent implements Controller { @@ -982,4 +1077,15 @@ public ModelAndView handleRequest(HttpServletRequest request, HttpServletRespons } } + private static class StubHandlerMapping implements HandlerMapping { + @Override + public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) { + return null; + } + } + + @Order(1) + private static class LowOrderAnnotatedHandlerMapping extends StubHandlerMapping { + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java index 101c0630692d..724ab79d87af 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java @@ -26,6 +26,8 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -74,8 +76,8 @@ void resolveUrlExistingNotInJarFile() { @Test void resolveUrlWebJarResource() { - String file = "underscorejs/underscore.js"; - String expected = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expected = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null); given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected); @@ -86,10 +88,24 @@ void resolveUrlWebJarResource() { verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); } + @Test + void resolveUrlWebJarDirectory() { + String folder = "momentjs/locale/"; + String expected = "momentjs/2.29.4/locale/"; + given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(null); + given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(null); + + String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain); + + assertThat(actual).isEqualTo(expected); + verify(this.chain, times(1)).resolveUrlPath(folder, this.locations); + verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); + } + @Test void resolveUrlWebJarResourceNotFound() { - String file = "something/something.js"; - given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null); + String file = "momentjs/locale/unknown.js"; + given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(null); String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain); @@ -125,8 +141,8 @@ void resolveResourceNotFound() { @Test void resolveResourceWebJar() { Resource expected = mock(); - String file = "underscorejs/underscore.js"; - String expectedPath = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expectedPath = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveResource(this.request, expectedPath, this.locations)).willReturn(expected); Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);