diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js index eb6702362d..b9ecd51175 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/service-version-compare.cy.js @@ -38,7 +38,7 @@ describe('>>> Service version compare Test', () => { 'exist' ); - const expectedServicesCount = 17; + const expectedServicesCount = 18; cy.get('div.MuiTabs-flexContainer.MuiTabs-flexContainerVertical') // Select the parent div .find('a.MuiTab-root') // Find all the anchor elements within the div diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java index b92af1420d..848149d9f2 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/routing/transform/TransformService.java @@ -40,10 +40,10 @@ public class TransformService { /** * Construct the URL using gateway hostname and route * - * @param type the type of the route - * @param serviceId the service id - * @param serviceUrl the service URL - * @param routes the routes + * @param type the type of the route + * @param serviceId the service id + * @param serviceUrl the service URL + * @param routes the routes * @param httpsScheme https scheme flag * @return the new URL * @throws URLTransformationException if the path of the service URL is not valid @@ -59,7 +59,7 @@ public String transformURL(ServiceType type, } URI serviceUri = URI.create(serviceUrl); - String serviceUriPath = serviceUri.getPath(); + String serviceUriPath = serviceUri.getRawPath(); if (serviceUriPath == null) { String message = String.format("The URI %s is not valid.", serviceUri); throw new URLTransformationException(message); @@ -71,40 +71,61 @@ public String transformURL(ServiceType type, throw new URLTransformationException(message); } - if (serviceUri.getQuery() != null) { - serviceUriPath += "?" + serviceUri.getQuery(); + if (StringUtils.isNotBlank(serviceUri.getRawQuery())) { + serviceUriPath += "?" + serviceUri.getRawQuery(); } return transformURL(serviceId, serviceUriPath, route, httpsScheme, serviceUri); } - public String transformURL(String serviceId, - String serviceUriPath, - RoutedService route, - boolean httpsScheme, - URI originalUri + private String transformURL(String serviceId, + String serviceUriPath, + RoutedService route, + boolean httpsScheme, + URI originalUri ) throws URLTransformationException { - if (!gatewayClient.isInitialized()) { - apimlLog.log("org.zowe.apiml.common.gatewayNotFoundForTransformRequest"); - throw new URLTransformationException("Gateway not found yet, transform service cannot perform the request"); - } - String endPoint = getShortEndPoint(route.getServiceUrl(), serviceUriPath); + String endPoint = getShortEndpoint(route.getServiceUrl(), serviceUriPath); if (!endPoint.isEmpty() && !endPoint.startsWith("/")) { throw new URLTransformationException("The path " + originalUri.getPath() + " of the service URL " + originalUri + " is not valid."); } ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties(); - + if (originalUri != null && originalUri.toString().startsWith("//")) { + return String.format("//%s/%s%s%s", + gatewayConfigProperties.getHostname(), + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endPoint); + } String scheme = httpsScheme ? "https" : gatewayConfigProperties.getScheme(); - return String.format("%s://%s/%s/%s%s", + return String.format("%s://%s/%s%s%s", scheme, gatewayConfigProperties.getHostname(), serviceId, - route.getGatewayUrl(), + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), endPoint); } + public String transformAbsoluteURL(String serviceId, + String locationUri, + RoutedService route + ) throws URLTransformationException { + + String endpoint = getShortEndpoint(route.getServiceUrl(), locationUri); + if (isRelative(endpoint)) { + throw new URLTransformationException("The path " + locationUri + " of the service " + serviceId + " is not valid."); + } + return String.format("/%s%s%s", + serviceId, + StringUtils.isEmpty(route.getGatewayUrl()) ? "" : "/" + route.getGatewayUrl(), + endpoint); + } + + boolean isRelative(String endpoint) { + return !endpoint.isEmpty() && !endpoint.startsWith("/"); + } + /** * Construct the API base path using the route * @@ -146,7 +167,7 @@ public String retrieveApiBasePath(String serviceId, * @param endPoint the endpoint of method * @return short endpoint */ - private String getShortEndPoint(String routeServiceUrl, String endPoint) { + private String getShortEndpoint(String routeServiceUrl, String endPoint) { String shortEndPoint = endPoint; if (!SEPARATOR.equals(routeServiceUrl) && StringUtils.isNotBlank(routeServiceUrl)) { shortEndPoint = shortEndPoint.replaceFirst(UrlUtils.removeLastSlash(routeServiceUrl), ""); diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java index c98866d014..b41ff2c72f 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java +++ b/apiml-common/src/test/java/org/zowe/apiml/product/routing/transform/TransformServiceTest.java @@ -28,293 +28,192 @@ class TransformServiceTest { private static final String UI_PREFIX = "ui"; private static final String API_PREFIX = "api"; private static final String WS_PREFIX = "ws"; - private static final String SERVICE_ID = "service"; private GatewayClient gatewayClient; + private TransformService transformService; @BeforeEach void setup() { - ServiceAddress gatewayConfigProperties = ServiceAddress.builder() + ServiceAddress gatewayConfig = ServiceAddress.builder() .scheme("https") - .hostname("localhost") + .hostname("localhost:10010") .build(); - gatewayClient = new GatewayClient(gatewayConfigProperties); - } - - @Test - void givenHomePageAndUIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ui"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); + gatewayClient = new GatewayClient(gatewayConfig); + transformService = new TransformService(gatewayClient); } - @Test - void givenHomePageWithAttlsEnabled_whenTransform_thenUseNewUrlWithHttps() throws URLTransformationException { - String url = "http://localhost:8080/ui"; + private RoutedServices buildRoutedServices(RoutedService... routes) { RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, true); - - String expectedUrl = String.format("%s://%s/%s/%s", - "https", - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX); - assertEquals(expectedUrl, actualUrl); - } - - @Test - void givenHomePage_whenRouteNotFound_thenThrowException() { - String url = "https://localhost:8080/u"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - }); - assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", exception.getMessage()); + for (RoutedService route : routes) { + routedServices.addRoutedService(route); + } + return routedServices; } - @Test - void givenHomePageAndWSRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/ws"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", + private String expectedUrl(String serviceId, String prefix, String path) { + return String.format("%s://%s/%s/%s%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX); - assertEquals(expectedUrl, actualUrl); + serviceId, + prefix, + path == null ? "" : path + ); } - @Test - void givenHomePageAndAPIRoute_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/api"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, API_PREFIX, "/api"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); - - TransformService transformService = new TransformService(gatewayClient); - String actualUrl = transformService.transformURL(ServiceType.API, SERVICE_ID, url, routedServices, false); - - String expectedUrl = String.format("%s://%s/%s/%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - API_PREFIX); - assertEquals(expectedUrl, actualUrl); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/ui,UI,ui,''", + "https://localhost:8080/ws,WS,ws,''", + "https://localhost:8080/api,API,api,''", + "https://locahost:8080/ui/service/login.do?action=secure,UI,ui,'/login.do?action=secure'" + }) + void givenValidRoutes_whenTransform_thenReturnExpectedUrl( + String url, ServiceType type, String prefix, String extraPath + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + + String actual = transformService.transformURL(type, SERVICE_ID, url, routes, false); + assertEquals(expectedUrl(SERVICE_ID, prefix, extraPath), actual); } - @Test - void givenInvalidHomePage_thenThrowException() { - String url = "https:localhost:8080/wss"; - - TransformService transformService = new TransformService(gatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + void givenSchemeRelativeUrl_whenTransform_thenKeepSchemeRelativeUrl() throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, "api", "/api"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + String actual = transformService.transformURL(ServiceType.API, SERVICE_ID, "//localhost:8080/api", routes, false); + assertEquals("//localhost:10010/service/api", actual); } - @Test - void givenEmptyGatewayClient_thenThrowException() { - String url = "https://localhost:8080/wss"; - - GatewayClient emptyGatewayClient = new GatewayClient(null); - TransformService transformService = new TransformService(emptyGatewayClient); - - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(null, null, url, null, false); - }); - assertEquals("Gateway not found yet, transform service cannot perform the request", exception.getMessage()); + @ParameterizedTest + @CsvSource({ + "https://localhost:8080/,WS,ws,'/'", + "https://locahost:8080/test,UI,ui,'/test'", + }) + void givenMissingRoutes_whenTransform_thenThrow( + String url, ServiceType type, String prefix, String extraPath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, prefix, url.contains("/ui/service") ? "/ui/service" : "/" + prefix), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); + assertThrows(URLTransformationException.class, () -> transformService.transformURL(type, SERVICE_ID, url, routes, false)); } - @Test - void givenHomePage_whenPathIsNotValid_thenThrowException() { - String url = "https://localhost:8080/wss"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenHttpUrlAndAttlsEnabled_whenTransform_thenForceHttps() throws URLTransformationException { + String url = "http://localhost:8080/ui"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); + String actual = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, true); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - }); - assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", exception.getMessage()); + assertEquals("https://localhost:10010/service/ui", actual); } @Test - void givenEmptyPathInHomePage_whenTransform_thenUseNewUrl() throws URLTransformationException { - String url = "https://localhost:8080/"; - - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, WS_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenRouteNotFound_whenTransform_thenThrowException() { + String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routes, false)); - String actualUrl = transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - WS_PREFIX, - "/"); - assertEquals(expectedUrl, actualUrl); + assertEquals("Not able to select route for url https://localhost:8080/u of the service service. Original url used.", + ex.getMessage()); } @Test - void givenServiceUrl_whenItsRoot_thenKeepHomePagePathSame() throws URLTransformationException { - String url = "https://locahost:8080/test"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenInvalidUrl_whenTransform_thenThrowException() { + String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(gatewayClient); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, url, null, false)); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - "/test"); - assertEquals(expectedUrl, actualUrl); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenUrlContainingPathAndQuery_whenTransform_thenKeepQueryPartInTheNewUrl() throws URLTransformationException { - String url = "https://locahost:8080/ui/service/login.do?action=secure"; - String path = "/login.do?action=secure"; - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui/service"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + void givenEmptyGatewayClient_whenTransform_thenThrowException() { + GatewayClient emptyClient = new GatewayClient(null); + TransformService service = new TransformService(emptyClient); - TransformService transformService = new TransformService(gatewayClient); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.transformURL(null, null, "https://localhost:8080/wss", null, false)); - String actualUrl = transformService.transformURL(ServiceType.UI, SERVICE_ID, url, routedServices, false); - String expectedUrl = String.format("%s://%s/%s/%s%s", - gatewayClient.getGatewayConfigProperties().getScheme(), - gatewayClient.getGatewayConfigProperties().getHostname(), - SERVICE_ID, - UI_PREFIX, - path); - assertEquals(expectedUrl, actualUrl); + assertEquals("Gateway not found yet, transform service cannot perform the request", ex.getMessage()); } @Test - void givenServiceAndApiRoute_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s", SERVICE_ID, API_PREFIX); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX, serviceUrl); - routedServices.addRoutedService(routedService); + void givenInvalidPath_whenTransform_thenThrowException() { + String url = "https://localhost:8080/wss"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, WS_PREFIX, "/ws"), + new RoutedService(SERVICE_ID, "api/v1", "/") + ); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> transformService.transformURL(ServiceType.WS, SERVICE_ID, url, routes, false)); - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s", - SERVICE_ID, - API_PREFIX); - assertEquals(expectedPath, actualPath); + assertEquals("The path /wss of the service URL https://localhost:8080/wss is not valid.", ex.getMessage()); } - @Test - void givenServiceAndApiRouteWithVersion_whenGetApiBasePath_thenReturnApiPath() throws URLTransformationException { - String url = "https://localhost:8080/" + SERVICE_ID + "/" + API_PREFIX + "/v1"; - - String serviceUrl = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "v1"); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService = new RoutedService(SERVICE_ID, API_PREFIX + "/v1", serviceUrl); - routedServices.addRoutedService(routedService); + @ParameterizedTest + @CsvSource({ + // url, routePath, expected + "https://localhost:8080/service/api/v1,/service/api,/service/api/{api-version}", + "https://localhost:8080/service/api/v1,/service/api/v1,/service/api/{api-version}" + }) + void givenApiRoute_whenRetrieveApiBasePath_thenReturnExpected( + String url, String routePath, String expected + ) throws URLTransformationException { + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, API_PREFIX + "/v1", routePath) + ); - TransformService transformService = new TransformService(null); + TransformService service = new TransformService(null); + String actual = service.retrieveApiBasePath(SERVICE_ID, url, routes); - String actualPath = transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - String expectedPath = String.format("/%s/%s/%s", SERVICE_ID, API_PREFIX, "{api-version}"); - assertEquals(expectedPath, actualPath); + assertEquals(expected, actual); } @Test - void givenInvalidUriPath_whenGetApiBasePath_thenThrowError() { + void givenInvalidUrl_whenRetrieveApiBasePath_thenThrowException() { String url = "https:localhost:8080/wss"; + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(null, url, null)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(null, url, null); - }); - assertEquals("The URI " + url + " is not valid.", exception.getMessage()); + assertEquals("The URI " + url + " is not valid.", ex.getMessage()); } @Test - void givenNoRoutes_whenGetApiBasePath_thenThrowError() { + void givenNoMatchingRoute_whenRetrieveApiBasePath_thenThrowException() { String url = "https://localhost:8080/u"; + RoutedServices routes = buildRoutedServices( + new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"), + new RoutedService(SERVICE_ID, "api/v1", "/api") + ); - RoutedServices routedServices = new RoutedServices(); - RoutedService routedService1 = new RoutedService(SERVICE_ID, UI_PREFIX, "/ui"); - RoutedService routedService2 = new RoutedService(SERVICE_ID, "api/v1", "/api"); - routedServices.addRoutedService(routedService1); - routedServices.addRoutedService(routedService2); + TransformService service = new TransformService(null); - TransformService transformService = new TransformService(null); + URLTransformationException ex = assertThrows(URLTransformationException.class, + () -> service.retrieveApiBasePath(SERVICE_ID, url, routes)); - Exception exception = assertThrows(URLTransformationException.class, () -> { - transformService.retrieveApiBasePath(SERVICE_ID, url, routedServices); - }); - assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", exception.getMessage()); + assertEquals("Not able to select API base path for the service " + SERVICE_ID + ". Original url used.", + ex.getMessage()); } @ParameterizedTest @@ -324,16 +223,17 @@ void givenNoRoutes_whenGetApiBasePath_thenThrowError() { "srv,wrong/url,api/v1,api/v1,", "srv,apiV1/home/page.html,api/v1,apiV1,/srv/api/{api-version}" }) - void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath) { - RoutedService route = new RoutedService("api", gatewayUrl, serviceUrl); - - RoutedServices routedServices = new RoutedServices(); - routedServices.addRoutedService(route); - - TransformService transformService = new TransformService(null); + void testRetrieveApiBasePathParameterized( + String serviceId, String url, String gatewayUrl, String serviceUrl, String expectedBasePath + ) { + RoutedServices routes = buildRoutedServices( + new RoutedService("api", gatewayUrl, serviceUrl) + ); + + TransformService service = new TransformService(null); String basePath; try { - basePath = transformService.retrieveApiBasePath(serviceId, url, routedServices); + basePath = service.retrieveApiBasePath(serviceId, url, routes); } catch (URLTransformationException e) { basePath = null; } @@ -341,4 +241,24 @@ void testRetrieveApiBasePath(String serviceId, String url, String gatewayUrl, St assertEquals(expectedBasePath, basePath); } + @Test + void givenLocationUriNotMatchingServiceRoutes_whenTransformAbsoluteURL_thenThrowException() { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var ex = assertThrows(URLTransformationException.class, () -> + transformService.transformAbsoluteURL("dcpassticket", "/apisome-path", route)); + + assertEquals("The path /apisome-path of the service dcpassticket is not valid.", ex.getMessage()); + } + + @Test + void givenMatchingLocation_whenTransformAbsoluteURL_thenReturnAbsoluteURL() throws URLTransformationException { + + RoutedService route = new RoutedService("dcpassticket", "api", "/api"); + + var result = transformService.transformAbsoluteURL("dcpassticket", "/api/v1/some-path", route); + assertEquals("/dcpassticket/api/v1/some-path", result); + } + } diff --git a/config/docker/api-defs/staticclient.yml b/config/docker/api-defs/staticclient.yml index 7df25b9083..a478cb9f72 100644 --- a/config/docker/api-defs/staticclient.yml +++ b/config/docker/api-defs/staticclient.yml @@ -216,6 +216,26 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://mock-services:10013/ # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + authentication: + scheme: zosmf + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: api/v1 + version: 1.0.0 + swaggerUrl: https://mock-services:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 5f16df1b5f..bb778ce0a8 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -217,6 +217,24 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: redirectclient # unique lowercase ID of the service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Redirect client # Title of the service in the API catalog + description: REST API in the mockservices to test redirect on a service without context path. + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:10013/ # scheme://hostname:port/contextPath + homePageRelativeUrl: /api/v1 # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: ui # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: # relativePath that is added to baseUrl of an instance + apiInfo: + - apiId: zowe.apiml.redirectclient + gatewayUrl: ui + version: 1.0.0 + swaggerUrl: https://localhost:10013/zosmf/api/docs + # Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java index 8f28d6b61a..f9659801ea 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactory.java @@ -24,6 +24,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.ServiceType; @@ -73,14 +74,14 @@ public GatewayFilter apply(Config config) { .then(Mono.defer(() -> processNewLocationUrl(exchange, config, instance))); } - private URI getHostUri(ServiceInstance instance) { + private URI getHostAndPortUri(ServiceInstance instance) { return UriComponentsBuilder.newInstance() .host(instance.getHost()) .port(instance.getPort()) .build().toUri(); } - private URI getHostUri(URI uri) { + private URI getHostAndPortUri(URI uri) { return UriComponentsBuilder.newInstance() .host(uri.getHost()) .port(uri.getPort()) @@ -92,18 +93,29 @@ private Optional getInstance(URI locationUri, Optional getHostUri(i).equals(hostUri)).orElse(false)) { + var locationHostAndPortUri = getHostAndPortUri(locationUri); + if (matchesInstance(instance, locationHostAndPortUri)) { return instance; } return discoveryClient.getServices().stream() .map(discoveryClient::getInstances) .flatMap(List::stream) - .filter(i -> hostUri.equals(getHostUri(i))) + .filter(i -> locationHostAndPortUri.equals(getHostAndPortUri(i))) .findFirst(); } + /** + * Compares URI with instance + * + * @param instance + * @param hostAndPortUri + * @return true if URI equals URI from instance + */ + private boolean matchesInstance(Optional instance, URI hostAndPortUri) { + return instance.map(i -> getHostAndPortUri(i).equals(hostAndPortUri)).orElse(false); + } + private String normalizePath(String path) { if (!path.startsWith(SLASH)) { path = SLASH + path; @@ -116,7 +128,7 @@ private String normalizePath(String path) { private boolean isMatching(RoutedService route, URI uri) { var servicePath = normalizePath(route.getServiceUrl()); - var locationPath = normalizePath(uri.getPath()); + var locationPath = normalizePath(uri.getRawPath()); return locationPath.startsWith(servicePath); } @@ -129,24 +141,28 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf var location = response.getHeaders().getFirst(HttpHeaders.LOCATION); if (StringUtils.isBlank(location)) { + log.debug("Location header is empty"); return Mono.empty(); } var locationUri = URI.create(location); var targetInstance = getInstance(locationUri, instance); + if (isGateway(targetInstance)) { + log.debug("Target instance is Gateway. Location header was not translated. {}", locationUri); + return Mono.empty(); + } var defaultRoute = config.getRoutedService(); AtomicReference newUrl = new AtomicReference<>(); if (targetInstance == instance && isMatching(defaultRoute, locationUri)) { // try the preferable route on the same instance (the same as in the original request) try { - newUrl.set(transformService.transformURL( + newUrl.set(transformService.transformAbsoluteURL( StringUtils.toRootLowerCase(config.serviceId), - UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getQuery()).build().toUri().toString(), - defaultRoute, - false, - locationUri + UriComponentsBuilder.fromPath(locationUri.getPath()).query(locationUri.getRawQuery()).build().toUriString(), + defaultRoute )); + log.debug("Location is matching service URL. New Location header value is: {}", newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL on the same route", e); return Mono.empty(); @@ -166,6 +182,7 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf routes, false )); + log.debug("Target instance: {}. New Location header value is: {}", i.getInstanceId(), newUrl.get()); } catch (URLTransformationException e) { log.debug("Cannot transform URL", e); } @@ -174,8 +191,9 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf if (newUrl.get() != null) { // if the new URL was defined, decorate (scheme by AT-TLS) and set - if (isServerAttlsEnabled) { + if (isServerAttlsEnabled && newUrl.get().startsWith("http://")) { newUrl.set(UriComponentsBuilder.fromUriString(newUrl.get()).scheme("https").build().toUriString()); + log.debug("AT-TLS server is enabled. Location url was updated with: {}", newUrl.get()); } exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION, newUrl.toString()); @@ -185,6 +203,11 @@ private Mono processNewLocationUrl(ServerWebExchange exchange, Config conf return Mono.empty(); } + boolean isGateway(Optional targetInstance) { + return targetInstance.filter(target -> CoreService.GATEWAY.getServiceId().equalsIgnoreCase(target.getServiceId())) + .isPresent(); + } + @Data public static class Config { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java index f162bdc576..7dfd34785a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/PageRedirectionFilterFactoryTest.java @@ -14,6 +14,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.gateway.filter.GatewayFilter; @@ -23,6 +26,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.instance.ServiceAddress; import reactor.core.publisher.Mono; @@ -32,6 +36,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -42,6 +47,8 @@ class PageRedirectionFilterFactoryTest { + public static final String DISCOVERABLECLIENT = "DISCOVERABLECLIENT"; + public static final String GATEWAY = CoreService.GATEWAY.toString(); private GatewayClient gatewayClient; private DiscoveryClient discoveryClient; private static final String GW_HOSTNAME = "gateway"; @@ -51,62 +58,82 @@ class PageRedirectionFilterFactoryTest { private final ServiceAddress serviceAddress = ServiceAddress.builder() .scheme(GW_SCHEME).hostname(GW_HOSTNAME + ":" + GW_PORT).build(); + PageRedirectionFilterFactory factory; + GatewayFilterChain chain; + ServerWebExchange exchange; + ServerHttpResponse res; + ServiceInstance serviceInstance; + @BeforeEach void setUp() { gatewayClient = mock(GatewayClient.class); discoveryClient = mock(DiscoveryClient.class); - } + factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - private void commonSetup(PageRedirectionFilterFactory factory, ServerWebExchange exchange, ServerHttpResponse res, GatewayFilterChain chain, boolean isAttlsEnabled) { - ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", isAttlsEnabled); + chain = mock(GatewayFilterChain.class); + exchange = mock(ServerWebExchange.class); + res = mock(ServerHttpResponse.class); + serviceInstance = mock(ServiceInstance.class); + when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); + when(gatewayClient.isInitialized()).thenReturn(true); + when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_MOVED_PERMANENTLY)); when(exchange.getResponse()).thenReturn(res); - when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty()); - when(gatewayClient.isInitialized()).thenReturn(true); - when(gatewayClient.getGatewayConfigProperties()).thenReturn(serviceAddress); } - private PageRedirectionFilterFactory.Config createConfig() { + + private PageRedirectionFilterFactory.Config createConfig(String serviceId) { var config = new PageRedirectionFilterFactory.Config(); - config.setInstanceId("instanceId"); - config.setServiceId("GATEWAY"); + config.setInstanceId("localhost:" + serviceId.toLowerCase() + ":10012"); + config.setServiceId(serviceId); config.setGatewayUrl("api/v1"); - config.setServiceUrl("/"); + config.setServiceUrl("/discoverableclient"); return config; } - private void setupInstanceInfo() { - var serviceInstance = mock(ServiceInstance.class); + private void mockServiceInstance(String serviceId) { Map metadata = new HashMap<>(); metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1"); metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/"); when(serviceInstance.getMetadata()).thenReturn(metadata); when(serviceInstance.getInstanceId()).thenReturn("instanceId"); + when(serviceInstance.getServiceId()).thenReturn(serviceId); when(serviceInstance.getHost()).thenReturn("localhost"); when(serviceInstance.getPort()).thenReturn(10010); - when(discoveryClient.getInstances("GATEWAY")).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getInstances(serviceId)).thenReturn(new ArrayList<>(Collections.singletonList(serviceInstance))); + when(discoveryClient.getServices()).thenReturn(Collections.singletonList(serviceId)); + } + + void mockLocationHeaderResponse(String url) { + var header = new HttpHeaders(); + header.put(HttpHeaders.LOCATION, Collections.singletonList(url)); + when(res.getHeaders()).thenReturn(header); } @Nested class GivenValidUrl { - @Test - void whenNoAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); + static Stream locationUrls() { + return Stream.of( + Arguments.of("/discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "https://localhost:10010/discoverableclient/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", "discoverableclient/api/v1/login?redirected_url=%2Fsome%2Fpath", false), + Arguments.of("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/api/v1/redirected_url?arg=1&arg=2", true), + Arguments.of("%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", true), + Arguments.of("api/request", "api/request", true), + Arguments.of("//localhost:10010/api/v1/request", "//localhost:10010/api/v1/request", true), + Arguments.of("/discoverableclient/api/v1/api/v1/redirected_url?arg=1&arg=2", "http://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2", true) + + ); + } - header.put(HttpHeaders.LOCATION, Collections.singletonList("https://localhost:10010/api/v1/redirected_url")); - when(res.getHeaders()).thenReturn(header); + @ParameterizedTest + @MethodSource(value = "locationUrls") + void whenNoAttls_thenAddRedirectionUrl(String expectedUrl, String originalUrl, boolean attlsEnabled) { + ReflectionTestUtils.setField(factory, "isServerAttlsEnabled", attlsEnabled); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); - var config = createConfig(); + mockLocationHeaderResponse(originalUrl); + var config = createConfig(DISCOVERABLECLIENT); GatewayFilter gatewayFilter = factory.apply(config); StepVerifier.create(gatewayFilter.filter(exchange, chain)) @@ -117,23 +144,19 @@ void whenNoAttls_thenAddRedirectionUrl() { } @Test - void whenAttls_thenAddRedirectionUrl() { - var expectedUrl = GW_BASE_URL + "/gateway/api/v1/api/v1/redirected_url?arg=1&arg=2"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList("http://localhost:10010/api/v1/redirected_url?arg=1&arg=2")); - when(res.getHeaders()).thenReturn(header); - - commonSetup(factory, exchange, res, chain, true); - setupInstanceInfo(); - var config = createConfig(); - - StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); - assertEquals(expectedUrl, res.getHeaders().getFirst(HttpHeaders.LOCATION)); + void whenTargetInstanceIsGateway_thenDoNotUpdate() { + var url = "https://localhost:10010/discoverableclient/api/v1/redirected_url?arg=1&arg=2"; + mockLocationHeaderResponse(url); + var config = createConfig(GATEWAY); + mockServiceInstance(GATEWAY); + GatewayFilter gatewayFilter = factory.apply(config); + StepVerifier.create(gatewayFilter.filter(exchange, chain)) + .thenAwait() + .expectComplete() + .verify(); + assertEquals(url, res.getHeaders().getFirst(HttpHeaders.LOCATION)); } + } @Nested @@ -142,16 +165,10 @@ class GivenMissingGwConfig { @Test void thenDoNotTransform() { var expectedUrl = GW_BASE_URL + "/api/v1/redirected_url"; - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); - var header = new HttpHeaders(); - header.put(HttpHeaders.LOCATION, Collections.singletonList(expectedUrl)); - when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); - setupInstanceInfo(); - var config = createConfig(); + + mockLocationHeaderResponse(expectedUrl); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); when(gatewayClient.isInitialized()).thenReturn(false); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); @@ -163,15 +180,11 @@ void thenDoNotTransform() { class GivenNullUrl { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); - var config = createConfig(); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); @@ -182,18 +195,14 @@ void thenDoNotTransform() { class GivenDifferentResponseStatusCode { @Test void thenDoNotTransform() { - var factory = new PageRedirectionFilterFactory(gatewayClient, discoveryClient); - var chain = mock(GatewayFilterChain.class); - var exchange = mock(ServerWebExchange.class); - var res = mock(ServerHttpResponse.class); + var header = new HttpHeaders(); header.put(HttpHeaders.LOCATION, Collections.emptyList()); when(res.getHeaders()).thenReturn(header); - commonSetup(factory, exchange, res, chain, false); when(res.getStatusCode()).thenReturn(HttpStatusCode.valueOf(HttpStatus.SC_CONTINUE)); - setupInstanceInfo(); - var config = createConfig(); + mockServiceInstance(DISCOVERABLECLIENT); + var config = createConfig(DISCOVERABLECLIENT); StepVerifier.create(factory.apply(config).filter(exchange, chain)).expectComplete().verify(); assertNull(res.getHeaders().getFirst(HttpHeaders.LOCATION)); diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java index e3e6d8f03c..a6d0419331 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/PageRedirectionTest.java @@ -75,7 +75,6 @@ public PageRedirectionTest() throws URISyntaxException { @TestsNotMeantForZowe void apiRouteOfDiscoverableClient() { String location = String.format("%s://%s:%d%s", dcScheme, dcHost, dcPort, DISCOVERABLE_GREET); - String transformedLocation = String.format("%s://%s:%d%s", gatewayScheme, gatewayHost, gatewayPort, STATIC_GREET); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -86,7 +85,7 @@ void apiRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, transformedLocation); + .header(LOCATION, STATIC_GREET); } /** diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java new file mode 100644 index 0000000000..91bcede7f2 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/RedirectTest.java @@ -0,0 +1,139 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.integration.proxy; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import lombok.Data; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; +import org.zowe.apiml.util.config.*; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; + +@DiscoverableClientDependentTest +class RedirectTest { + + static GatewayServiceConfiguration gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + static DiscoverableClientConfiguration dcConf = ConfigReader.environmentConfiguration().getDiscoverableClientConfiguration(); + static String gatewayUrl; + static String dcUrl; + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); + gatewayUrl = String.format("%s:%d", gwConf.getHost(), gwConf.getPort()); + dcUrl = String.format("%s:%d", dcConf.getHost(), dcConf.getPort()); + } + + static Stream headerValues() { + return Stream.of( + Arguments.of( + "relative URL with encoding", + "%2Fapi%2Frequest", + "%2Fapi%2Frequest" + ), + Arguments.of( + "relative URL with encoding and service route", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest" + ), + Arguments.of( + "absolute URL doesn't match service route", "/api/request", "/api/request"), + Arguments.of( + "relative URL", + "api/request", + "api/request"), + Arguments.of( + "absolute URL containing encoded characters matches service route", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "/discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "relative URL that contains service ID", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest", + "discoverableclient/api/v1/login?returnUrl=%2Fapi%2Frequest" + ), + Arguments.of( + "absolute URL matches service route", + "/discoverableclient/api/v1/request", + "/discoverableclient/api/v1/request"), + Arguments.of( + "Full URL contains service host and port", + String.format("https://%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "Full URL contains gateway host and port", + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl), + String.format("https://%s/discoverableclient/api/v3/request", gatewayUrl) + ), + Arguments.of( + "scheme-relative URL contains service host and port", + String.format("//%s/discoverableclient/api/v1/request", dcUrl), + "/discoverableclient/api/v1/request" + ), + Arguments.of( + "scheme-relative URL contains gateway host and port", + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl), + String.format("//%s/discoverableclient/api/v1/request", gatewayUrl) + ) + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("headerValues") + void giveLocationHeaderFromService(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/discoverableclient/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(307); + } + + static Stream urlsWithoutContextPath() { + return Stream.of( + Arguments.of( + "absolute URL matches / as a service route ", + "/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest", + "/redirectclient/ui/common/login?returnUrl=%2Fdiscoverableclient%2Fapi%2Fv1%2Frequest") + ); + } + + @ParameterizedTest(name = "given {0} then Location header value {1} was transform to {2}") + @MethodSource("urlsWithoutContextPath") + void giveLocationHeaderFromServiceWithoutContextPath(String msg, String original, String translated) { + var baseUrl = String.format("%s://%s:%d", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort()); + var targetUrl = baseUrl + "/redirectclient/ui/api/v1/redirect"; + given() + .body(new LocationReq(original)) + .contentType(ContentType.JSON) + .post(targetUrl) + .then().log().ifValidationFails() + .header("Location", translated) + .statusCode(302); + } + + @Data + static class LocationReq { + final String location; + } +} diff --git a/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java new file mode 100644 index 0000000000..9c541082bc --- /dev/null +++ b/mock-services/src/main/java/org/zowe/apiml/client/api/RedirectController.java @@ -0,0 +1,50 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.client.api; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.HttpHeaders.LOCATION; + +@RestController +public class RedirectController { + + /** + * Get url from POST request body, then set the url to Location response header, and set status code to 307 + * + * @param redirectLocation request body which contains a url + * @param response return the same data as request body + * @return + */ + @PostMapping( + value = "/api/v1/redirect", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @ResponseStatus(HttpStatus.FOUND) + public RedirectLocation redirectPage(@RequestBody RedirectLocation redirectLocation, + HttpServletResponse response) { + response.setHeader(LOCATION, redirectLocation.getLocation()); + return redirectLocation; + } + + @Data + static class RedirectLocation { + private String location; + } +}