This is an automated email from the ASF dual-hosted git repository. Lukas-Finster pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
commit df8fcbc9820e7a4392487fddb8fbc8a14c22e2cc Author: Lukas Finster <[email protected]> AuthorDate: Thu Jun 18 14:54:06 2026 +0200 Improved: Added basic JUnit test for relevant rest-api classes and integration test for rest-api services (OFBIZ-13443) --- framework/rest-api/ofbiz-component.xml | 2 - .../apache/ofbiz/ws/rs/test/RestServicesTests.java | 213 +++++++++++++++++++++ .../ofbiz/ws/rs/core/OFBizApiConfigTest.java | 107 +++++++++++ .../ofbiz/ws/rs/core/ResponseStatusTest.java | 70 +++++++ .../ws/rs/listener/ApiContextListenerTest.java | 72 +++++++ .../ofbiz/ws/rs/model/ModelApiReaderTest.java | 174 +++++++++++++++++ .../ws/rs/openapi/OFBizOpenApiReaderTest.java | 185 ++++++++++++++++++ .../ws/rs/openapi/OFBizResourceScannerTest.java | 96 ++++++++++ .../ws/rs/process/RestRequestHandlerTest.java | 168 ++++++++++++++++ .../ofbiz/ws/rs/resources/ApiRootResourceTest.java | 80 ++++++++ .../ofbiz/ws/rs/resources/OpenApiResourceTest.java | 75 ++++++++ .../apache/ofbiz/ws/rs/util/RestApiUtilTest.java | 159 +++++++++++++++ framework/rest-api/testdef/rest-apiTests.xml | 3 + 13 files changed, 1402 insertions(+), 2 deletions(-) diff --git a/framework/rest-api/ofbiz-component.xml b/framework/rest-api/ofbiz-component.xml index ff6785703b..d256e386dd 100644 --- a/framework/rest-api/ofbiz-component.xml +++ b/framework/rest-api/ofbiz-component.xml @@ -30,9 +30,7 @@ under the License. <!-- service resources: model(s), eca(s) and group definitions --> <service-resource type="model" loader="main" location="servicedef/services.xml"/> - <!-- <test-suite loader="main" location="testdef/rest-apiTests.xml"/> - --> <!-- web applications; will be mounted when using the embedded container --> <webapp name="rest-api" diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java new file mode 100644 index 0000000000..c325fc1434 --- /dev/null +++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java @@ -0,0 +1,213 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import org.apache.ofbiz.base.util.UtilMisc; +import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.service.ServiceUtil; +import org.apache.ofbiz.service.testtools.OFBizTestCase; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RestServicesTests extends OFBizTestCase { + + public RestServicesTests(String name) { + super(name); + } + + /** + * Verifies that the {@code generateAuthTokenService} returns a success response + * when called with a valid {@code userLogin} in the service context. + * + * <p>This is the happy-path test — it confirms the service completes without + * error and that {@link ServiceUtil#isSuccess(Map)} returns {@code true}.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenReturnsSuccess() throws Exception { + GenericValue userLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + assertNotNull("admin userLogin must exist in demo data", userLogin); + + Map<String, Object> ctx = UtilMisc.toMap("userLogin", (Object) userLogin); + Map<String, Object> result = getDispatcher().runSync("generateAuthTokenService", ctx); + + assertTrue("Service should return success", ServiceUtil.isSuccess(result)); + } + + /** + * Verifies that the {@code access_token} output attribute is present and non-empty + * in the service response. + * + * <p>A null or empty token would indicate that {@code JWTManager.createJwt()} + * failed silently or returned an unexpected value.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenAccessTokenPresent() throws Exception { + GenericValue userLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + + Map<String, Object> result = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) userLogin)); + + String accessToken = (String) result.get("access_token"); + assertNotNull("access_token should not be null", accessToken); + assertFalse("access_token should not be empty", accessToken.isEmpty()); + } + + /** + * Verifies that the {@code token_type} output attribute is exactly {@code "Bearer"}. + * + * <p>REST clients rely on this value to correctly construct the + * {@code Authorization: Bearer <token>} header. Any deviation would break + * standard OAuth2 / JWT bearer token flows.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenTokenTypeIsBearer() throws Exception { + GenericValue userLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + + Map<String, Object> result = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) userLogin)); + + assertEquals("token_type should be Bearer", "Bearer", result.get("token_type")); + } + + /** + * Verifies that the {@code expires_in} output attribute is present and represents + * a valid positive integer (in seconds). + * + * <p>The value is read from the {@code security.jwt.token.expireTime} property + * in {@code security.properties}. This test guards against a missing property + * file entry or a non-numeric value being returned.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly, + * or if {@code expires_in} cannot be parsed as an integer + */ + public void testGenerateAuthTokenExpiresInIsValid() throws Exception { + GenericValue userLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + + Map<String, Object> result = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) userLogin)); + + String expiresIn = (String) result.get("expires_in"); + assertNotNull("expires_in should not be null", expiresIn); + + int expiresCurrent = Integer.parseInt(expiresIn); + assertTrue("expires_in should be a positive number", expiresCurrent > 0); + + int expiresTarget = 1800; + assertTrue("expires_in should match the configured amount", expiresCurrent == expiresTarget); + } + + /** + * Verifies that the {@code access_token} is a well-formed JWT by checking + * it consists of exactly three Base64-encoded parts separated by dots + * ({@code header.payload.signature}). + * + * <p>This does not validate the cryptographic signature — it confirms that + * {@code JWTManager.createJwt()} produced a structurally valid token that + * JWT libraries and API clients will be able to parse.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenTokenIsValidJwtFormat() throws Exception { + GenericValue userLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + + Map<String, Object> result = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) userLogin)); + + String accessToken = (String) result.get("access_token"); + String[] parts = accessToken.split("\\."); + assertEquals("JWT should have 3 parts (header.payload.signature)", 3, parts.length); + } + + /** + * Verifies that two different users receive different tokens. + * + * <p>Since the {@code userLoginId} is embedded in the JWT payload, tokens + * generated for different users must not be identical. Identical tokens would + * indicate that the {@code userLogin} is not being read from context correctly, + * allowing one user to authenticate as another.</p> + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenDifferentUsersGetDifferentTokens() throws Exception { + GenericValue adminLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + GenericValue systemLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "system"), false); + + assertNotNull("admin userLogin must exist", adminLogin); + assertNotNull("system userLogin must exist", systemLogin); + + Map<String, Object> adminResult = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) adminLogin)); + + Map<String, Object> systemResult = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) systemLogin)); + + assertFalse( + "Different users should receive different tokens", + adminResult.get("access_token").equals(systemResult.get("access_token"))); + } + + /** + * Verifies JWT payload hast correct issuer and userLoginId. + * + * @throws Exception if the service call or entity lookup fails unexpectedly + */ + public void testGenerateAuthTokenIssuerAndUserLoginIdInPayload() throws Exception { + GenericValue adminLogin = getDelegator().findOne("UserLogin", UtilMisc.toMap("userLoginId", "admin"), false); + Map<String, Object> adminResult = getDispatcher().runSync( + "generateAuthTokenService", + UtilMisc.toMap("userLogin", (Object) adminLogin)); + String jwt = (String) adminResult.get("access_token"); + + String[] parts = jwt.split("\\."); + String payloadSegment = parts[1]; + byte[] decodedBytes = Base64.getUrlDecoder().decode(padBase64(payloadSegment)); + String jwtDecoded = new String(decodedBytes, StandardCharsets.UTF_8); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode claims = mapper.readTree(jwtDecoded); + + String iss = claims.path("iss").asText(null); + String userLoginId = claims.path("userLoginId").asText(null); + + assertTrue("Issuer is ApacheOFBiz", iss.equals("ApacheOFBiz")); + assertTrue("UserLoginId is admin", userLoginId.equals("admin")); + } + + private static String padBase64(String input) { + int padding = (4 - input.length() % 4) % 4; + StringBuilder sb = new StringBuilder(input); + for (int i = 0; i < padding; i++) sb.append('='); + return sb.toString(); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java new file mode 100644 index 0000000000..2e9dd22b26 --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +class OFBizApiConfigTest { + + @Test + void testBuildCleanPathRemovesDuplicateSlashes() throws Exception { + OFBizApiConfig config = new OFBizApiConfig(); + + Method method = OFBizApiConfig.class.getDeclaredMethod( + "buildCleanPath", + String[].class); + method.setAccessible(true); + + String path = (String) method.invoke( + config, + (Object) new String[] { + "/party/", + "/customers/", + "/{partyId}/" + }); + + assertEquals("party/customers/{partyId}", path); + } + + @Test + void testBuildCleanPathIgnoresNullParts() throws Exception { + OFBizApiConfig config = new OFBizApiConfig(); + + Method method = OFBizApiConfig.class.getDeclaredMethod( + "buildCleanPath", + String[].class); + method.setAccessible(true); + + String path = (String) method.invoke( + config, + (Object) new String[] { + null, + "", + " ", + "/party/" + }); + + assertEquals("party", path); + } + + @Test + void testBuildCleanPathReturnsEmptyStringWhenNoValidParts() throws Exception { + OFBizApiConfig config = new OFBizApiConfig(); + + Method method = OFBizApiConfig.class.getDeclaredMethod( + "buildCleanPath", + String[].class); + method.setAccessible(true); + + String path = (String) method.invoke( + config, + (Object) new String[] { + "", + null, + " " + }); + + assertEquals("", path); + } + + @Test + void testBuildCleanPathWithSinglePart() throws Exception { + OFBizApiConfig config = new OFBizApiConfig(); + + Method method = OFBizApiConfig.class.getDeclaredMethod( + "buildCleanPath", + String[].class); + method.setAccessible(true); + + String path = (String) method.invoke( + config, + (Object) new String[] { + "/services/" + }); + + assertEquals("services", path); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/ResponseStatusTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/ResponseStatusTest.java new file mode 100644 index 0000000000..8cb1837b30 --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/ResponseStatusTest.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.apache.ofbiz.ws.rs.core.ResponseStatus.Custom; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.Response.Status.Family; + +class ResponseStatusTest { + + @Test + void testUnprocessableEntityHasCorrectStatusCode() { + assertEquals(422, Custom.UNPROCESSABLE_ENTITY.getStatusCode()); + } + + @Test + void testUnprocessableEntityHasCorrectReasonPhrase() { + assertEquals("Unprocessable Entity", Custom.UNPROCESSABLE_ENTITY.getReasonPhrase()); + } + + @Test + void testToStringReturnsReasonPhrase() { + assertEquals("Unprocessable Entity", Custom.UNPROCESSABLE_ENTITY.toString()); + } + + @Test + void testFamilyIsClientError() { + // 422 falls in the 4xx range + assertEquals(Family.CLIENT_ERROR, Custom.UNPROCESSABLE_ENTITY.getFamily()); + } + + @Test + void testFromStatusCodeReturnsMatchingEnum() { + Custom result = Custom.fromStatusCode(422); + assertNotNull(result); + assertSame(Custom.UNPROCESSABLE_ENTITY, result); + } + + @Test + void testFromStatusCodeReturnsNullForUnknownStatusCode() { + assertNull(Custom.fromStatusCode(999)); + } + + @Test + void testFromStatusCodeReturnsNullForStandardStatusCode() { + assertNull(Custom.fromStatusCode(404)); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/listener/ApiContextListenerTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/listener/ApiContextListenerTest.java new file mode 100644 index 0000000000..a6f301204b --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/listener/ApiContextListenerTest.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.listener; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.ofbiz.entity.Delegator; +import org.apache.ofbiz.entity.DelegatorFactory; +import org.apache.ofbiz.service.LocalDispatcher; +import org.apache.ofbiz.service.ServiceContainer; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; + +class ApiContextListenerTest { + + @Test + void testContextInitializedWiresDelegatorAndDispatcherUnderCorrectKeys() { + ServletContext servletContext = mock(ServletContext.class); + ServletContextEvent event = mock(ServletContextEvent.class); + when(event.getServletContext()).thenReturn(servletContext); + when(servletContext.getInitParameter("entityDelegatorName")).thenReturn("default"); + when(servletContext.getInitParameter("localDispatcherName")).thenReturn("api-dispatcher"); + + Delegator delegator = mock(Delegator.class); + LocalDispatcher dispatcher = mock(LocalDispatcher.class); + + try (MockedStatic<DelegatorFactory> delegatorFactory = mockStatic(DelegatorFactory.class); + MockedStatic<ServiceContainer> serviceContainer = mockStatic(ServiceContainer.class)) { + delegatorFactory.when(() -> DelegatorFactory.getDelegator("default")).thenReturn(delegator); + serviceContainer.when(() -> ServiceContainer.getLocalDispatcher("api-dispatcher", delegator)).thenReturn(dispatcher); + + new ApiContextListener().contextInitialized(event); + + verify(servletContext).setAttribute("delegator", delegator); + verify(servletContext).setAttribute("dispatcher", dispatcher); + } + } + + @Test + void testContextDestroyedRemovesDelegatorAndDispatcherAttributes() { + ServletContext servletContext = mock(ServletContext.class); + ServletContextEvent event = mock(ServletContextEvent.class); + when(event.getServletContext()).thenReturn(servletContext); + + new ApiContextListener().contextDestroyed(event); + + verify(servletContext).removeAttribute("delegator"); + verify(servletContext).removeAttribute("dispatcher"); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java new file mode 100644 index 0000000000..5cb0f398da --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Integration-style tests for {@link ModelApiReader}. + * + * <p>These tests deliberately parse real, temporary {@code *.rest.xml} files + * rather than mocking {@code UtilXml}. The behavior under test is the + * mapping between XML attribute names and {@link ModelApi} / + * {@link ModelResource} / {@link ModelOperation} fields, which can only be + * verified meaningfully against a real XML document.</p> + */ +class ModelApiReaderTest { + + @TempDir + private File tempDir; + + private File writeXml(String content) throws IOException { + File file = new File(tempDir, "test.rest.xml"); + Files.writeString(file.toPath(), content); + return file; + } + + @Test + void testGetModelApiParsesTopLevelAttributes() throws IOException { + File file = writeXml(""" + <api displayName="My API" name="myApi" description="desc" path="/api" publish="true"/> + """); + + ModelApi api = ModelApiReader.getModelApi(file); + + assertEquals("My API", api.getDisplayName()); + assertEquals("myApi", api.getName()); + assertEquals("desc", api.getDescription()); + assertEquals("/api", api.getPath()); + assertTrue(api.isPublish()); + } + + @Test + void testGetModelApiMissingPublishAttributeDefaultsToFalse() throws IOException { + // No "publish" attribute at all -> UtilXml.checkEmpty(...) yields "", + // and Boolean.parseBoolean("") is false. This locks in that implicit + // default so a change in checkEmpty/parseBoolean behavior is caught. + File file = writeXml("<api name=\"myApi\"/>"); + + ModelApi api = ModelApiReader.getModelApi(file); + + assertFalse(api.isPublish()); + } + + @Test + void testGetModelAPiParsesSingleResourceAttributes() throws IOException { + File file = writeXml(""" + <api name="myApi"> + <resource name="users" description="user resource" displayName="Users" + path="/users" publish="true" auth="true"/> + </api> + """); + + ModelApi api = ModelApiReader.getModelApi(file); + + List<ModelResource> resources = api.getResources(); + assertEquals(1, resources.size()); + + ModelResource users = resources.get(0); + assertEquals("users", users.getName()); + assertEquals("user resource", users.getDescription()); + assertEquals("Users", users.getDisplayName()); + assertEquals("/users", users.getPath()); + assertTrue(users.isPublish()); + assertTrue(users.isAuth()); + } + + @Test + void testGetModelApiParsesNestedResourcesRecursively() throws IOException { + File file = writeXml(""" + <api name="myApi"> + <resource name="users" path="/users"> + <resource name="orders" path="/orders"> + <resource name="items" path="/items"/> + </resource> + </resource> + </api> + """); + + ModelApi api = ModelApiReader.getModelApi(file); + + ModelResource users = api.getResources().get(0); + assertEquals("users", users.getName()); + assertEquals(1, users.getSubResources().size()); + + ModelResource orders = users.getSubResources().get(0); + assertEquals("orders", orders.getName()); + assertEquals(1, orders.getSubResources().size()); + + ModelResource items = orders.getSubResources().get(0); + assertEquals("items", items.getName()); + assertTrue(items.getSubResources().isEmpty()); + } + + @Test + void testGetModelApiParsesOperationsOnAResource() throws IOException { + File file = writeXml(""" + <api name="myApi"> + <resource name="orders" path="/orders"> + <operation path="/{id}" verb="GET" produces="application/json" + consumes="application/json" description="Get an order" auth="true"> + <service name="getOrder"/> + </operation> + </resource> + </api> + """); + + ModelApi api = ModelApiReader.getModelApi(file); + ModelResource orders = api.getResources().get(0); + + List<ModelOperation> operations = orders.getOperations(); + assertEquals(1, operations.size()); + + ModelOperation getOrder = operations.get(0); + assertEquals("/{id}", getOrder.getPath()); + assertEquals("GET", getOrder.getVerb()); + assertEquals("application/json", getOrder.getProduces()); + assertEquals("application/json", getOrder.getConsumes()); + assertEquals("Get an order", getOrder.getDescription()); + assertEquals("getOrder", getOrder.getService()); + assertTrue(getOrder.isAuth()); + } + + @Test + void testGetModelApiThrowsRuntimeExceptionForMalformedXml() throws IOException { + File file = writeXml("<api name=\"broken\"><unclosed>"); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> ModelApiReader.getModelApi(file)); + assertTrue(ex.getMessage().contains("Failed to parse REST API definition")); + } + + @Test + void getModelApiThrowsForNonExistentFile() { + File file = new File(tempDir, "does-not-exist.rest.xml"); + + assertThrows(RuntimeException.class, () -> ModelApiReader.getModelApi(file)); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java new file mode 100644 index 0000000000..c0ff824ac9 --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.openapi; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.ofbiz.service.DispatchContext; +import org.apache.ofbiz.service.LocalDispatcher; +import org.apache.ofbiz.ws.rs.core.OFBizApiConfig; +import org.apache.ofbiz.ws.rs.model.ModelApi; +import org.apache.ofbiz.ws.rs.model.ModelOperation; +import org.apache.ofbiz.ws.rs.model.ModelResource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import jakarta.servlet.ServletContext; + +class OFBizOpenApiReaderTest { + + private OFBizOpenApiReader reader; + + @BeforeEach + void setUp() { + reader = new OFBizOpenApiReader(); + } + + @Test + void testOpenApiIsGenerated() throws Exception { + OpenAPI openAPI = buildOpenApi(); + assertNotNull(openAPI); + } + + @Test + void testPathItemExists() throws Exception { + OpenAPI openAPI = buildOpenApi(); + PathItem pathItem = openAPI.getPaths().get("/api/resource/hello"); + assertNotNull(pathItem); + } + + @Test + void testGetOperationExists() throws Exception { + PathItem pathItem = getPathItem(); + Operation getOp = pathItem.getGet(); + assertNotNull(getOp); + } + + @Test + void testOperationIdMatchesServiceName() throws Exception { + Operation getOp = getGetOperation(); + assertEquals("testService", getOp.getOperationId()); + } + + @Test + void testOperationSummaryMatchesDescription() throws Exception { + Operation getOp = getGetOperation(); + assertEquals("test operation", getOp.getSummary()); + } + + /** + * Verifies that buildNestedUrl correctly joins segments. + */ + @Test + void testBuildNestedUrlShouldNormalizeSegments() { + List<String> segments = List.of("/api/", "/resource/", "hello"); + + String url = OFBizOpenApiReader.buildNestedUrl(segments); + + assertEquals("/api/resource/hello", url); + } + + /** + * Verifies that empty segments are ignored. + */ + @Test + void buildNestedUrlShouldIgnoreEmptySegments() { + List<String> segments = new ArrayList<>(); + segments.add(""); + segments.add("/"); + segments.add("api"); + segments.add(null); + segments.add("test"); + + String url = OFBizOpenApiReader.buildNestedUrl(segments); + + assertEquals("/api/test", url); + } + + /** + * Sets up all mocks (dispatcher, servlet context, static access, API model) + * and runs reader.read(...), returning the resulting OpenAPI object. + */ + @SuppressWarnings("deprecation") + private OpenAPI buildOpenApi() throws Exception { + LocalDispatcher dispatcher = mock(LocalDispatcher.class); + DispatchContext dctx = mock(DispatchContext.class); + when(dispatcher.getDispatchContext()).thenReturn(dctx); + + try (MockedStatic<org.apache.ofbiz.ws.rs.listener.ApiContextListener> ctxMock = + Mockito.mockStatic(org.apache.ofbiz.ws.rs.listener.ApiContextListener.class); + MockedStatic<org.apache.ofbiz.webapp.WebAppUtil> webMock = + Mockito.mockStatic(org.apache.ofbiz.webapp.WebAppUtil.class); + MockedStatic<OFBizApiConfig> apiMock = Mockito.mockStatic(OFBizApiConfig.class)) { + + ServletContext servletContext = mock(ServletContext.class); + + ctxMock.when(org.apache.ofbiz.ws.rs.listener.ApiContextListener::getApplicationCntx) + .thenReturn(servletContext); + + webMock.when(() -> org.apache.ofbiz.webapp.WebAppUtil.getDispatcher(servletContext)) + .thenReturn(dispatcher); + + ModelOperation op = mock(ModelOperation.class); + when(op.getPath()).thenReturn("hello"); + when(op.getVerb()).thenReturn("GET"); + when(op.getService()).thenReturn("testService"); + when(op.getDescription()).thenReturn("test operation"); + + ModelResource resource = mock(ModelResource.class); + when(resource.getPath()).thenReturn("resource"); + when(resource.getDisplayName()).thenReturn("TestResource"); + when(resource.getDescription()).thenReturn("desc"); + when(resource.getOperations()).thenReturn(List.of(op)); + when(resource.getSubResources()).thenReturn(List.of()); + + ModelApi api = mock(ModelApi.class); + when(api.isPublish()).thenReturn(true); + when(api.getPath()).thenReturn("api"); + when(api.getResources()).thenReturn(List.of(resource)); + + apiMock.when(OFBizApiConfig::getModelApis) + .thenReturn(Map.of("test", api)); + + org.apache.ofbiz.service.ModelService service = + mock(org.apache.ofbiz.service.ModelService.class); + when(service.getName()).thenReturn("testService"); + when(service.getInModelParamList()).thenReturn(List.of()); + when(service.isExport()).thenReturn(true); + when(dctx.getModelService("testService")).thenReturn(service); + + return reader.read(Set.of(), Map.of()); + } + } + + private PathItem getPathItem() throws Exception { + OpenAPI openAPI = buildOpenApi(); + PathItem pathItem = openAPI.getPaths().get("/api/resource/hello"); + assertNotNull(pathItem, "Expected path item for /api/resource/hello"); + return pathItem; + } + + private Operation getGetOperation() throws Exception { + Operation getOp = getPathItem().getGet(); + assertNotNull(getOp, "Expected GET operation to be present"); + return getOp; + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizResourceScannerTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizResourceScannerTest.java new file mode 100644 index 0000000000..a40580682c --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizResourceScannerTest.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.openapi; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class OFBizResourceScannerTest { + + private OFBizResourceScanner scanner; + + @BeforeEach + private void setUp() { + scanner = new OFBizResourceScanner(); + } + + /** + * Verifies that a {@code null} input is treated as ignored, + * consistent with {@code UtilValidate.isEmpty} returning {@code true} for null. + */ + @Test + void testIsIgnoredNull() { + assertTrue(scanner.isIgnored(null)); + } + + /** + * Verifies that an empty string is treated as ignored. + */ + @Test + void testIsIgnoredEmptyString() { + assertTrue(scanner.isIgnored("")); + } + + /** + * Verifies that the exact class name in {@code IGNORED} is ignored. + */ + @Test + void testIsIgnoredExactMatch() { + assertTrue(scanner.isIgnored("org.apache.ofbiz.ws.rs.resources.OFBizServiceResource")); + } + + /** + * Verifies that a class name starting with an ignored entry is also ignored. + * Since {@code startsWith} is used, any sub-class or nested path under the + * ignored prefix is treated as ignored. + */ + @Test + void testIsIgnoredPrefixMatch() { + assertTrue(scanner.isIgnored("org.apache.ofbiz.ws.rs.resources.OFBizServiceResource$InnerClass")); + } + + /** + * Verifies that a class name not in {@code IGNORED} and not starting with + * any ignored entry is not ignored. + */ + @Test + void testIsIgnoredNonMatchingClass() { + assertFalse(scanner.isIgnored("org.apache.ofbiz.ws.rs.resources.OFBizOpenApiReader")); + } + + /** + * Verifies that a partial match that is not a prefix is not ignored. + * The ignored entry must match from the start of the string. + */ + @Test + void testIsIgnoredPartialNonPrefixMatch() { + assertFalse(scanner.isIgnored("com.example.OFBizServiceResource")); + } + + /** + * Verifies that a completely unrelated class name is not ignored. + */ + @Test + void testIsIgnoredUnrelatedClass() { + assertFalse(scanner.isIgnored("com.example.SomeOtherResource")); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/process/RestRequestHandlerTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/process/RestRequestHandlerTest.java new file mode 100644 index 0000000000..611d17645d --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/process/RestRequestHandlerTest.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.process; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.glassfish.jersey.server.ContainerRequest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +/** + * Tests for {@link RestRequestHandler#extract(MultivaluedMap)} and + * {@link RestRequestHandler#extractRequestBody(ContainerRequestContext)}. + * + * <p>{@code RestRequestHandler} is abstract and its {@code @Inject} fields + * are not relevant to these two methods, so a minimal concrete subclass is + * used purely to obtain an instance. {@code extract} and + * {@code extractRequestBody} are exercised directly rather than through + * {@code apply(...)}.</p> + */ +class RestRequestHandlerTest { + + /** + * Minimal concrete subclass solely to instantiate the abstract class + * under test. {@code execute} is never invoked by the tests below. + */ + private static final class TestHandler extends RestRequestHandler { + @Override + protected Response execute(ContainerRequestContext data, Map<String, Object> arguments) { + return null; + } + } + + private final TestHandler handler = new TestHandler(); + + + @Test + void testExtractSingleValueParameterIsStoredAsPlainString() { + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + params.putSingle("testParam", "testValue"); + + Map<String, Object> result = handler.extract(params); + + assertEquals("testValue", result.get("testParam")); + } + + @Test + void testExtractMultiValueParameterIsStoredAsList() { + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + params.add("tag", "testValue"); + params.add("tag", "testValueTwo"); + + Map<String, Object> result = handler.extract(params); + + assertEquals(List.of("testValue", "testValueTwo"), result.get("tag")); + } + + @Test + void testExtractSkipsKeysWithEmptyValueList() { + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + params.put("empty", List.of()); + params.putSingle("present", "value"); + + Map<String, Object> result = handler.extract(params); + + assertEquals(1, result.size()); + assertEquals("value", result.get("present")); + assertTrue(!result.containsKey("empty")); + } + + @Test + void testExtractOfEmptyMapReturnsEmptyMap() { + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + Map<String, Object> result = handler.extract(params); + + assertTrue(result.isEmpty()); + } + + + @Test + void testExtractRequestBodyReturnsEmptyMapWhenNotAContainerRequest() { + ContainerRequestContext requestContext = mock(ContainerRequestContext.class); + + Map<String, Object> result = handler.extractRequestBody(requestContext); + + assertTrue(result.isEmpty()); + verifyNoInteractions(requestContext); + } + + @Test + void testExtractRequestBodyReturnsEmptyMapWhenNoEntity() { + ContainerRequest requestContext = mock(ContainerRequest.class); + when(requestContext.hasEntity()).thenReturn(false); + + Map<String, Object> result = handler.extractRequestBody(requestContext); + + assertTrue(result.isEmpty()); + } + + @Test + void testExtractRequestBodyReturnsEmptyMapWhenContentTypeIsNotJson() { + ContainerRequest requestContext = mock(ContainerRequest.class); + when(requestContext.hasEntity()).thenReturn(true); + when(requestContext.getMediaType()).thenReturn(MediaType.TEXT_PLAIN_TYPE); + + Map<String, Object> result = handler.extractRequestBody(requestContext); + + assertTrue(result.isEmpty()); + verify(requestContext, org.mockito.Mockito.never()).readEntity(eq(Map.class)); + } + + @Test + void testExtractRequestBodyReturnsEmptyMapWhenJsonEntityIsNull() { + ContainerRequest requestContext = mock(ContainerRequest.class); + when(requestContext.hasEntity()).thenReturn(true); + when(requestContext.getMediaType()).thenReturn(MediaType.APPLICATION_JSON_TYPE); + when(requestContext.readEntity(eq(Map.class))).thenReturn(null); + + Map<String, Object> result = handler.extractRequestBody(requestContext); + + assertTrue(result.isEmpty()); + } + + @Test + void testExtractRequestBodyReturnsParsedMapForJsonEntity() { + ContainerRequest requestContext = mock(ContainerRequest.class); + when(requestContext.hasEntity()).thenReturn(true); + when(requestContext.getMediaType()).thenReturn(MediaType.APPLICATION_JSON_TYPE); + when(requestContext.readEntity(eq(Map.class))).thenReturn(Map.of("id", "123", "active", true)); + + Map<String, Object> result = handler.extractRequestBody(requestContext); + + assertEquals("123", result.get("id")); + assertEquals(true, result.get("active")); + verify(requestContext).bufferEntity(); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/ApiRootResourceTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/ApiRootResourceTest.java new file mode 100644 index 0000000000..8bf48f37cd --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/ApiRootResourceTest.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.resources; + +import static org.apache.ofbiz.ws.rs.resources.ApiRootResource.joinUri; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ApiRootResourceTest { + + @ParameterizedTest + @CsvSource({ + "base, path", + "base, /path", + "base/, path", + "base/, /path" + }) + void testJoinUriBaseAndPath(String base, String path) { + assertEquals("base/path", joinUri(base, path)); + } + + @ParameterizedTest + @CsvSource({ + "base, /", + "base/, /" + }) + void testJoinUriBase(String base, String path) { + assertEquals("base/", joinUri(base, path)); + } + + @Test + void testJoinUriPreservePathParams() { + assertEquals("base/path.{myParam}", joinUri("base", "/path.{myParam}")); + } + + @Test + void testJoinUriOneArgument() { + assertEquals("base", joinUri("base")); + } + + @Test + void testJoinUriEmpty() { + assertEquals("", joinUri()); + } + + // Current known Limitations, change Tests once these change. + // Ensures that a change to these is a concious decision + + //No full normalization with double '/' -> // + @Test + void testJoinUriDoubleSlash() { + assertEquals("base//path", joinUri("base//", "path")); + } + + // No fault tolerance for null parts + @Test + void testNullPartThrowsNullPointerException() { + assertThrows(NullPointerException.class, () -> joinUri("base", null)); + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java new file mode 100644 index 0000000000..83c4d43ac0 --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.ofbiz.ws.rs.openapi.OFBizOpenApiReader; +import org.apache.ofbiz.ws.rs.openapi.OFBizResourceScanner; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +class OpenApiResourceTest { + + + @SuppressWarnings("try") + @ParameterizedTest + @CsvSource({ + "json, application/json", + "yaml, application/yaml" + }) + void testGetOpenApi(String type, String expectedType) throws Exception { + + // Mock HTTP request context + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getScheme()).thenReturn("http"); + when(request.getServerName()).thenReturn("localhost"); + when(request.getServerPort()).thenReturn(8080); + when(request.getContextPath()).thenReturn("/ofbiz"); + + OpenApiResource resource = new OpenApiResource(); + + // inject request via reflection (since field is @Context) + java.lang.reflect.Field f = OpenApiResource.class.getDeclaredField("request"); + f.setAccessible(true); + f.set(resource, request); + + // Mock heavy Swagger builder + try (MockedConstruction<org.apache.ofbiz.ws.rs.openapi.OFBizOpenApiReader> readerMock = + Mockito.mockConstruction(OFBizOpenApiReader.class, (mock, context) -> { }); + MockedConstruction<OFBizResourceScanner> scannerMock = Mockito.mockConstruction(OFBizResourceScanner.class)) { + + Response response = resource.getOpenApi(mock(HttpHeaders.class), mock(UriInfo.class), type); + + assertEquals(200, response.getStatus()); + assertEquals(expectedType, response.getMediaType().toString()); + assertNotNull(response.getEntity()); + } + } +} diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java new file mode 100644 index 0000000000..679dc99574 --- /dev/null +++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.ofbiz.ws.rs.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.ofbiz.service.ModelService; +import org.apache.ofbiz.ws.rs.response.Error; +import org.apache.ofbiz.ws.rs.response.Success; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +public final class RestApiUtilTest { + + private RestApiUtilTest() { } + + @Test + void testSuccess() { + String message = "Success"; + String data = "some data"; + + Response response = RestApiUtil.success(message, data); + + Success expected = new Success(200, "OK", message, data); + Success actual = (Success) response.getEntity(); + + assertEquals(expected.getStatusCode(), actual.getStatusCode()); + assertEquals(expected.getStatusDescription(), actual.getStatusDescription()); + assertEquals(expected.getSuccessMessage(), actual.getSuccessMessage()); + assertEquals(expected.getData(), actual.getData()); + } + + @Test + void testError() { + String message = "Error"; + String reason = "reason for error"; + + Response response = RestApiUtil.error(400, reason, message); + + Error expected = new Error(400, reason, message); + Error actual = (Error) response.getEntity(); + + assertEquals(expected.getAdditionalErrors(), actual.getAdditionalErrors()); + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getErrorDesc(), actual.getErrorDesc()); + assertEquals(expected.getErrorMessage(), actual.getErrorMessage()); + assertEquals(expected.getStatusCode(), actual.getStatusCode()); + assertEquals(expected.getStatusDescription(), actual.getStatusDescription()); + assertEquals(expected.getType(), actual.getType()); + } + + @Test + void testResponseBuilder() { + String message = "Error"; + String reason = "reason for error"; + + Response response = RestApiUtil.errorBuilder(400, reason, message).build(); + + assertEquals(400, response.getStatus()); + assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString()); + + Error expected = (Error) response.getEntity(); + assertEquals(expected, response.getEntity()); + } + + @Test + void testExtractParams() { + MultivaluedMap<String, String> input = new MultivaluedHashMap<>(); + input.add("name", "John"); + input.add("role", "admin"); + input.add("role", "user"); + input.put("empty", List.of()); + input.put("null", null); + + Map<String, Object> result = RestApiUtil.extractParams(input); + + // Assert single-value parameter + assertEquals("John", result.get("name")); + assertTrue(result.get("name") instanceof String); + + // Assert multi-value parameter + assertTrue(result.get("role") instanceof List); + + @SuppressWarnings("unchecked") + List<String> roles = (List<String>) result.get("role"); + assertEquals(List.of("admin", "user"), roles); + + // Assert empty parameter is ignored + assertFalse(result.containsKey("empty")); + + // Assert null parameter is ignored + assertFalse(result.containsKey("null")); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = { "my/path/without/parameters", ""}) + void testGetPathParametersReturnEmptyList(String pathInfo) { + List<String> pathParameters = RestApiUtil.getPathParameters(pathInfo); + assertTrue(pathParameters.isEmpty()); + } + + @Test + void testGetPathParameters() { + String pathInfo = "my/path/with/parameters/{parameterOne}/middle/and/end/{parameterTwo}"; + + List<String> pathParameters = RestApiUtil.getPathParameters(pathInfo); + + assertTrue(pathParameters.contains("parameterOne")); + assertTrue(pathParameters.contains("parameterTwo")); + } + + @Test + void testBuildErrorFromServiceResult() { + Map<String, Object> result = new HashMap<>(); + + List<String> errors = new LinkedList<>(List.of("errorOne", "errorTwo", "errorThree")); + result.put(ModelService.ERROR_MESSAGE_LIST, errors); + + Response response = + RestApiUtil.buildErrorFromServiceResult("TestService", result, Locale.ENGLISH); + + Error error = (Error) response.getEntity(); + + assertEquals("errorOne", error.getErrorDesc()); + assertEquals(List.of("errorTwo", "errorThree"), error.getAdditionalErrors()); + } +} diff --git a/framework/rest-api/testdef/rest-apiTests.xml b/framework/rest-api/testdef/rest-apiTests.xml index 48e0630b15..3cb93145f5 100644 --- a/framework/rest-api/testdef/rest-apiTests.xml +++ b/framework/rest-api/testdef/rest-apiTests.xml @@ -22,5 +22,8 @@ under the License. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/test-suite.xsd"> + <test-case case-name="rest-api-services-tests"> + <junit-test-suite class-name="org.apache.ofbiz.ws.rs.test.RestServicesTests"/> + </test-case> </test-suite> \ No newline at end of file

