From cd965fb157403f5e893c1d610df98cdf295bdadd Mon Sep 17 00:00:00 2001 From: sofi2002sofi Date: Mon, 27 Jul 2026 17:59:36 +0000 Subject: [PATCH 1/2] CARDS-2154: Increase test coverage of cards-utils --- modules/utils/pom.xml | 100 +++- .../ResourceToJsonAdapterFactory.java | 6 +- .../scripting/ContentTypeSetterTest.java | 95 ++++ .../cards/scripting/StatusCodeSetterTest.java | 154 ++++++ .../cards/serialize/CSVStringTest.java | 53 ++ .../ResourceToCSVAdapterFactoryTest.java | 137 +++++ .../ResourceToJsonAdapterFactoryTest.java | 493 ++++++++++++++++++ .../ResourceToMarkdownAdapterFactoryTest.java | 137 +++++ .../ResourceToTextAdapterFactoryTest.java | 157 ++++++ .../serialize/internal/BareProcessorTest.java | 411 +++++++++++++++ .../serialize/internal/DeepProcessorTest.java | 104 ++++ .../internal/DereferenceProcessorTest.java | 317 +++++++++++ .../internal/IdentificationProcessorTest.java | 150 ++++++ .../internal/PropertiesProcessorTest.java | 104 ++++ .../internal/SimpleProcessorTest.java | 139 +++++ .../DenyScriptsSlingPostProcessorTest.java | 197 +++++++ .../src/test/resources/Questionnaires.json | 10 + .../src/test/resources/SubjectTypes.json | 18 + 18 files changed, 2778 insertions(+), 4 deletions(-) create mode 100644 modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java create mode 100644 modules/utils/src/test/resources/Questionnaires.json create mode 100644 modules/utils/src/test/resources/SubjectTypes.json diff --git a/modules/utils/pom.xml b/modules/utils/pom.xml index 20e17644b5..b3da6a2b37 100644 --- a/modules/utils/pom.xml +++ b/modules/utils/pom.xml @@ -31,7 +31,7 @@ CARDS - Utilities - 0.04 + 0.45 @@ -56,6 +56,17 @@ + + maven-compiler-plugin + + + -Werror + + true + true + true + + @@ -105,14 +116,97 @@ jakarta.servlet-api - io.uhndata.cards + org.apache.jackrabbit + oak-jackrabbit-api + + + org.apache.jackrabbit + oak-api + test + + + ${project.groupId} + cards-data-model-items-api + ${project.version} + runtime + + + ${project.groupId} + cards-data-model-links-api + ${project.version} + runtime + + + ${project.groupId} + cards-data-model-resources-api + ${project.version} + runtime + + + ${project.groupId} cards-data-model-forms-api ${project.version} - + + ${project.groupId} + cards-data-model-subjects-api + ${project.version} + runtime + + + org.apache.sling + org.apache.sling.resourcebuilder + 1.0.4 + test + + + org.apache.sling + org.apache.sling.testing.sling-mock.core + 3.2.2 + test + + + org.apache.sling + org.apache.sling.jcr.resource + 3.2.0 + test + + + org.apache.sling + org.apache.sling.testing.sling-mock.junit4 + 3.2.2 + test + + + org.apache.sling + org.apache.sling.testing.sling-mock-oak + 3.1.4-1.40.0 + test + + + org.apache.sling + org.apache.sling.testing.jcr-mock + 1.5.4 + test + + + com.google.guava + guava + test + junit junit + + org.mockito + mockito-core + + + org.assertj + assertj-core + 3.24.2 + test + diff --git a/modules/utils/src/main/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactory.java b/modules/utils/src/main/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactory.java index 0751b1b76f..77b5833497 100644 --- a/modules/utils/src/main/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactory.java +++ b/modules/utils/src/main/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactory.java @@ -81,6 +81,11 @@ public A getAdapter(final Object adaptable, final Class type) return null; } final Resource resource = (Resource) adaptable; + final Node node = resource.adaptTo(Node.class); + if (node == null) { + return null; + } + // The list of processors that are enabled for the current resource serialization. List enabledProcessors = setupProcessors(resource); @@ -89,7 +94,6 @@ public A getAdapter(final Object adaptable, final Class type) Stack processedNodes = new Stack<>(); start(resource, enabledProcessors); - final Node node = resource.adaptTo(Node.class); JsonValue result = serializeNode(node, enabledProcessors, processedNodes); end(resource, enabledProcessors); if (result != null) { diff --git a/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java b/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java new file mode 100644 index 0000000000..8e3572c31c --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java @@ -0,0 +1,95 @@ +/* + * 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 io.uhndata.cards.scripting; + +import javax.script.Bindings; + +import org.apache.sling.api.SlingHttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link ContentTypeSetter}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class ContentTypeSetterTest +{ + @InjectMocks + private ContentTypeSetter contentTypeSetter; + + @Mock + private SlingHttpServletResponse response; + + @Test + public void initGetsResponseKeyFromBindings() + { + Bindings bindings = mock(Bindings.class); + this.contentTypeSetter.init(bindings); + verify(bindings, times(1)).get("response"); + } + + @Test + public void htmlSetsHtmlContentType() + { + this.contentTypeSetter.html(); + verify(this.response, times(1)).setContentType("text/html;charset=UTF-8"); + } + + @Test + public void javascriptSetsJavascriptContentType() + { + this.contentTypeSetter.javascript(); + verify(this.response, times(1)).setContentType("application/javascript;charset=UTF-8"); + } + + @Test + public void jsonSetsJsonContentType() + { + this.contentTypeSetter.json(); + verify(this.response, times(1)).setContentType("application/json;charset=UTF-8"); + } + + @Test + public void csvSetsCsvContentType() + { + this.contentTypeSetter.csv(); + verify(this.response, times(1)).setContentType("text/csv;charset=UTF-8"); + } + + @Test + public void textSetsPlainContentType() + { + this.contentTypeSetter.text(); + verify(this.response, times(1)).setContentType("text/plain;charset=UTF-8"); + } + + @Test + public void markdownSetsMarkdownContentType() + { + this.contentTypeSetter.markdown(); + verify(this.response, times(1)).setContentType("text/markdown;charset=UTF-8"); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java b/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java new file mode 100644 index 0000000000..ad7a99b2a9 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java @@ -0,0 +1,154 @@ +/* + * 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 io.uhndata.cards.scripting; + +import javax.script.Bindings; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link StatusCodeSetter}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class StatusCodeSetterTest +{ + private static final int LOCKED = 423; + @InjectMocks + private StatusCodeSetter statusCodeSetter; + + @Mock + private SlingHttpServletResponse response; + + @Test + public void initGetsResponseKeyFromBindings() + { + Bindings bindings = mock(Bindings.class); + this.statusCodeSetter.init(bindings); + verify(bindings, times(1)).get("response"); + } + + @Test + public void okSetsOkStatus() + { + this.statusCodeSetter.ok(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_OK); + } + + @Test + public void createdSetsCreatedStatus() + { + this.statusCodeSetter.created(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_CREATED); + } + + @Test + public void acceptedSetsAcceptedStatus() + { + this.statusCodeSetter.accepted(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_ACCEPTED); + } + + @Test + public void noContentNoContentSetsStatus() + { + this.statusCodeSetter.noContent(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NO_CONTENT); + } + + @Test + public void badRequestSetsBadRequestStatus() + { + this.statusCodeSetter.badRequest(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void unauthorizedSetsUnauthorizedStatus() + { + this.statusCodeSetter.unauthorized(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void forbiddenSetsForbiddenStatus() + { + this.statusCodeSetter.forbidden(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + @Test + public void notFoundSetsNotFoundStatus() + { + this.statusCodeSetter.notFound(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_FOUND); + } + + @Test + public void methodNotAllowedSetsMethodNotAllowedStatus() + { + this.statusCodeSetter.methodNotAllowed(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void notAcceptableSetsNotAcceptableStatus() + { + this.statusCodeSetter.notAcceptable(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + + @Test + public void conflictSetsConflictStatus() + { + this.statusCodeSetter.conflict(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_CONFLICT); + } + + @Test + public void lockedSetsLockedStatus() + { + this.statusCodeSetter.locked(); + verify(this.response, times(1)).setStatus(LOCKED); + } + + @Test + public void internalServerErrorSetsInternalServerErrorStatus() + { + this.statusCodeSetter.internalServerError(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void notImplementedSetsNotImplementedStatus() + { + this.statusCodeSetter.notImplemented(); + verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java new file mode 100644 index 0000000000..221805304c --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java @@ -0,0 +1,53 @@ +/* + * 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 io.uhndata.cards.serialize; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link CSVString}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class CSVStringTest +{ + @InjectMocks + private CSVString csvString; + + @Test + public void toStringReturnsInputData() + { + String input = "Input\ndata"; + Whitebox.setInternalState(this.csvString, "data", input); + assertEquals(input, this.csvString.toString()); + } + + @Test + public void toStringWithNullDataReturnsNull() + { + Whitebox.setInternalState(this.csvString, "data", null); + assertNull(this.csvString.toString()); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java new file mode 100644 index 0000000000..4b7ca130e5 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java @@ -0,0 +1,137 @@ +/* + * 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 io.uhndata.cards.serialize; + +import java.util.List; +import java.util.UUID; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.runners.MockitoJUnitRunner; + +import io.uhndata.cards.serialize.spi.ResourceCSVProcessor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ResourceToCSVAdapterFactory}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class ResourceToCSVAdapterFactoryTest +{ + private static final String NODE_IDENTIFIER = "jcr:uuid"; + private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; + private static final String TEST_SUBJECT_PATH = "/Subjects/Test"; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private ResourceToCSVAdapterFactory factory; + + @Test + public void getAdapterForNullAdaptableObjectReturnsNull() + { + assertNull(this.factory.getAdapter(null, CSVString.class)); + } + + @Test + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + { + Resource adaptable = mock(Resource.class); + String identifier = UUID.randomUUID().toString(); + ResourceCSVProcessor processor = mock(ResourceCSVProcessor.class); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + identifier + ",admin"; + + when(processor.canProcess(adaptable)).thenReturn(true); + when(processor.serialize(adaptable)).thenReturn(data); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); + assertNotNull(adapter); + assertEquals(data, adapter.toString()); + } + + @Test + public void getAdapterForUnsupportedResourceReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + ResourceCSVProcessor processor = mock(ResourceCSVProcessor.class); + + when(adaptable.getPath()).thenReturn(TEST_SUBJECT_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); + assertNotNull(adapter); + assertEquals(TEST_SUBJECT_PATH, adapter.toString()); + } + + @Test + public void getAdapterWithNoProcessorsReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + + when(adaptable.getPath()).thenReturn(TEST_SUBJECT_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); + assertNotNull(adapter); + assertEquals(TEST_SUBJECT_PATH, adapter.toString()); + } + + @Test + public void getAdapterUsesFirstProcessorThatCanProcess() + { + Resource adaptable = mock(Resource.class); + + ResourceCSVProcessor processor1 = mock(ResourceCSVProcessor.class); + when(processor1.canProcess(adaptable)).thenReturn(false); + + ResourceCSVProcessor processor2 = mock(ResourceCSVProcessor.class); + when(processor2.canProcess(adaptable)).thenReturn(true); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + UUID.randomUUID() + ",admin"; + when(processor2.serialize(adaptable)).thenReturn(data); + + ResourceCSVProcessor processor3 = mock(ResourceCSVProcessor.class); + + Whitebox.setInternalState(this.factory, "allProcessors", + List.of(processor1, processor2, processor3)); + CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); + verify(processor1, times(0)).serialize(adaptable); + verify(processor2, times(1)).serialize(adaptable); + verify(processor3, times(0)).serialize(adaptable); + assertNotNull(adapter); + assertEquals(data, adapter.toString()); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java new file mode 100644 index 0000000000..223a77c13d --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java @@ -0,0 +1,493 @@ +/* + * 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 io.uhndata.cards.serialize; + +import java.util.List; +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.json.Json; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceMetadata; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.runners.MockitoJUnitRunner; + +import io.uhndata.cards.serialize.spi.ResourceJsonProcessor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ResourceToJsonAdapterFactory}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class ResourceToJsonAdapterFactoryTest +{ + private static final String NODE_TYPE = "jcr:primaryType"; + private static final String SUBJECT_TYPE = "cards:Subject"; + private static final String FORM_TYPE = "cards:Form"; + private static final String ANSWER_TYPE = "cards:TextAnswer"; + private static final String TYPE_PROPERTY = "type"; + private static final String QUESTIONNAIRE_PROPERTY = "questionnaire"; + private static final String QUESTION_PROPERTY = "question"; + private static final String SUBJECT_PROPERTY = "subject"; + private static final String IDENTIFIER_PROPERTY = "identifier"; + private static final String TEST_PROCESSOR_NAME = "test"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + private static final String TEST_SUBJECT_PATH = "/Subjects/r1"; + private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire"; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private ResourceToJsonAdapterFactory factory; + + @Test + public void getAdapterForNullAdaptableObjectReturnsNull() + { + assertNull(this.factory.getAdapter(null, JsonObject.class)); + } + + @Test + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + + mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); + assertNotNull(adapter); + } + + @Test + public void getAdapterForNullNodeReturnsNull() + { + Resource adaptable = mock(Resource.class); + when(adaptable.adaptTo(Node.class)).thenReturn(null); + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + assertNull(adapter); + } + + @Test + public void getAdapterCatchesRepositoryExceptionReturnsNull() throws RepositoryException + { + Resource adaptable = mock(Resource.class); + ResourceMetadata resourceMetadata = mock(ResourceMetadata.class); + Node node = mock(Node.class); + when(adaptable.getResourceMetadata()).thenReturn(resourceMetadata); + when(adaptable.adaptTo(Node.class)).thenReturn(node); + + when(node.getPath()).thenReturn(TEST_FORM_PATH); + when(node.getProperties()).thenThrow(new RepositoryException()); + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + + mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + assertNull(adapter); + } + + @Test + public void getAdapterUsesAllSupportedAndEnabledProcessors() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + ResourceJsonProcessor processor1 = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor1, adaptable, false, false, TEST_PROCESSOR_NAME + "1"); + + ResourceJsonProcessor processor2 = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor2, adaptable, true, true, TEST_PROCESSOR_NAME + "2"); + + ResourceJsonProcessor processor3 = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor3, adaptable, false, true, TEST_PROCESSOR_NAME + "3"); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor1, processor2, processor3)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + verifyProcessorMethodsInvocation(processor1, 0, 0, 0, 0); + verifyProcessorMethodsInvocation(processor2, 1, 1, 15, 2); + verifyProcessorMethodsInvocation(processor3, 0, 0, 0, 0); + + assertNotNull(adapter); + } + + @Test + public void getAdapterSortsProcessors() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + TestResourceJsonProcessor processor1 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "1", 1, true, 1, 4); + TestResourceJsonProcessor processor2 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "2", 2, true, 2, 3); + TestResourceJsonProcessor processor3 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "3", 3, true, 3, 2); + TestResourceJsonProcessor processor4 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "4", 4, true, 4, 1); + + Whitebox.setInternalState(this.factory, "allProcessors", + List.of(processor1, processor3, processor4, processor2)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + assertNotNull(adapter); + assertEquals(141, adapter.getInt(NODE_TYPE)); + } + + @Test + public void getAdapterWithNoProcessorsReturnsEmptyJsonObject() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + assertNotNull(adapter); + assertTrue(adapter.isEmpty()); + } + + @Test + public void getAdapterWithNoSupportedProcessorsReturnsEmptyJsonObject() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor, adaptable, true, false, TEST_PROCESSOR_NAME); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + assertNotNull(adapter); + assertTrue(adapter.isEmpty()); + } + + @Test + public void getAdapterUsesResourceSelectorsToDisableDefaultProcessors() + { + Resource adaptable = mock(Resource.class); + mockAdaptableResource(adaptable, TEST_FORM_PATH, "-" + TEST_PROCESSOR_NAME); + + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + this.factory.getAdapter(adaptable, JsonObject.class); + + // There is no enabled processor, so these methods are not invoked + verifyProcessorMethodsInvocation(processor, 0, 0, 0, 0); + } + + @Test + public void getAdapterUsesResourceSelectorsToEnableProcessors() + { + Resource adaptable = mock(Resource.class); + mockAdaptableResource(adaptable, TEST_FORM_PATH, TEST_PROCESSOR_NAME); + + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor, adaptable, false, true, TEST_PROCESSOR_NAME); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + this.factory.getAdapter(adaptable, JsonObject.class); + + // Methods of not enabledByDefault processor are invoked + verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); + } + + @Test + public void getAdapterWithBothEnableAndDisableSelectorsPrioritizesEnable() + { + Resource adaptable = mock(Resource.class); + mockAdaptableResource(adaptable, TEST_FORM_PATH, TEST_PROCESSOR_NAME + ".-" + TEST_PROCESSOR_NAME); + + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor, adaptable, false, true, TEST_PROCESSOR_NAME); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + this.factory.getAdapter(adaptable, JsonObject.class); + + verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); + } + + @Test + public void getAdapterWithRecursiveReferencesUsesResourcePathForNestedReferences() throws RepositoryException + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + Node adaptableNode = adaptable.adaptTo(Node.class); + adaptableNode.setProperty("form", adaptableNode.getIdentifier(), Type.REFERENCE.tag()); + + ResourceJsonProcessor processor = new FormRecursiveTestResourceJsonProcessor(); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + + assertEquals(TEST_FORM_PATH, adapter.getString("form")); + } + + @Test + public void getAdapterWithProcessedAnswerChildNode() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + + ResourceJsonProcessor processor = new ChildNodeTestResourceJsonProcessor(); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + + assertTrue(adapter.containsKey("a1")); + } + + @Test + public void getAdapterWithBothInvokedAndNotInvokedDefaultProcessors() + { + Resource adaptable = mock(Resource.class); + mockAdaptableResource(adaptable, TEST_FORM_PATH, TEST_PROCESSOR_NAME); + + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); + + ResourceJsonProcessor processorNotInvoked = mock(ResourceJsonProcessor.class); + mockWorkingProcessor(processorNotInvoked, adaptable, true, true, "test_not_invoked"); + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor, processorNotInvoked)); + this.factory.getAdapter(adaptable, JsonObject.class); + + verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); + verifyProcessorMethodsInvocation(processorNotInvoked, 1, 1, 15, 2); + } + + @Before + public void setUp() throws RepositoryException + { + this.context.build() + .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage") + .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage") + .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage") + .resource("/Forms", NODE_TYPE, "cards:FormsHomepage") + .commit(); + this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH); + this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root"); + this.context.build() + .resource(TEST_SUBJECT_PATH, + NODE_TYPE, SUBJECT_TYPE, + TYPE_PROPERTY, + this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class), + IDENTIFIER_PROPERTY, "Root subject1") + .commit(); + final Session session = this.context.resourceResolver().adaptTo(Session.class); + Node subject = session.getNode(TEST_SUBJECT_PATH); + Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH); + Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1"); + + this.context.build() + .resource(TEST_FORM_PATH, + NODE_TYPE, FORM_TYPE, + SUBJECT_PROPERTY, subject, + QUESTIONNAIRE_PROPERTY, questionnaire) + .resource(TEST_FORM_PATH + "/a1", + NODE_TYPE, ANSWER_TYPE, + QUESTION_PROPERTY, question) + .commit(); + } + + private void mockAdaptableResource(Resource adaptable, String path, String resolutionPathInfo) + { + Node adaptableNode = this.context.resourceResolver().getResource(path).adaptTo(Node.class); + when(adaptable.adaptTo(Node.class)).thenReturn(adaptableNode); + + ResourceMetadata resourceMetadata = mock(ResourceMetadata.class); + when(adaptable.getResourceMetadata()).thenReturn(resourceMetadata); + when(resourceMetadata.getResolutionPathInfo()).thenReturn(resolutionPathInfo); + } + + private void mockWorkingProcessor(ResourceJsonProcessor processor, Resource adaptable, boolean isEnabled, + boolean canProcess, String processorName) + { + when(processor.isEnabledByDefault(adaptable)).thenReturn(isEnabled); + when(processor.getName()).thenReturn(processorName); + when(processor.canProcess(adaptable)).thenReturn(canProcess); + } + + private void verifyProcessorMethodsInvocation(ResourceJsonProcessor processor, int startAndEndProcess, + int enterAndLeaveProcess, int processProperty, int processChild) + { + verify(processor, times(startAndEndProcess)).start(any()); + verify(processor, times(enterAndLeaveProcess)).enter(any(), any(), any()); + verify(processor, times(processProperty)).processProperty(any(), any(), any(), any()); + verify(processor, times(processChild)).processChild(any(), any(), any(), any()); + verify(processor, times(enterAndLeaveProcess)).leave(any(), any(), any()); + verify(processor, times(startAndEndProcess)).end(any()); + } + + private static class TestResourceJsonProcessor implements ResourceJsonProcessor + { + private final String name; + private final int priority; + private final boolean isEnabledByDefault; + private final int a; + private final int b; + + TestResourceJsonProcessor(String name, int priority, boolean isEnabledByDefault, int a, int b) + { + this.name = name; + this.priority = priority; + this.isEnabledByDefault = isEnabledByDefault; + this.a = a; + this.b = b; + } + + @Override + public String getName() + { + return this.name; + } + + @Override + public int getPriority() + { + return this.priority; + } + + @Override + public boolean isEnabledByDefault(final Resource resource) + { + return this.isEnabledByDefault; + } + @Override + public JsonValue processProperty(final Node node, final Property property, final JsonValue input, + final Function serializeNode) + { + return Json.createValue(input == null ? this.b : ((JsonNumber) input).intValue() * this.a + this.b); + } + + @Override + public String getDescription() + { + return "TestResourceJsonProcessor"; + } + } + + private static class FormRecursiveTestResourceJsonProcessor implements ResourceJsonProcessor + { + @Override + public String getName() + { + return "formRecursive"; + } + + @Override + public int getPriority() + { + return 1; + } + + @Override + public boolean isEnabledByDefault(Resource resource) + { + return true; + } + + @Override + public boolean canProcess(Resource resource) + { + return true; + } + + @Override + public JsonValue processProperty(final Node node, final Property property, final JsonValue input, + final Function serializeNode) + { + try { + if ("form".equals(property.getName())) { + Session session = property.getSession(); + Node form = session.getNodeByIdentifier(property.getString()); + return serializeNode.apply(form); + } + } catch (RepositoryException e) { + // Should not happen + } + return input; + } + + @Override + public String getDescription() + { + return "FormRecursiveTestResourceJsonProcessor"; + } + } + + private static class ChildNodeTestResourceJsonProcessor implements ResourceJsonProcessor + { + @Override + public String getName() + { + return "childNode"; + } + + @Override + public int getPriority() + { + return 1; + } + + @Override + public boolean isEnabledByDefault(Resource resource) + { + return true; + } + + @Override + public boolean canProcess(Resource resource) + { + return true; + } + + @Override + public JsonValue processChild(final Node node, final Node child, final JsonValue input, + final Function serializeNode) + { + try { + if (child.getName().contains("a")) { + Session session = child.getSession(); + Node answer = session.getNodeByIdentifier(child.getIdentifier()); + return serializeNode.apply(answer); + } + } catch (RepositoryException e) { + // Should not happen + } + return input; + } + + @Override + public String getDescription() + { + return "ChildNodeTestResourceJsonProcessor"; + } + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java new file mode 100644 index 0000000000..0f8041d84a --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java @@ -0,0 +1,137 @@ +/* + * 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 io.uhndata.cards.serialize; + +import java.util.List; +import java.util.UUID; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.runners.MockitoJUnitRunner; + +import io.uhndata.cards.serialize.spi.ResourceMarkdownProcessor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ResourceToMarkdownAdapterFactory}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class ResourceToMarkdownAdapterFactoryTest +{ + private static final String NODE_IDENTIFIER = "jcr:uuid"; + private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private ResourceToMarkdownAdapterFactory factory; + + @Test + public void getAdapterForNullAdaptableObjectReturnsNull() + { + assertNull(this.factory.getAdapter(null, CharSequence.class)); + } + + @Test + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + { + Resource adaptable = mock(Resource.class); + String identifier = UUID.randomUUID().toString(); + ResourceMarkdownProcessor processor = mock(ResourceMarkdownProcessor.class); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + identifier + ",admin"; + + when(processor.canProcess(adaptable)).thenReturn(true); + when(processor.serialize(adaptable)).thenReturn(data); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); + assertNotNull(adapter); + assertEquals(data, adapter); + } + + @Test + public void getAdapterForUnsupportedResourceReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + ResourceMarkdownProcessor processor = mock(ResourceMarkdownProcessor.class); + + when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); + assertNotNull(adapter); + assertEquals(TEST_FORM_PATH, adapter); + } + + @Test + public void getAdapterWithNoProcessorsReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + + when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); + assertNotNull(adapter); + assertEquals(TEST_FORM_PATH, adapter); + } + + @Test + public void getAdapterUsesFirstProcessorThatCanProcess() + { + Resource adaptable = mock(Resource.class); + + ResourceMarkdownProcessor processor1 = mock(ResourceMarkdownProcessor.class); + when(processor1.canProcess(adaptable)).thenReturn(false); + + ResourceMarkdownProcessor processor2 = mock(ResourceMarkdownProcessor.class); + when(processor2.canProcess(adaptable)).thenReturn(true); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + UUID.randomUUID() + ",admin"; + when(processor2.serialize(adaptable)).thenReturn(data); + + ResourceMarkdownProcessor processor3 = mock(ResourceMarkdownProcessor.class); + + Whitebox.setInternalState(this.factory, "allProcessors", + List.of(processor1, processor2, processor3)); + CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); + verify(processor1, times(0)).serialize(adaptable); + verify(processor2, times(1)).serialize(adaptable); + verify(processor3, times(0)).serialize(adaptable); + assertNotNull(adapter); + assertEquals(data, adapter); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java new file mode 100644 index 0000000000..c5ab760355 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java @@ -0,0 +1,157 @@ +/* + * 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 io.uhndata.cards.serialize; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.jcr.resource.internal.HelperData; +import org.apache.sling.jcr.resource.internal.helper.jcr.JcrItemResourceFactory; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.runners.MockitoJUnitRunner; + +import io.uhndata.cards.serialize.spi.ResourceTextProcessor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ResourceToTextAdapterFactory}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class ResourceToTextAdapterFactoryTest +{ + private static final String NODE_IDENTIFIER = "jcr:uuid"; + private static final String NODE_TYPE = "jcr:primaryType"; + private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private ResourceToTextAdapterFactory factory; + + @Test + public void getAdapterForNullAdaptableObjectReturnsNull() + { + assertNull(this.factory.getAdapter(null, String.class)); + } + + @Test + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + { + Resource adaptable = mock(Resource.class); + String identifier = UUID.randomUUID().toString(); + ResourceTextProcessor processor = mock(ResourceTextProcessor.class); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + identifier + ",admin"; + + when(processor.canProcess(adaptable)).thenReturn(true); + when(processor.serialize(adaptable)).thenReturn(data); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + String adapter = this.factory.getAdapter(adaptable, String.class); + assertNotNull(adapter); + assertEquals(data, adapter); + } + + @Test + public void getAdapterForUnsupportedResourceReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + ResourceTextProcessor processor = mock(ResourceTextProcessor.class); + + when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + String adapter = this.factory.getAdapter(adaptable, String.class); + assertNotNull(adapter); + assertEquals(TEST_FORM_PATH, adapter); + } + + @Test + public void getAdapterWithNoProcessorsReturnsResourcePath() + { + Resource adaptable = mock(Resource.class); + + when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); + + Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + String adapter = this.factory.getAdapter(adaptable, String.class); + assertNotNull(adapter); + assertEquals(TEST_FORM_PATH, adapter); + } + + @Test + public void getAdapterUsesFirstProcessorThatCanProcess() + { + Resource adaptable = mock(Resource.class); + + ResourceTextProcessor processor1 = mock(ResourceTextProcessor.class); + when(processor1.canProcess(adaptable)).thenReturn(false); + + ResourceTextProcessor processor2 = mock(ResourceTextProcessor.class); + when(processor2.canProcess(adaptable)).thenReturn(true); + String data = NODE_IDENTIFIER + "," + CREATED_BY_PROPERTY + "\n" + + UUID.randomUUID() + ",admin"; + when(processor2.serialize(adaptable)).thenReturn(data); + + ResourceTextProcessor processor3 = mock(ResourceTextProcessor.class); + + Whitebox.setInternalState(this.factory, "allProcessors", + List.of(processor1, processor2, processor3)); + String adapter = this.factory.getAdapter(adaptable, String.class); + verify(processor1, times(0)).serialize(adaptable); + verify(processor2, times(1)).serialize(adaptable); + verify(processor3, times(0)).serialize(adaptable); + assertNotNull(adapter); + assertEquals(data, adapter); + } + + @Test + public void getAdapterForJcrPropertyResourceReturnsNull() throws RepositoryException + { + this.context.build().resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage").commit(); + this.context.build() + .resource("/SubjectTypes/Root", NODE_TYPE, "cards:SubjectType", "label", "Root").commit(); + Resource resource = new JcrItemResourceFactory( + this.context.resourceResolver().adaptTo(Session.class), mock(HelperData.class)) + .createResource(this.context.resourceResolver(), "/SubjectTypes/Root/label", + this.context.resourceResolver().getResource("/SubjectTypes/Root"), Map.of()); + assertNull(this.factory.getAdapter(resource, String.class)); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java new file mode 100644 index 0000000000..c93bc05033 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java @@ -0,0 +1,411 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.function.Function; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; + +import org.apache.jackrabbit.oak.api.Type; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link BareProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class BareProcessorTest +{ + private static final String NODE_TYPE = "jcr:primaryType"; + private static final String RESOURCE_TYPE = "sling:resourceType"; + private static final String SUBJECT_TYPE = "cards:Subject"; + private static final String FORM_TYPE = "cards:Form"; + private static final String ANSWER_TYPE = "cards:TextAnswer"; + private static final String TYPE_PROPERTY = "type"; + private static final String QUESTIONNAIRE_PROPERTY = "questionnaire"; + private static final String QUESTION_PROPERTY = "question"; + private static final String SUBJECT_PROPERTY = "subject"; + private static final String IDENTIFIER_PROPERTY = "identifier"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + private static final String TEST_SUBJECT_PATH = "/Subjects/r1"; + private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire"; + private static final String NAME = "bare"; + private static final int PRIORITY = 90; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private BareProcessor bareProcessor; + + @Mock + private ThreadLocal depth; + + @Test + public void getNameReturnBare() + { + assertEquals(NAME, this.bareProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.bareProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertFalse(this.bareProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void startTest() + { + this.bareProcessor.start(mock(Resource.class)); + verify(this.depth).set(0); + } + + @Test + public void enterTest() + { + when(this.depth.get()).thenReturn(0); + this.bareProcessor.enter(mock(Node.class), mock(JsonObjectBuilder.class), mock(Function.class)); + verify(this.depth).get(); + verify(this.depth).set(1); + } + + @Test + public void processPropertyForNullProperty() + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + + JsonValue jsonValue = this.bareProcessor.processProperty(node, null, mock(JsonValue.class), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyCatchesRepositoryExceptionReturnsInputValue() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + + Property property = mock(Property.class); + when(property.getName()).thenThrow(new RepositoryException()); + JsonString value = Json.createValue(node.getProperty(QUESTIONNAIRE_PROPERTY).getString()); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, value, mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(value, jsonValue); + } + + @Test + public void processPropertyForQuestionnairePropertyReturnsInputValue() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + + Property property = node.getProperty(QUESTIONNAIRE_PROPERTY); + JsonString input = Json.createValue(property.getString()); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, input, mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + @Test + public void processPropertyForJcrPropertyReturnsNull() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + + Property property = node.getProperty(NODE_TYPE); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyForSlingPropertyReturnsNull() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + + Property property = node.getProperty(RESOURCE_TYPE); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyForFormPropertyReturnsNull() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + node.setProperty("form", node.getIdentifier(), Type.REFERENCE.tag()); + + Property property = node.getProperty("form"); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processChildForJcrChildReturnsNull() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + Node child = mock(Node.class); + when(child.getName()).thenReturn("jcr:name"); + + JsonValue jsonValue = this.bareProcessor.processChild(node, child, mock(JsonValue.class), mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processChildCatchesRepositoryExceptionReturnsInputValue() throws RepositoryException + { + Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); + Node child = mock(Node.class); + when(child.getName()).thenThrow(new RepositoryException()); + + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + @Test + public void processChildForAnswerChildReturnsInputValue() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Node child = session.getNode(TEST_FORM_PATH + "/a1"); + + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + @Test + public void leaveSerializesCreatedAndLastModifiedAndFileContent() throws RepositoryException, ParseException + { + Node node = mock(Node.class); + when(this.depth.get()).thenReturn(1, 0); + + Calendar date = Calendar.getInstance(); + date.set(2023, Calendar.JANUARY, 1); + date.getTimeZone().getRawOffset(); + mockCreatedAndLastModifiedDate(node, date); + mockFileContent(node, getMockedDataProperty()); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.bareProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + + verify(this.depth, times(3)).get(); + verify(this.depth).set(0); + + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + + assertNotNull(jsonObject); + assertTrue(jsonObject.containsKey("created")); + assertEquals(date.getTime(), format.parse(jsonObject.getString("created"))); + assertTrue(jsonObject.containsKey("lastModified")); + assertEquals(date.getTime(), format.parse(jsonObject.getString("lastModified"))); + assertTrue(jsonObject.containsKey("content")); + } + + @Test + public void leaveWithNonRootNodeDoesNotAddMetadata() throws RepositoryException + { + Node node = mock(Node.class); + when(this.depth.get()).thenReturn(2, 1); + + Calendar date = Calendar.getInstance(); + date.set(2023, Calendar.JANUARY, 1); + date.getTimeZone().getRawOffset(); + mockCreatedAndLastModifiedDate(node, date); + mockFileContent(node, getMockedDataProperty()); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.bareProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + + verify(this.depth, times(3)).get(); + verify(this.depth).set(1); + + assertNotNull(jsonObject); + assertFalse(jsonObject.containsKey("created")); + assertFalse(jsonObject.containsKey("lastModified")); + assertTrue(jsonObject.containsKey("content")); + } + + @Test + public void leaveCatchesIOException() throws RepositoryException, IOException + { + Node node = mock(Node.class); + when(this.depth.get()).thenReturn(1, 0); + + Calendar date = Calendar.getInstance(); + date.set(2023, Calendar.JANUARY, 1); + date.getTimeZone().getRawOffset(); + + // mocking data property with closed InputStream + Property dataProperty = mock(Property.class); + Binary dataBinary = mock(Binary.class); + InputStream stream = InputStream.nullInputStream(); + stream.close(); + when(dataProperty.getBinary()).thenReturn(dataBinary); + when(dataBinary.getStream()).thenReturn(stream); + + mockCreatedAndLastModifiedDate(node, date); + mockFileContent(node, dataProperty); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.bareProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + + verify(this.depth, times(3)).get(); + verify(this.depth).set(0); + + assertNotNull(jsonObject); + assertTrue(jsonObject.containsKey("created")); + assertTrue(jsonObject.containsKey("lastModified")); + assertTrue(jsonObject.containsKey("content")); + } + + @Test + public void leaveCatchesRepositoryException() throws RepositoryException + { + Node node = mock(Node.class); + + when(node.hasProperty("jcr:created")).thenThrow(new RepositoryException()); + when(node.hasProperty("jcr:lastModified")).thenThrow(new RepositoryException()); + when(node.isNodeType("nt:file")).thenThrow(new RepositoryException()); + when(this.depth.get()).thenReturn(1, 0); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.bareProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + + verify(this.depth, times(3)).get(); + verify(this.depth).set(0); + + assertNotNull(jsonObject); + assertFalse(jsonObject.containsKey("created")); + assertFalse(jsonObject.containsKey("lastModified")); + assertFalse(jsonObject.containsKey("content")); + } + + @Before + public void setUp() throws RepositoryException + { + this.context.build() + .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage") + .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage") + .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage") + .resource("/Forms", NODE_TYPE, "cards:FormsHomepage") + .commit(); + this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH); + this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root"); + this.context.build() + .resource(TEST_SUBJECT_PATH, + NODE_TYPE, SUBJECT_TYPE, + TYPE_PROPERTY, + this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class), + IDENTIFIER_PROPERTY, "Root subject1") + .commit(); + final Session session = this.context.resourceResolver().adaptTo(Session.class); + Node subject = session.getNode(TEST_SUBJECT_PATH); + Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH); + Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1"); + + this.context.build() + .resource(TEST_FORM_PATH, + NODE_TYPE, FORM_TYPE, + SUBJECT_PROPERTY, subject, + QUESTIONNAIRE_PROPERTY, questionnaire) + .resource(TEST_FORM_PATH + "/a1", + NODE_TYPE, ANSWER_TYPE, + QUESTION_PROPERTY, question) + .commit(); + } + + private void mockFileContent(Node node, Property dataProperty) throws RepositoryException + { + NodeIterator iterator = mock(NodeIterator.class); + Node child = mock(Node.class); + when(node.isNodeType("nt:file")).thenReturn(true); + when(node.getNodes()).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true, false); + when(iterator.nextNode()).thenReturn(child); + when(child.isNodeType("nt:resource")).thenReturn(true); + + // data property + when(child.hasProperty("jcr:data")).thenReturn(true); + when(child.getProperty("jcr:data")).thenReturn(dataProperty); + } + + private Property getMockedDataProperty() throws RepositoryException + { + Property dataProperty = mock(Property.class); + Binary dataBinary = mock(Binary.class); + when(dataProperty.getBinary()).thenReturn(dataBinary); + when(dataBinary.getStream()).thenReturn(InputStream.nullInputStream()); + return dataProperty; + } + + private void mockCreatedAndLastModifiedDate(Node node, Calendar createdDate) throws RepositoryException + { + Property createdDateProperty = mock(Property.class); + when(node.hasProperty("jcr:created")).thenReturn(true); + when(node.hasProperty("jcr:lastModified")).thenReturn(true); + when(node.getProperty("jcr:created")).thenReturn(createdDateProperty); + when(node.getProperty("jcr:lastModified")).thenReturn(createdDateProperty); + when(createdDateProperty.getDate()).thenReturn(createdDate); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java new file mode 100644 index 0000000000..537d998e24 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java @@ -0,0 +1,104 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.json.Json; +import javax.json.JsonValue; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DeepProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class DeepProcessorTest +{ + private static final String TEST_FORM_PATH = "/Forms/f1"; + private static final String NAME = "deep"; + private static final int PRIORITY = 10; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private DeepProcessor deepProcessor; + + @Test + public void getNameReturnDeep() + { + assertEquals(NAME, this.deepProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.deepProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertFalse(this.deepProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void processChildForNullJsonValueInputReturnsSerializedChild() throws RepositoryException + { + Node child = mock(Node.class); + when(child.getPath()).thenReturn(TEST_FORM_PATH); + + JsonValue jsonValue = this.deepProcessor.processChild(mock(Node.class), child, null, this::serializeNode); + assertNotNull(jsonValue); + assertEquals(Json.createValue(TEST_FORM_PATH), jsonValue); + } + + @Test + public void processChildForNotNullJsonValueInputReturnsInputValue() + { + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.deepProcessor.processChild(mock(Node.class), mock(Node.class), input, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + private JsonValue serializeNode(Node node) + { + try { + return Json.createValue(node.getPath()); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java new file mode 100644 index 0000000000..d96a94fec8 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java @@ -0,0 +1,317 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.List; +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonString; +import javax.json.JsonValue; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DereferenceProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class DereferenceProcessorTest +{ + private static final String NODE_TYPE = "jcr:primaryType"; + private static final String SUBJECT_TYPE = "cards:Subject"; + private static final String FORM_TYPE = "cards:Form"; + private static final String ANSWER_TYPE = "cards:TextAnswer"; + private static final String TYPE_PROPERTY = "type"; + private static final String QUESTIONNAIRE_PROPERTY = "questionnaire"; + private static final String QUESTION_PROPERTY = "question"; + private static final String SUBJECT_PROPERTY = "subject"; + private static final String IDENTIFIER_PROPERTY = "identifier"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + private static final String TEST_SUBJECT_PATH = "/Subjects/r1"; + private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire"; + private static final String NAME = "dereference"; + private static final int PRIORITY = 10; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private DereferenceProcessor dereferenceProcessor; + + @Test + public void getNameReturnDereference() + { + assertEquals(NAME, this.dereferenceProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.dereferenceProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertTrue(this.dereferenceProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void processPropertyForMultiValueReferenceProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Property property = node.getProperty("relatedSubjects"); + JsonValue json = Json.createValue("relatedSubjects"); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(node, property, json, this::serializeNode); + assertNotNull(jsonValue); + assertTrue(jsonValue instanceof JsonArray); + assertEquals(1, ((JsonArray) jsonValue).size()); + assertEquals(Json.createValue("r1"), ((JsonArray) jsonValue).get(0)); + } + + @Test + public void processPropertyForMultiValueStringProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Property property = node.getProperty("statusFlags"); + JsonValue json = Json.createValue("statusFlags"); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(node, property, json, this::serializeNode); + assertNotNull(jsonValue); + assertEquals(json, jsonValue); + } + + @Test + public void processPropertyForMultiValuePathProperty() throws RepositoryException + { + Node parent = mock(Node.class); + Property property = mock(Property.class); + Value value = mock(Value.class); + Node valuePathNode = mock(Node.class); + String valueName = "valuePath"; + + when(property.isMultiple()).thenReturn(true); + when(property.getName()).thenReturn("paths"); + when(property.getType()).thenReturn(PropertyType.PATH); + when(property.getValues()).thenReturn(new Value[] {value}); + when(value.getString()).thenReturn(valueName); + when(property.getParent()).thenReturn(parent); + when(parent.getNode(valueName)).thenReturn(valuePathNode); + when(valuePathNode.getName()).thenReturn(valueName); + + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, + mock(JsonValue.class), this::serializeNode); + assertNotNull(jsonValue); + assertTrue(jsonValue instanceof JsonArray); + assertEquals(1, ((JsonArray) jsonValue).size()); + assertEquals(Json.createValue(valueName), ((JsonArray) jsonValue).get(0)); + } + + @Test + public void processPropertyForMultiValueJcrProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + String nodeIdentifier = session.getNode(TEST_FORM_PATH).getIdentifier(); + Property property = mock(Property.class); + Value value = mock(Value.class); + + when(property.isMultiple()).thenReturn(true); + when(property.getName()).thenReturn("jcr:test"); + when(property.getType()).thenReturn(PropertyType.REFERENCE); + when(property.getSession()).thenReturn(session); + when(property.getValues()).thenReturn(new Value[] {value}); + when(value.getString()).thenReturn(nodeIdentifier); + + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, + mock(JsonValue.class), this::serializeNode); + assertNotNull(jsonValue); + assertTrue(jsonValue instanceof JsonArray); + assertEquals(1, ((JsonArray) jsonValue).size()); + assertEquals(TEST_FORM_PATH, ((JsonArray) jsonValue).getString(0)); + } + + @Test + public void processPropertyForMultiValuePathPropertyCatchesRepositoryException() throws RepositoryException + { + Property property = mock(Property.class); + Value value = mock(Value.class); + String valueName = "valuePath"; + + when(property.isMultiple()).thenReturn(true); + when(property.getName()).thenReturn("paths"); + when(property.getType()).thenReturn(PropertyType.PATH); + when(property.getValues()).thenReturn(new Value[] {value}); + when(value.getString()).thenReturn("/" + valueName); + when(property.getSession()).thenThrow(new RepositoryException()); + + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, + mock(JsonValue.class), this::serializeNode); + assertNotNull(jsonValue); + assertTrue(jsonValue instanceof JsonArray); + assertEquals(1, ((JsonArray) jsonValue).size()); + assertEquals(Json.createValue("/" + valueName), ((JsonArray) jsonValue).get(0)); + } + + @Test + public void processPropertyCatchesRepositoryExceptionWhileSerializationMultiValueProperty() + throws RepositoryException + { + Property property = mock(Property.class); + when(property.isMultiple()).thenReturn(true); + when(property.getName()).thenReturn("relatedSubjects"); + when(property.getType()).thenReturn(PropertyType.REFERENCE); + when(property.getValues()).thenReturn(new Value[] {mock(Value.class)}); + when(property.getSession()).thenThrow(new RepositoryException()); + JsonValue json = Json.createValue("relatedSubjects"); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(json, jsonValue); + } + + @Test + public void processPropertyForSingleValueReferenceProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Property property = node.getProperty(QUESTIONNAIRE_PROPERTY); + JsonValue json = Json.createValue(QUESTIONNAIRE_PROPERTY); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(node, property, json, this::serializeNode); + assertNotNull(jsonValue); + assertEquals(Json.createValue("TestSerializableQuestionnaire"), jsonValue); + } + + @Test + public void processPropertyForSingleValueStringProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Property property = node.getProperty(NODE_TYPE); + JsonValue json = Json.createValue(FORM_TYPE); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(node, property, json, this::serializeNode); + assertNotNull(jsonValue); + assertEquals(json, jsonValue); + } + + @Test + public void processPropertyCatchesRepositoryExceptionWhileSerializationSingleValueProperty() + throws RepositoryException + { + Property property = mock(Property.class); + when(property.isMultiple()).thenReturn(false); + when(property.getType()).thenReturn(PropertyType.REFERENCE); + when(property.getNode()).thenThrow(new RepositoryException()); + JsonValue json = Json.createValue(QUESTIONNAIRE_PROPERTY); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(json, jsonValue); + } + + @Test + public void processPropertyForJcrSingleValueProperty() + throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + Property property = node.getProperty("jcr:baseVersion"); + JsonValue json = Json.createValue(FORM_TYPE); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(node, property, json, this::serializeNode); + assertNotNull(jsonValue); + assertEquals(property.getNode().getPath(), ((JsonString) jsonValue).getString()); + } + + @Test + public void processPropertyCatchesRepositoryException() throws RepositoryException + { + Property property = mock(Property.class); + when(property.isMultiple()).thenThrow(new RepositoryException()); + JsonValue json = Json.createValue(FORM_TYPE); + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(json, jsonValue); + } + + @Before + public void setUp() throws RepositoryException + { + this.context.build() + .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage") + .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage") + .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage") + .resource("/Forms", NODE_TYPE, "cards:FormsHomepage") + .commit(); + this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH); + this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root"); + this.context.build() + .resource(TEST_SUBJECT_PATH, + NODE_TYPE, SUBJECT_TYPE, + TYPE_PROPERTY, + this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class), + IDENTIFIER_PROPERTY, "Root subject1") + .commit(); + final Session session = this.context.resourceResolver().adaptTo(Session.class); + Node subject = session.getNode(TEST_SUBJECT_PATH); + Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH); + Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1"); + + this.context.build() + .resource(TEST_FORM_PATH, + NODE_TYPE, FORM_TYPE, + SUBJECT_PROPERTY, subject, + QUESTIONNAIRE_PROPERTY, questionnaire, + "relatedSubjects", List.of(subject).toArray()) + .resource(TEST_FORM_PATH + "/a1", + NODE_TYPE, ANSWER_TYPE, + QUESTION_PROPERTY, question) + .commit(); + } + + private JsonValue serializeNode(Node node) + { + try { + return Json.createValue(node.getName()); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java new file mode 100644 index 0000000000..ccc7ca63f5 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java @@ -0,0 +1,150 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link IdentificationProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class IdentificationProcessorTest +{ + private static final String NODE_TYPE = "jcr:primaryType"; + private static final String SUBJECT_TYPE = "cards:Subject"; + private static final String FORM_TYPE = "cards:Form"; + private static final String ANSWER_TYPE = "cards:TextAnswer"; + private static final String TYPE_PROPERTY = "type"; + private static final String QUESTIONNAIRE_PROPERTY = "questionnaire"; + private static final String QUESTION_PROPERTY = "question"; + private static final String SUBJECT_PROPERTY = "subject"; + private static final String IDENTIFIER_PROPERTY = "identifier"; + private static final String TEST_FORM_PATH = "/Forms/f1"; + private static final String TEST_SUBJECT_PATH = "/Subjects/r1"; + private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire"; + private static final String NAME = "identify"; + private static final int PRIORITY = 10; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private IdentificationProcessor identificationProcessor; + + @Test + public void getNameReturnIdentify() + { + assertEquals(NAME, this.identificationProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.identificationProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertTrue(this.identificationProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void leaveCatchesRepositoryException() throws RepositoryException + { + JsonObjectBuilder json = Json.createObjectBuilder(); + Node node = mock(Node.class); + when(node.getPath()).thenThrow(new RepositoryException()); + this.identificationProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + assertTrue(jsonObject.isEmpty()); + } + + @Test + public void leaveAddsPathAndNameParameters() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Node node = session.getNode(TEST_FORM_PATH); + JsonObjectBuilder json = Json.createObjectBuilder(); + this.identificationProcessor.leave(node, json, mock(Function.class)); + JsonObject jsonObject = json.build(); + assertFalse(jsonObject.isEmpty()); + assertTrue(jsonObject.containsKey("@path")); + assertEquals(TEST_FORM_PATH, jsonObject.getString("@path")); + assertTrue(jsonObject.containsKey("@name")); + assertEquals("f1", jsonObject.getString("@name")); + } + + @Before + public void setUp() throws RepositoryException + { + this.context.build() + .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage") + .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage") + .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage") + .resource("/Forms", NODE_TYPE, "cards:FormsHomepage") + .commit(); + this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH); + this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root"); + this.context.build() + .resource(TEST_SUBJECT_PATH, + NODE_TYPE, SUBJECT_TYPE, + TYPE_PROPERTY, + this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class), + IDENTIFIER_PROPERTY, "Root subject1") + .commit(); + final Session session = this.context.resourceResolver().adaptTo(Session.class); + Node subject = session.getNode(TEST_SUBJECT_PATH); + Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH); + Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1"); + + this.context.build() + .resource(TEST_FORM_PATH, + NODE_TYPE, FORM_TYPE, + SUBJECT_PROPERTY, subject, + QUESTIONNAIRE_PROPERTY, questionnaire) + .resource(TEST_FORM_PATH + "/a1", + NODE_TYPE, ANSWER_TYPE, + QUESTION_PROPERTY, question) + .commit(); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java new file mode 100644 index 0000000000..f0674f8a76 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java @@ -0,0 +1,104 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.json.JsonValue; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import io.uhndata.cards.forms.api.FormUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link PropertiesProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class PropertiesProcessorTest +{ + private static final String NAME = "properties"; + private static final int PRIORITY = 0; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private PropertiesProcessor propertiesProcessor; + + @Mock + private FormUtils formUtils; + + @Test + public void getNameReturnProperties() + { + assertEquals(NAME, this.propertiesProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.propertiesProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertTrue(this.propertiesProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void processPropertyForNullJsonValueInputReturnsSerializedProperty() + { + JsonValue serializedProperty = mock(JsonValue.class); + when(this.formUtils.serializeProperty(any())).thenReturn(serializedProperty); + + JsonValue jsonValue = this.propertiesProcessor.processProperty(mock(Node.class), mock(Property.class), null, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(serializedProperty, jsonValue); + } + + @Test + public void processPropertyForNotNullJsonValueInputReturnsInputValue() + { + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.propertiesProcessor.processProperty(mock(Node.class), mock(Property.class), input, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java new file mode 100644 index 0000000000..ef4f09cc3a --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java @@ -0,0 +1,139 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.function.Function; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.json.JsonValue; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link SimpleProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class SimpleProcessorTest +{ + private static final String BASE_VERSION = "jcr:baseVersion"; + private static final String CREATED = "jcr:created"; + private static final String RESOURCE_TYPE = "sling:resourceType"; + private static final String NAME = "simple"; + private static final int PRIORITY = 25; + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private SimpleProcessor simpleProcessor; + + @Test + public void getNameReturnSimple() + { + assertEquals(NAME, this.simpleProcessor.getName()); + } + + @Test + public void getPriorityTest() + { + assertEquals(PRIORITY, this.simpleProcessor.getPriority()); + } + + @Test + public void isEnabledByDefaultTest() + { + assertFalse(this.simpleProcessor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void processPropertyForNullPropertyReturnsNull() + { + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), null, mock(JsonValue.class), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyForNotNullPropertyCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenThrow(new RepositoryException()); + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, input, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + @Test + public void processPropertyReturnsInput() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn(CREATED); + JsonValue input = mock(JsonValue.class); + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, input, + mock(Function.class)); + assertNotNull(jsonValue); + assertEquals(input, jsonValue); + } + + @Test + public void processPropertyForJcrPropertyReturnsNull() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn(BASE_VERSION); + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyForSlingPropertyReturnsNull() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn(RESOURCE_TYPE); + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), + mock(Function.class)); + assertNull(jsonValue); + } + + @Test + public void processPropertyForFormPropertyReturnsNull() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn("form"); + JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), + mock(Function.class)); + assertNull(jsonValue); + } + +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java new file mode 100644 index 0000000000..9a6c820839 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java @@ -0,0 +1,197 @@ +/* + * 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 io.uhndata.cards.utils.internal; + +import java.util.List; + +import org.apache.sling.api.request.builder.impl.SlingHttpServletRequestImpl; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceMetadata; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.ModificationType; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DenyScriptsSlingPostProcessor}. + * + * @version $Id$ + */ +@RunWith(MockitoJUnitRunner.class) +public class DenyScriptsSlingPostProcessorTest +{ + + @Rule + public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + + @InjectMocks + private DenyScriptsSlingPostProcessor denyScriptsSlingPostProcessor; + + @Test + public void processAllowsResourceWithNullResourceMetadata() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + Resource resource = mock(Resource.class); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + when(resourceResolver.getResource("/Forms/f1")).thenReturn(resource); + when(resource.getResourceMetadata()).thenReturn(null); + + Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) + .doesNotThrowAnyException(); + } + + @Test + public void processAllowsResourceWithNullContentType() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", null); + + Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) + .doesNotThrowAnyException(); + } + + @Test + public void processScriptResourceThrowsException() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "text/script;charset=UTF-8"); + + Assert.assertThrows("Script files are not allowed", Exception.class, + () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + } + + @Test + public void processHtmlResourceThrowsException() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "text/html;charset=UTF-8"); + + Assert.assertThrows("HTML files are not allowed", Exception.class, + () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + } + + @Test + public void processScriptResourceIgnoresCase() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "application/TypeScript"); + + Assert.assertThrows("Script files are not allowed", Exception.class, + () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + } + + @Test + public void processHtmlResourceIgnoresCase() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "application/XHTML"); + + Assert.assertThrows("HTML files are not allowed", Exception.class, + () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + } + + @Test + public void processAllowsOtherContentTypeResource() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "text/plain;charset=UTF-8"); + mockRecourseContentType(resourceResolver, "/Forms/f2", "text/plain;charset=UTF-8"); + + Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) + .doesNotThrowAnyException(); + } + + @Test + public void processAllowsAllForAdminUser() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + mockRecourseContentType(resourceResolver, "/Forms/f1", "application/XHTML"); + mockRecourseContentType(resourceResolver, "/Forms/f2", "application/TypeScript"); + + when(request.getRemoteUser()).thenReturn("admin"); + Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) + .doesNotThrowAnyException(); + } + + @Test + public void processAllowsNullResource() + { + SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + + when(request.getResourceResolver()).thenReturn(resourceResolver); + when(resourceResolver.getResource("/Forms/f1")).thenReturn(null); + when(resourceResolver.getResource("/Forms/f2")).thenReturn(null); + + Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) + .doesNotThrowAnyException(); + } + + private void mockRecourseContentType(ResourceResolver resourceResolver, String resourcePath, String contentType) + { + Resource resource = mock(Resource.class); + ResourceMetadata metadata = mock(ResourceMetadata.class); + when(resourceResolver.getResource(eq(resourcePath))).thenReturn(resource); + when(resource.getResourceMetadata()).thenReturn(metadata); + when(metadata.getContentType()).thenReturn(contentType); + } + +} diff --git a/modules/utils/src/test/resources/Questionnaires.json b/modules/utils/src/test/resources/Questionnaires.json new file mode 100644 index 0000000000..ea10b1c99c --- /dev/null +++ b/modules/utils/src/test/resources/Questionnaires.json @@ -0,0 +1,10 @@ +{ + "jcr:primaryType": "cards:Questionnaire", + "title": "Test Serializable Questionnaire", + "description": "A test serializable questionnaire", + "question_1": { + "jcr:primaryType": "cards:Question", + "text": "Long Question", + "dataType": "long" + } +} \ No newline at end of file diff --git a/modules/utils/src/test/resources/SubjectTypes.json b/modules/utils/src/test/resources/SubjectTypes.json new file mode 100644 index 0000000000..5620a1c833 --- /dev/null +++ b/modules/utils/src/test/resources/SubjectTypes.json @@ -0,0 +1,18 @@ +{ + "jcr:primaryType": "cards:SubjectType", + "label": "Root", + "subjectListLabel" : "Roots", + "cards:defaultOrder": 0, + "Branch": { + "jcr:primaryType": "cards:SubjectType", + "label": "Branch", + "subjectListLabel" : "Branches", + "cards:defaultOrder": 1, + "Leaf": { + "jcr:primaryType": "cards:SubjectType", + "label": "Leaf", + "subjectListLabel" : "Leafs", + "cards:defaultOrder": 2 + } + } +} \ No newline at end of file From b09a627fa7e3bfa37a7c7567273aa6c53883a493 Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu Date: Mon, 27 Jul 2026 19:06:28 +0000 Subject: [PATCH 2/2] CARDS-2154: Increase test coverage of cards-utils Modernize the revived tests and complete the module coverage to 100%: - migrate the tests to the current platform APIs: jakarta.json, SlingJakartaHttpServletRequest/Response, the jakartaResponse binding, Mockito 5, sling-mock 4 - drop the Mockito runner in favor of plain mocks with reflection injection, since concurrently running runners collide on Mockito's global listener registry under the parallel surefire configuration - raise the sling-mock resource resolver factory timeout, the default 500ms is flaky when several Oak-backed contexts start in parallel - add tests for the previously uncovered classes: SelectorServlet, DateUtils, SelectorDetails, BaseFilterFactory, DataFilter and the serialization SPI default methods, DefaultDataFilters(Parser), DefaultOptionsProcessor, ExcludeDefaultPropertiesProcessor, ImportableProcessor, ReferencedProcessor, PreventVersionOverrideServletFilter - fix a latent NPE: SelectorDetails built through the vararg options constructor with fewer than two option strings left the options array null, crashing SelectorServlet's getOptions().length - remove an unreachable branch in DateUtils.parseDateTime, parseBest either returns one of the two queried types or throws - remove the module's coverage exemption, restoring the default 1.00 required instruction coverage ratio --- modules/utils/pom.xml | 46 +--- .../cards/serialize/spi/SelectorDetails.java | 10 +- .../io/uhndata/cards/utils/DateUtils.java | 7 +- .../scripting/ContentTypeSetterTest.java | 43 +-- .../cards/scripting/StatusCodeSetterTest.java | 66 ++--- .../cards/serialize/CSVStringTest.java | 14 +- .../ResourceToCSVAdapterFactoryTest.java | 38 +-- .../ResourceToJsonAdapterFactoryTest.java | 137 +++++++--- .../ResourceToMarkdownAdapterFactoryTest.java | 36 +-- .../ResourceToTextAdapterFactoryTest.java | 51 ++-- .../serialize/api/SelectorServletTest.java | 188 +++++++++++++ .../serialize/internal/BareProcessorTest.java | 113 +++++--- .../serialize/internal/DeepProcessorTest.java | 38 ++- .../DefaultDataFiltersParserTest.java | 92 +++++++ .../internal/DefaultDataFiltersTest.java | 124 +++++++++ .../internal/DefaultOptionsProcessorTest.java | 109 ++++++++ .../internal/DereferenceProcessorTest.java | 58 ++-- ...ExcludeDefaultPropertiesProcessorTest.java | 256 ++++++++++++++++++ .../internal/IdentificationProcessorTest.java | 33 +-- .../internal/ImportableProcessorTest.java | 196 ++++++++++++++ .../internal/PropertiesProcessorTest.java | 47 ++-- .../internal/ReferencedProcessorTest.java | 102 +++++++ .../internal/SimpleProcessorTest.java | 50 ++-- .../serialize/spi/BaseFilterFactoryTest.java | 178 ++++++++++++ .../cards/serialize/spi/DataFilterTest.java | 70 +++++ .../spi/ResourceCSVProcessorTest.java | 53 ++++ .../spi/ResourceJsonProcessorTest.java | 122 +++++++++ .../spi/ResourceMarkdownProcessorTest.java | 47 ++++ .../spi/ResourceTextProcessorTest.java | 47 ++++ .../serialize/spi/SelectorDetailsTest.java | 115 ++++++++ .../io/uhndata/cards/utils/DateUtilsTest.java | 213 +++++++++++++++ .../DenyScriptsSlingPostProcessorTest.java | 150 ++++------ ...eventVersionOverrideServletFilterTest.java | 139 ++++++++++ 33 files changed, 2528 insertions(+), 460 deletions(-) create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/api/SelectorServletTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersParserTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultOptionsProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ExcludeDefaultPropertiesProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ImportableProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ReferencedProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/BaseFilterFactoryTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/DataFilterTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceCSVProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceJsonProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceMarkdownProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceTextProcessorTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/serialize/spi/SelectorDetailsTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/utils/DateUtilsTest.java create mode 100644 modules/utils/src/test/java/io/uhndata/cards/utils/internal/PreventVersionOverrideServletFilterTest.java diff --git a/modules/utils/pom.xml b/modules/utils/pom.xml index b3da6a2b37..084bc232b3 100644 --- a/modules/utils/pom.xml +++ b/modules/utils/pom.xml @@ -30,10 +30,6 @@ bundle CARDS - Utilities - - 0.45 - - @@ -57,14 +53,12 @@ - maven-compiler-plugin + maven-surefire-plugin - - -Werror - - true - true - true + + + 10000 + @@ -124,6 +118,11 @@ oak-api test + + org.apache.jackrabbit + oak-core + test + ${project.groupId} cards-data-model-items-api @@ -162,36 +161,25 @@ org.apache.sling org.apache.sling.testing.sling-mock.core - 3.2.2 - test - - - org.apache.sling - org.apache.sling.jcr.resource - 3.2.0 + 4.0.6 test org.apache.sling org.apache.sling.testing.sling-mock.junit4 - 3.2.2 + 4.0.6 test org.apache.sling org.apache.sling.testing.sling-mock-oak - 3.1.4-1.40.0 + 4.1.0-1.86.0 test org.apache.sling org.apache.sling.testing.jcr-mock - 1.5.4 - test - - - com.google.guava - guava + 1.8.2 test @@ -202,11 +190,5 @@ org.mockito mockito-core - - org.assertj - assertj-core - 3.24.2 - test - diff --git a/modules/utils/src/main/java/io/uhndata/cards/serialize/spi/SelectorDetails.java b/modules/utils/src/main/java/io/uhndata/cards/serialize/spi/SelectorDetails.java index 09ddf80ab8..7b2dca0f63 100644 --- a/modules/utils/src/main/java/io/uhndata/cards/serialize/spi/SelectorDetails.java +++ b/modules/utils/src/main/java/io/uhndata/cards/serialize/spi/SelectorDetails.java @@ -80,12 +80,10 @@ public SelectorDetails(String name, String description, Boolean enabledByDefault this.name = name; this.description = description; this.enabledByDefault = enabledByDefault; - if (options.length >= 2) { - int numOptions = Math.floorDiv(options.length, 2); - this.options = new SelectorOption[numOptions]; - for (int i = 0; i < numOptions; i++) { - this.options[i] = new SelectorOption(options[2 * i], options[2 * i + 1]); - } + int numOptions = Math.floorDiv(options.length, 2); + this.options = new SelectorOption[numOptions]; + for (int i = 0; i < numOptions; i++) { + this.options[i] = new SelectorOption(options[2 * i], options[2 * i + 1]); } } diff --git a/modules/utils/src/main/java/io/uhndata/cards/utils/DateUtils.java b/modules/utils/src/main/java/io/uhndata/cards/utils/DateUtils.java index aea99d29ac..74f744dbb5 100644 --- a/modules/utils/src/main/java/io/uhndata/cards/utils/DateUtils.java +++ b/modules/utils/src/main/java/io/uhndata/cards/utils/DateUtils.java @@ -141,14 +141,13 @@ public static ZonedDateTime parseDateTime(final String str) if (result instanceof ZonedDateTime) { // Good, we managed to parse the timezone from the string, just return the result return (ZonedDateTime) result; - } else if (result instanceof LocalDateTime) { - // No timezone in the string, use the system default - return ((LocalDateTime) result).atZone(ZoneId.systemDefault()); } + // No timezone in the string, use the system default + return ((LocalDateTime) result).atZone(ZoneId.systemDefault()); } catch (Exception ex) { // Not important, the date string doesn't match the expected format, just try the next format + return null; } - return null; }).filter(Objects::nonNull).findFirst().orElse(null); return date; } diff --git a/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java b/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java index 8e3572c31c..07ffcff567 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/scripting/ContentTypeSetterTest.java @@ -18,78 +18,79 @@ import javax.script.Bindings; -import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.SlingJakartaHttpServletResponse; +import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Unit tests for {@link ContentTypeSetter}. * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class ContentTypeSetterTest { - @InjectMocks - private ContentTypeSetter contentTypeSetter; + private final ContentTypeSetter contentTypeSetter = new ContentTypeSetter(); - @Mock - private SlingHttpServletResponse response; + private final Bindings bindings = mock(Bindings.class); + + private final SlingJakartaHttpServletResponse response = mock(SlingJakartaHttpServletResponse.class); + + @Before + public void setUp() + { + when(this.bindings.get("jakartaResponse")).thenReturn(this.response); + this.contentTypeSetter.init(this.bindings); + } @Test - public void initGetsResponseKeyFromBindings() + public void initGetsJakartaResponseFromBindings() { - Bindings bindings = mock(Bindings.class); - this.contentTypeSetter.init(bindings); - verify(bindings, times(1)).get("response"); + verify(this.bindings).get("jakartaResponse"); } @Test public void htmlSetsHtmlContentType() { this.contentTypeSetter.html(); - verify(this.response, times(1)).setContentType("text/html;charset=UTF-8"); + verify(this.response).setContentType("text/html;charset=UTF-8"); } @Test public void javascriptSetsJavascriptContentType() { this.contentTypeSetter.javascript(); - verify(this.response, times(1)).setContentType("application/javascript;charset=UTF-8"); + verify(this.response).setContentType("application/javascript;charset=UTF-8"); } @Test public void jsonSetsJsonContentType() { this.contentTypeSetter.json(); - verify(this.response, times(1)).setContentType("application/json;charset=UTF-8"); + verify(this.response).setContentType("application/json;charset=UTF-8"); } @Test public void csvSetsCsvContentType() { this.contentTypeSetter.csv(); - verify(this.response, times(1)).setContentType("text/csv;charset=UTF-8"); + verify(this.response).setContentType("text/csv;charset=UTF-8"); } @Test public void textSetsPlainContentType() { this.contentTypeSetter.text(); - verify(this.response, times(1)).setContentType("text/plain;charset=UTF-8"); + verify(this.response).setContentType("text/plain;charset=UTF-8"); } @Test public void markdownSetsMarkdownContentType() { this.contentTypeSetter.markdown(); - verify(this.response, times(1)).setContentType("text/markdown;charset=UTF-8"); + verify(this.response).setContentType("text/markdown;charset=UTF-8"); } } diff --git a/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java b/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java index ad7a99b2a9..035e92007a 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/scripting/StatusCodeSetterTest.java @@ -17,138 +17,140 @@ package io.uhndata.cards.scripting; import javax.script.Bindings; -import javax.servlet.http.HttpServletResponse; -import org.apache.sling.api.SlingHttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingJakartaHttpServletResponse; +import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Unit tests for {@link StatusCodeSetter}. * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class StatusCodeSetterTest { private static final int LOCKED = 423; - @InjectMocks - private StatusCodeSetter statusCodeSetter; - @Mock - private SlingHttpServletResponse response; + private final StatusCodeSetter statusCodeSetter = new StatusCodeSetter(); + + private final Bindings bindings = mock(Bindings.class); + + private final SlingJakartaHttpServletResponse response = mock(SlingJakartaHttpServletResponse.class); + + @Before + public void setUp() + { + when(this.bindings.get("jakartaResponse")).thenReturn(this.response); + this.statusCodeSetter.init(this.bindings); + } @Test - public void initGetsResponseKeyFromBindings() + public void initGetsJakartaResponseFromBindings() { - Bindings bindings = mock(Bindings.class); - this.statusCodeSetter.init(bindings); - verify(bindings, times(1)).get("response"); + verify(this.bindings).get("jakartaResponse"); } @Test public void okSetsOkStatus() { this.statusCodeSetter.ok(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_OK); + verify(this.response).setStatus(HttpServletResponse.SC_OK); } @Test public void createdSetsCreatedStatus() { this.statusCodeSetter.created(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_CREATED); + verify(this.response).setStatus(HttpServletResponse.SC_CREATED); } @Test public void acceptedSetsAcceptedStatus() { this.statusCodeSetter.accepted(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_ACCEPTED); + verify(this.response).setStatus(HttpServletResponse.SC_ACCEPTED); } @Test - public void noContentNoContentSetsStatus() + public void noContentSetsNoContentStatus() { this.statusCodeSetter.noContent(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NO_CONTENT); + verify(this.response).setStatus(HttpServletResponse.SC_NO_CONTENT); } @Test public void badRequestSetsBadRequestStatus() { this.statusCodeSetter.badRequest(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_BAD_REQUEST); + verify(this.response).setStatus(HttpServletResponse.SC_BAD_REQUEST); } @Test public void unauthorizedSetsUnauthorizedStatus() { this.statusCodeSetter.unauthorized(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(this.response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } @Test public void forbiddenSetsForbiddenStatus() { this.statusCodeSetter.forbidden(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_FORBIDDEN); + verify(this.response).setStatus(HttpServletResponse.SC_FORBIDDEN); } @Test public void notFoundSetsNotFoundStatus() { this.statusCodeSetter.notFound(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_FOUND); + verify(this.response).setStatus(HttpServletResponse.SC_NOT_FOUND); } @Test public void methodNotAllowedSetsMethodNotAllowedStatus() { this.statusCodeSetter.methodNotAllowed(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + verify(this.response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } @Test public void notAcceptableSetsNotAcceptableStatus() { this.statusCodeSetter.notAcceptable(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + verify(this.response).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); } @Test public void conflictSetsConflictStatus() { this.statusCodeSetter.conflict(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_CONFLICT); + verify(this.response).setStatus(HttpServletResponse.SC_CONFLICT); } @Test public void lockedSetsLockedStatus() { this.statusCodeSetter.locked(); - verify(this.response, times(1)).setStatus(LOCKED); + verify(this.response).setStatus(LOCKED); } @Test public void internalServerErrorSetsInternalServerErrorStatus() { this.statusCodeSetter.internalServerError(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(this.response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } @Test public void notImplementedSetsNotImplementedStatus() { this.statusCodeSetter.notImplemented(); - verify(this.response, times(1)).setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); + verify(this.response).setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); } - } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java index 221805304c..228c8a4c71 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/CSVStringTest.java @@ -17,10 +17,6 @@ package io.uhndata.cards.serialize; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.internal.util.reflection.Whitebox; -import org.mockito.runners.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -30,24 +26,18 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class CSVStringTest { - @InjectMocks - private CSVString csvString; - @Test public void toStringReturnsInputData() { String input = "Input\ndata"; - Whitebox.setInternalState(this.csvString, "data", input); - assertEquals(input, this.csvString.toString()); + assertEquals(input, new CSVString(input).toString()); } @Test public void toStringWithNullDataReturnsNull() { - Whitebox.setInternalState(this.csvString, "data", null); - assertNull(this.csvString.toString()); + assertNull(new CSVString(null).toString()); } } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java index 4b7ca130e5..dbdc64f82a 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToCSVAdapterFactoryTest.java @@ -19,21 +19,15 @@ import java.util.List; import java.util.UUID; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.internal.util.reflection.Whitebox; -import org.mockito.runners.MockitoJUnitRunner; import io.uhndata.cards.serialize.spi.ResourceCSVProcessor; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -44,18 +38,13 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class ResourceToCSVAdapterFactoryTest { private static final String NODE_IDENTIFIER = "jcr:uuid"; private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; private static final String TEST_SUBJECT_PATH = "/Subjects/Test"; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - - @InjectMocks - private ResourceToCSVAdapterFactory factory; + private final ResourceToCSVAdapterFactory factory = new ResourceToCSVAdapterFactory(); @Test public void getAdapterForNullAdaptableObjectReturnsNull() @@ -64,7 +53,7 @@ public void getAdapterForNullAdaptableObjectReturnsNull() } @Test - public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() throws IllegalAccessException { Resource adaptable = mock(Resource.class); String identifier = UUID.randomUUID().toString(); @@ -75,41 +64,41 @@ public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() when(processor.canProcess(adaptable)).thenReturn(true); when(processor.serialize(adaptable)).thenReturn(data); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); assertNotNull(adapter); assertEquals(data, adapter.toString()); } @Test - public void getAdapterForUnsupportedResourceReturnsResourcePath() + public void getAdapterForUnsupportedResourceReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); ResourceCSVProcessor processor = mock(ResourceCSVProcessor.class); when(adaptable.getPath()).thenReturn(TEST_SUBJECT_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); assertNotNull(adapter); assertEquals(TEST_SUBJECT_PATH, adapter.toString()); } @Test - public void getAdapterWithNoProcessorsReturnsResourcePath() + public void getAdapterWithNoProcessorsReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); when(adaptable.getPath()).thenReturn(TEST_SUBJECT_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + setProcessors(List.of()); CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); assertNotNull(adapter); assertEquals(TEST_SUBJECT_PATH, adapter.toString()); } @Test - public void getAdapterUsesFirstProcessorThatCanProcess() + public void getAdapterUsesFirstProcessorThatCanProcess() throws IllegalAccessException { Resource adaptable = mock(Resource.class); @@ -124,8 +113,7 @@ public void getAdapterUsesFirstProcessorThatCanProcess() ResourceCSVProcessor processor3 = mock(ResourceCSVProcessor.class); - Whitebox.setInternalState(this.factory, "allProcessors", - List.of(processor1, processor2, processor3)); + setProcessors(List.of(processor1, processor2, processor3)); CSVString adapter = this.factory.getAdapter(adaptable, CSVString.class); verify(processor1, times(0)).serialize(adaptable); verify(processor2, times(1)).serialize(adaptable); @@ -134,4 +122,8 @@ public void getAdapterUsesFirstProcessorThatCanProcess() assertEquals(data, adapter.toString()); } + private void setProcessors(final List processors) throws IllegalAccessException + { + FieldUtils.writeField(this.factory, "allProcessors", processors, true); + } } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java index 223a77c13d..de83797d59 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToJsonAdapterFactoryTest.java @@ -23,11 +23,14 @@ import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; -import javax.json.Json; -import javax.json.JsonNumber; -import javax.json.JsonObject; -import javax.json.JsonValue; +import jakarta.json.Json; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; + +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.jackrabbit.oak.api.Type; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceMetadata; @@ -36,10 +39,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.internal.util.reflection.Whitebox; -import org.mockito.runners.MockitoJUnitRunner; import io.uhndata.cards.serialize.spi.ResourceJsonProcessor; @@ -58,7 +57,6 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class ResourceToJsonAdapterFactoryTest { private static final String NODE_TYPE = "jcr:primaryType"; @@ -78,8 +76,7 @@ public class ResourceToJsonAdapterFactoryTest @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - @InjectMocks - private ResourceToJsonAdapterFactory factory; + private final ResourceToJsonAdapterFactory factory = new ResourceToJsonAdapterFactory(); @Test public void getAdapterForNullAdaptableObjectReturnsNull() @@ -94,7 +91,7 @@ public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); assertNotNull(adapter); @@ -107,7 +104,7 @@ public void getAdapterForNullNodeReturnsNull() when(adaptable.adaptTo(Node.class)).thenReturn(null); ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertNull(adapter); } @@ -126,7 +123,7 @@ public void getAdapterCatchesRepositoryExceptionReturnsNull() throws RepositoryE ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertNull(adapter); } @@ -144,7 +141,7 @@ public void getAdapterUsesAllSupportedAndEnabledProcessors() ResourceJsonProcessor processor3 = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor3, adaptable, false, true, TEST_PROCESSOR_NAME + "3"); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor1, processor2, processor3)); + setProcessors(List.of(processor1, processor2, processor3)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); verifyProcessorMethodsInvocation(processor1, 0, 0, 0, 0); verifyProcessorMethodsInvocation(processor2, 1, 1, 15, 2); @@ -162,8 +159,7 @@ public void getAdapterSortsProcessors() TestResourceJsonProcessor processor3 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "3", 3, true, 3, 2); TestResourceJsonProcessor processor4 = new TestResourceJsonProcessor(TEST_PROCESSOR_NAME + "4", 4, true, 4, 1); - Whitebox.setInternalState(this.factory, "allProcessors", - List.of(processor1, processor3, processor4, processor2)); + setProcessors(List.of(processor1, processor3, processor4, processor2)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertNotNull(adapter); assertEquals(141, adapter.getInt(NODE_TYPE)); @@ -174,7 +170,7 @@ public void getAdapterWithNoProcessorsReturnsEmptyJsonObject() { Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + setProcessors(List.of()); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertNotNull(adapter); assertTrue(adapter.isEmpty()); @@ -187,7 +183,7 @@ public void getAdapterWithNoSupportedProcessorsReturnsEmptyJsonObject() ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, true, false, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertNotNull(adapter); assertTrue(adapter.isEmpty()); @@ -201,7 +197,7 @@ public void getAdapterUsesResourceSelectorsToDisableDefaultProcessors() ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, true, true, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); this.factory.getAdapter(adaptable, JsonObject.class); // There is no enabled processor, so these methods are not invoked @@ -216,7 +212,7 @@ public void getAdapterUsesResourceSelectorsToEnableProcessors() ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, false, true, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); this.factory.getAdapter(adaptable, JsonObject.class); // Methods of not enabledByDefault processor are invoked @@ -231,7 +227,7 @@ public void getAdapterWithBothEnableAndDisableSelectorsPrioritizesEnable() ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processor, adaptable, false, true, TEST_PROCESSOR_NAME); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); this.factory.getAdapter(adaptable, JsonObject.class); verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); @@ -245,19 +241,34 @@ public void getAdapterWithRecursiveReferencesUsesResourcePathForNestedReferences adaptableNode.setProperty("form", adaptableNode.getIdentifier(), Type.REFERENCE.tag()); ResourceJsonProcessor processor = new FormRecursiveTestResourceJsonProcessor(); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertEquals(TEST_FORM_PATH, adapter.getString("form")); } + @Test + public void getAdapterOffersTheNodeSerializerInAllLifecyclePhases() + { + Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); + + setProcessors(List.of(new CallbackTestResourceJsonProcessor())); + JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); + + assertNotNull(adapter); + // Serializing a null node through the callback yields null + assertTrue(adapter.getBoolean("@nullSerializedAsNull")); + // The callback offered to leave() serialized the answer child + assertTrue(adapter.containsKey("@leaveChild")); + } + @Test public void getAdapterWithProcessedAnswerChildNode() { Resource adaptable = this.context.resourceResolver().getResource(TEST_FORM_PATH); ResourceJsonProcessor processor = new ChildNodeTestResourceJsonProcessor(); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); JsonObject adapter = this.factory.getAdapter(adaptable, JsonObject.class); assertTrue(adapter.containsKey("a1")); @@ -274,7 +285,7 @@ public void getAdapterWithBothInvokedAndNotInvokedDefaultProcessors() ResourceJsonProcessor processorNotInvoked = mock(ResourceJsonProcessor.class); mockWorkingProcessor(processorNotInvoked, adaptable, true, true, "test_not_invoked"); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor, processorNotInvoked)); + setProcessors(List.of(processor, processorNotInvoked)); this.factory.getAdapter(adaptable, JsonObject.class); verifyProcessorMethodsInvocation(processor, 1, 1, 15, 2); @@ -333,6 +344,15 @@ private void mockWorkingProcessor(ResourceJsonProcessor processor, Resource adap when(processor.canProcess(adaptable)).thenReturn(canProcess); } + private void setProcessors(final List processors) + { + try { + FieldUtils.writeField(this.factory, "allProcessors", processors, true); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + private void verifyProcessorMethodsInvocation(ResourceJsonProcessor processor, int startAndEndProcess, int enterAndLeaveProcess, int processProperty, int processChild) { @@ -344,21 +364,21 @@ private void verifyProcessorMethodsInvocation(ResourceJsonProcessor processor, i verify(processor, times(startAndEndProcess)).end(any()); } - private static class TestResourceJsonProcessor implements ResourceJsonProcessor + private static final class TestResourceJsonProcessor implements ResourceJsonProcessor { private final String name; private final int priority; private final boolean isEnabledByDefault; - private final int a; - private final int b; + private final int factor; + private final int offset; - TestResourceJsonProcessor(String name, int priority, boolean isEnabledByDefault, int a, int b) + TestResourceJsonProcessor(String name, int priority, boolean isEnabledByDefault, int factor, int offset) { this.name = name; this.priority = priority; this.isEnabledByDefault = isEnabledByDefault; - this.a = a; - this.b = b; + this.factor = factor; + this.offset = offset; } @Override @@ -378,11 +398,13 @@ public boolean isEnabledByDefault(final Resource resource) { return this.isEnabledByDefault; } + @Override public JsonValue processProperty(final Node node, final Property property, final JsonValue input, final Function serializeNode) { - return Json.createValue(input == null ? this.b : ((JsonNumber) input).intValue() * this.a + this.b); + return Json.createValue( + input == null ? this.offset : ((JsonNumber) input).intValue() * this.factor + this.offset); } @Override @@ -392,7 +414,54 @@ public String getDescription() } } - private static class FormRecursiveTestResourceJsonProcessor implements ResourceJsonProcessor + private static final class CallbackTestResourceJsonProcessor implements ResourceJsonProcessor + { + @Override + public String getName() + { + return "callback"; + } + + @Override + public int getPriority() + { + return 1; + } + + @Override + public boolean isEnabledByDefault(final Resource resource) + { + return true; + } + + @Override + public void enter(final Node node, final JsonObjectBuilder json, + final Function serializeNode) + { + json.add("@nullSerializedAsNull", serializeNode.apply(null) == null); + } + + @Override + public void leave(final Node node, final JsonObjectBuilder json, + final Function serializeNode) + { + try { + if (node.hasNode("a1")) { + json.add("@leaveChild", serializeNode.apply(node.getNode("a1"))); + } + } catch (RepositoryException e) { + // Should not happen + } + } + + @Override + public String getDescription() + { + return "CallbackTestResourceJsonProcessor"; + } + } + + private static final class FormRecursiveTestResourceJsonProcessor implements ResourceJsonProcessor { @Override public String getName() @@ -441,7 +510,7 @@ public String getDescription() } } - private static class ChildNodeTestResourceJsonProcessor implements ResourceJsonProcessor + private static final class ChildNodeTestResourceJsonProcessor implements ResourceJsonProcessor { @Override public String getName() diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java index 0f8041d84a..5768d6f87b 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToMarkdownAdapterFactoryTest.java @@ -19,15 +19,9 @@ import java.util.List; import java.util.UUID; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.internal.util.reflection.Whitebox; -import org.mockito.runners.MockitoJUnitRunner; import io.uhndata.cards.serialize.spi.ResourceMarkdownProcessor; @@ -44,18 +38,13 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class ResourceToMarkdownAdapterFactoryTest { private static final String NODE_IDENTIFIER = "jcr:uuid"; private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; private static final String TEST_FORM_PATH = "/Forms/f1"; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - - @InjectMocks - private ResourceToMarkdownAdapterFactory factory; + private final ResourceToMarkdownAdapterFactory factory = new ResourceToMarkdownAdapterFactory(); @Test public void getAdapterForNullAdaptableObjectReturnsNull() @@ -64,7 +53,7 @@ public void getAdapterForNullAdaptableObjectReturnsNull() } @Test - public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() throws IllegalAccessException { Resource adaptable = mock(Resource.class); String identifier = UUID.randomUUID().toString(); @@ -75,41 +64,41 @@ public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() when(processor.canProcess(adaptable)).thenReturn(true); when(processor.serialize(adaptable)).thenReturn(data); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); assertNotNull(adapter); assertEquals(data, adapter); } @Test - public void getAdapterForUnsupportedResourceReturnsResourcePath() + public void getAdapterForUnsupportedResourceReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); ResourceMarkdownProcessor processor = mock(ResourceMarkdownProcessor.class); when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); assertNotNull(adapter); assertEquals(TEST_FORM_PATH, adapter); } @Test - public void getAdapterWithNoProcessorsReturnsResourcePath() + public void getAdapterWithNoProcessorsReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + setProcessors(List.of()); CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); assertNotNull(adapter); assertEquals(TEST_FORM_PATH, adapter); } @Test - public void getAdapterUsesFirstProcessorThatCanProcess() + public void getAdapterUsesFirstProcessorThatCanProcess() throws IllegalAccessException { Resource adaptable = mock(Resource.class); @@ -124,8 +113,7 @@ public void getAdapterUsesFirstProcessorThatCanProcess() ResourceMarkdownProcessor processor3 = mock(ResourceMarkdownProcessor.class); - Whitebox.setInternalState(this.factory, "allProcessors", - List.of(processor1, processor2, processor3)); + setProcessors(List.of(processor1, processor2, processor3)); CharSequence adapter = this.factory.getAdapter(adaptable, CharSequence.class); verify(processor1, times(0)).serialize(adaptable); verify(processor2, times(1)).serialize(adaptable); @@ -134,4 +122,8 @@ public void getAdapterUsesFirstProcessorThatCanProcess() assertEquals(data, adapter); } + private void setProcessors(final List processors) throws IllegalAccessException + { + FieldUtils.writeField(this.factory, "allProcessors", processors, true); + } } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java index c5ab760355..2a9fd7b92b 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/ResourceToTextAdapterFactoryTest.java @@ -17,23 +17,14 @@ package io.uhndata.cards.serialize; import java.util.List; -import java.util.Map; import java.util.UUID; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.sling.api.resource.Resource; -import org.apache.sling.jcr.resource.internal.HelperData; -import org.apache.sling.jcr.resource.internal.helper.jcr.JcrItemResourceFactory; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.apache.sling.testing.mock.sling.junit.SlingContext; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.internal.util.reflection.Whitebox; -import org.mockito.runners.MockitoJUnitRunner; import io.uhndata.cards.serialize.spi.ResourceTextProcessor; @@ -50,19 +41,16 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class ResourceToTextAdapterFactoryTest { private static final String NODE_IDENTIFIER = "jcr:uuid"; - private static final String NODE_TYPE = "jcr:primaryType"; private static final String CREATED_BY_PROPERTY = "jcr:createdBy"; private static final String TEST_FORM_PATH = "/Forms/f1"; @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - @InjectMocks - private ResourceToTextAdapterFactory factory; + private final ResourceToTextAdapterFactory factory = new ResourceToTextAdapterFactory(); @Test public void getAdapterForNullAdaptableObjectReturnsNull() @@ -71,7 +59,7 @@ public void getAdapterForNullAdaptableObjectReturnsNull() } @Test - public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() + public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() throws IllegalAccessException { Resource adaptable = mock(Resource.class); String identifier = UUID.randomUUID().toString(); @@ -82,41 +70,41 @@ public void getAdapterForResourceAdaptableObjectReturnsSerializedAdapter() when(processor.canProcess(adaptable)).thenReturn(true); when(processor.serialize(adaptable)).thenReturn(data); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); String adapter = this.factory.getAdapter(adaptable, String.class); assertNotNull(adapter); assertEquals(data, adapter); } @Test - public void getAdapterForUnsupportedResourceReturnsResourcePath() + public void getAdapterForUnsupportedResourceReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); ResourceTextProcessor processor = mock(ResourceTextProcessor.class); when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of(processor)); + setProcessors(List.of(processor)); String adapter = this.factory.getAdapter(adaptable, String.class); assertNotNull(adapter); assertEquals(TEST_FORM_PATH, adapter); } @Test - public void getAdapterWithNoProcessorsReturnsResourcePath() + public void getAdapterWithNoProcessorsReturnsResourcePath() throws IllegalAccessException { Resource adaptable = mock(Resource.class); when(adaptable.getPath()).thenReturn(TEST_FORM_PATH); - Whitebox.setInternalState(this.factory, "allProcessors", List.of()); + setProcessors(List.of()); String adapter = this.factory.getAdapter(adaptable, String.class); assertNotNull(adapter); assertEquals(TEST_FORM_PATH, adapter); } @Test - public void getAdapterUsesFirstProcessorThatCanProcess() + public void getAdapterUsesFirstProcessorThatCanProcess() throws IllegalAccessException { Resource adaptable = mock(Resource.class); @@ -131,8 +119,7 @@ public void getAdapterUsesFirstProcessorThatCanProcess() ResourceTextProcessor processor3 = mock(ResourceTextProcessor.class); - Whitebox.setInternalState(this.factory, "allProcessors", - List.of(processor1, processor2, processor3)); + setProcessors(List.of(processor1, processor2, processor3)); String adapter = this.factory.getAdapter(adaptable, String.class); verify(processor1, times(0)).serialize(adaptable); verify(processor2, times(1)).serialize(adaptable); @@ -142,16 +129,18 @@ public void getAdapterUsesFirstProcessorThatCanProcess() } @Test - public void getAdapterForJcrPropertyResourceReturnsNull() throws RepositoryException + public void getAdapterForJcrPropertyResourceReturnsNull() throws IllegalAccessException { - this.context.build().resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage").commit(); - this.context.build() - .resource("/SubjectTypes/Root", NODE_TYPE, "cards:SubjectType", "label", "Root").commit(); - Resource resource = new JcrItemResourceFactory( - this.context.resourceResolver().adaptTo(Session.class), mock(HelperData.class)) - .createResource(this.context.resourceResolver(), "/SubjectTypes/Root/label", - this.context.resourceResolver().getResource("/SubjectTypes/Root"), Map.of()); + this.context.build().resource(TEST_FORM_PATH, "label", "First form").commit(); + Resource resource = this.context.resourceResolver().getResource(TEST_FORM_PATH + "/label"); + assertNotNull(resource); + + setProcessors(List.of()); assertNull(this.factory.getAdapter(resource, String.class)); } + private void setProcessors(final List processors) throws IllegalAccessException + { + FieldUtils.writeField(this.factory, "allProcessors", processors, true); + } } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/api/SelectorServletTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/api/SelectorServletTest.java new file mode 100644 index 0000000000..216ea506fb --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/api/SelectorServletTest.java @@ -0,0 +1,188 @@ +/* + * 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 io.uhndata.cards.serialize.api; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.List; + +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.sling.api.SlingJakartaHttpServletRequest; +import org.apache.sling.api.SlingJakartaHttpServletResponse; +import org.junit.Before; +import org.junit.Test; + +import io.uhndata.cards.serialize.spi.DataFilterFactory; +import io.uhndata.cards.serialize.spi.ResourceCSVProcessor; +import io.uhndata.cards.serialize.spi.ResourceJsonProcessor; +import io.uhndata.cards.serialize.spi.SelectorDetails; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link SelectorServlet}. + * + * @version $Id$ + */ +public class SelectorServletTest +{ + private static final String PROCESSORS = "Processors"; + + private static final String FILTERS = "Filters"; + + private static final String CSV_OPTIONS = "CSV Adapter Options"; + + private final SelectorServlet servlet = new SelectorServlet(); + + private final SlingJakartaHttpServletRequest request = mock(SlingJakartaHttpServletRequest.class); + + private final SlingJakartaHttpServletResponse response = mock(SlingJakartaHttpServletResponse.class); + + private final StringWriter output = new StringWriter(); + + @Before + public void setUp() throws IllegalAccessException, IOException + { + setProcessors(List.of(), List.of(), List.of()); + when(this.response.getWriter()).thenReturn(new PrintWriter(this.output)); + } + + @Test + public void doGetWritesJsonByDefault() throws IllegalAccessException, IOException + { + setProcessors( + List.of(jsonProcessor(new SelectorDetails("bare", "Bare serialization", true))), + List.of(filterFactory(new SelectorDetails("status", "Filter by status", "include", "Statuses to keep"))), + List.of(csvProcessor(new SelectorDetails("labels", "Use labels")))); + when(this.request.getPathInfo()).thenReturn("/Forms.selectors.json"); + + this.servlet.doGet(this.request, this.response); + + verify(this.response).setCharacterEncoding("UTF-8"); + verify(this.response).setContentType("application/json"); + JsonObject result = Json.createReader(new StringReader(this.output.toString())).readObject(); + JsonObject processor = result.getJsonObject(PROCESSORS).getJsonObject("bare"); + assertEquals("Bare serialization", processor.getString("description")); + assertTrue(processor.getBoolean("isEnabledByDefault")); + assertFalse(processor.containsKey("options")); + JsonObject filter = result.getJsonObject(FILTERS).getJsonObject("status"); + assertFalse(filter.containsKey("isEnabledByDefault")); + assertEquals("Statuses to keep", filter.getJsonObject("options").getString("include")); + assertEquals("Use labels", result.getJsonObject(CSV_OPTIONS).getJsonObject("labels").getString("description")); + } + + @Test + public void doGetGroupsSameNameSelectorsInJsonArray() throws IllegalAccessException, IOException + { + setProcessors( + List.of(jsonProcessor(new SelectorDetails("bare", "First implementation")), + jsonProcessor(new SelectorDetails("bare", "Second implementation"))), + List.of(), List.of()); + when(this.request.getPathInfo()).thenReturn("/Forms.selectors.json"); + + this.servlet.doGet(this.request, this.response); + + JsonObject result = Json.createReader(new StringReader(this.output.toString())).readObject(); + assertEquals(2, result.getJsonObject(PROCESSORS).getJsonArray("bare").size()); + assertEquals("First implementation", + result.getJsonObject(PROCESSORS).getJsonArray("bare").getJsonObject(0).getString("description")); + } + + @Test + public void doGetWritesMarkdownForMdRequests() throws IllegalAccessException, IOException + { + setProcessors( + List.of(jsonProcessor( + new SelectorDetails("bare", "Bare serialization\nSecond line", true, "exclude", "What to exclude"))), + List.of(), List.of()); + when(this.request.getPathInfo()).thenReturn("/Forms.selectors.md"); + + this.servlet.doGet(this.request, this.response); + + verify(this.response).setContentType("text/markdown"); + String result = this.output.toString(); + assertTrue(result.contains("## " + PROCESSORS)); + assertTrue(result.contains("## " + FILTERS)); + assertTrue(result.contains("## " + CSV_OPTIONS)); + assertTrue(result.contains("### bare")); + // Newlines in the description are turned into markdown line breaks + assertTrue(result.contains("Bare serialization \nSecond line")); + assertTrue(result.contains("**Enabled by default**")); + assertTrue(result.contains("#### Options")); + assertTrue(result.contains(" - **exclude**: What to exclude")); + } + + @Test + public void doGetListsSameNameSelectorsAsMarkdownImplementations() throws IllegalAccessException, IOException + { + setProcessors( + List.of(jsonProcessor(new SelectorDetails("bare", "First implementation")), + jsonProcessor(new SelectorDetails("bare", "Second implementation", true, "exclude", "The exclusions"))), + List.of(), List.of()); + when(this.request.getPathInfo()).thenReturn("/Forms.selectors.md"); + + this.servlet.doGet(this.request, this.response); + + String result = this.output.toString(); + assertTrue(result.contains("#### Implementations")); + assertTrue(result.contains("1. First implementation")); + assertTrue(result.contains("2. Second implementation")); + // Options of a listed implementation are indented and nested one header level deeper + assertTrue(result.contains(" ##### Options")); + assertTrue(result.contains(" - **exclude**: The exclusions")); + } + + private ResourceJsonProcessor jsonProcessor(final SelectorDetails details) + { + ResourceJsonProcessor processor = mock(ResourceJsonProcessor.class); + when(processor.getDetails()).thenReturn(details); + return processor; + } + + private DataFilterFactory filterFactory(final SelectorDetails... details) + { + DataFilterFactory factory = mock(DataFilterFactory.class); + when(factory.getFilterDetails()).thenReturn(List.of(details)); + return factory; + } + + private ResourceCSVProcessor csvProcessor(final SelectorDetails... details) + { + ResourceCSVProcessor processor = mock(ResourceCSVProcessor.class); + when(processor.getDetails()).thenReturn(List.of(details)); + return processor; + } + + private void setProcessors(final List jsonProcessors, + final List filters, final List csvProcessors) + throws IllegalAccessException + { + FieldUtils.writeField(this.servlet, "allJsonProcessors", jsonProcessors, true); + FieldUtils.writeField(this.servlet, "allFilters", filters, true); + FieldUtils.writeField(this.servlet, "allCSVProcessors", csvProcessors, true); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java index c93bc05033..e00f90d411 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/BareProcessorTest.java @@ -21,7 +21,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.function.Function; import javax.jcr.Binary; import javax.jcr.Node; @@ -29,12 +28,14 @@ import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonObjectBuilder; -import javax.json.JsonString; -import javax.json.JsonValue; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.jackrabbit.oak.api.Type; import org.apache.sling.api.resource.Resource; import org.apache.sling.testing.mock.sling.ResourceResolverType; @@ -42,10 +43,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -62,7 +59,6 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class BareProcessorTest { private static final String NODE_TYPE = "jcr:primaryType"; @@ -84,42 +80,81 @@ public class BareProcessorTest @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - @InjectMocks - private BareProcessor bareProcessor; + private final BareProcessor bareProcessor = new BareProcessor(); - @Mock - private ThreadLocal depth; + @SuppressWarnings("unchecked") + private final ThreadLocal depth = mock(ThreadLocal.class); @Test - public void getNameReturnBare() + public void getNameReturnsBare() { assertEquals(NAME, this.bareProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsNinety() { assertEquals(PRIORITY, this.bareProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsFalse() { assertFalse(this.bareProcessor.isEnabledByDefault(mock(Resource.class))); } @Test - public void startTest() + public void getDescriptionIsNotEmpty() + { + assertFalse(this.bareProcessor.getDescription().isEmpty()); + } + + @Test + public void startResetsDepth() { this.bareProcessor.start(mock(Resource.class)); verify(this.depth).set(0); } @Test - public void enterTest() + public void depthStartsAtZero() throws RepositoryException + { + // A pristine processor, without the mocked depth counter, to check the counter's initial value + BareProcessor processor = new BareProcessor(); + Node node = mock(Node.class); + processor.enter(node, Json.createObjectBuilder(), n -> JsonValue.NULL); + + // A balanced enter+leave returns to depth 0, where the root node's metadata is looked up + JsonObjectBuilder json = Json.createObjectBuilder(); + processor.leave(node, json, n -> JsonValue.NULL); + verify(node).hasProperty("jcr:created"); + assertTrue(json.build().isEmpty()); + } + + @Test + public void leaveWithoutResourceChildDoesNotAddContent() throws RepositoryException + { + Node node = mock(Node.class); + when(this.depth.get()).thenReturn(2, 1); + + NodeIterator iterator = mock(NodeIterator.class); + Node child = mock(Node.class); + when(node.isNodeType("nt:file")).thenReturn(true); + when(node.getNodes()).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true, false); + when(iterator.nextNode()).thenReturn(child); + when(child.isNodeType("nt:resource")).thenReturn(false); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.bareProcessor.leave(node, json, n -> JsonValue.NULL); + assertFalse(json.build().containsKey("content")); + } + + @Test + public void enterIncrementsDepth() { when(this.depth.get()).thenReturn(0); - this.bareProcessor.enter(mock(Node.class), mock(JsonObjectBuilder.class), mock(Function.class)); + this.bareProcessor.enter(mock(Node.class), mock(JsonObjectBuilder.class), n -> JsonValue.NULL); verify(this.depth).get(); verify(this.depth).set(1); } @@ -130,7 +165,7 @@ public void processPropertyForNullProperty() Node node = this.context.resourceResolver().getResource(TEST_FORM_PATH).adaptTo(Node.class); JsonValue jsonValue = this.bareProcessor.processProperty(node, null, mock(JsonValue.class), - mock(Function.class)); + n -> JsonValue.NULL); assertNull(jsonValue); } @@ -142,7 +177,7 @@ public void processPropertyCatchesRepositoryExceptionReturnsInputValue() throws Property property = mock(Property.class); when(property.getName()).thenThrow(new RepositoryException()); JsonString value = Json.createValue(node.getProperty(QUESTIONNAIRE_PROPERTY).getString()); - JsonValue jsonValue = this.bareProcessor.processProperty(node, property, value, mock(Function.class)); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, value, n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(value, jsonValue); } @@ -154,7 +189,7 @@ public void processPropertyForQuestionnairePropertyReturnsInputValue() throws Re Property property = node.getProperty(QUESTIONNAIRE_PROPERTY); JsonString input = Json.createValue(property.getString()); - JsonValue jsonValue = this.bareProcessor.processProperty(node, property, input, mock(Function.class)); + JsonValue jsonValue = this.bareProcessor.processProperty(node, property, input, n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } @@ -166,7 +201,7 @@ public void processPropertyForJcrPropertyReturnsNull() throws RepositoryExceptio Property property = node.getProperty(NODE_TYPE); JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), - mock(Function.class)); + n -> JsonValue.NULL); assertNull(jsonValue); } @@ -177,7 +212,7 @@ public void processPropertyForSlingPropertyReturnsNull() throws RepositoryExcept Property property = node.getProperty(RESOURCE_TYPE); JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), - mock(Function.class)); + n -> JsonValue.NULL); assertNull(jsonValue); } @@ -189,7 +224,7 @@ public void processPropertyForFormPropertyReturnsNull() throws RepositoryExcepti Property property = node.getProperty("form"); JsonValue jsonValue = this.bareProcessor.processProperty(node, property, Json.createValue(property.getString()), - mock(Function.class)); + n -> JsonValue.NULL); assertNull(jsonValue); } @@ -200,7 +235,7 @@ public void processChildForJcrChildReturnsNull() throws RepositoryException Node child = mock(Node.class); when(child.getName()).thenReturn("jcr:name"); - JsonValue jsonValue = this.bareProcessor.processChild(node, child, mock(JsonValue.class), mock(Function.class)); + JsonValue jsonValue = this.bareProcessor.processChild(node, child, mock(JsonValue.class), n -> JsonValue.NULL); assertNull(jsonValue); } @@ -212,7 +247,7 @@ public void processChildCatchesRepositoryExceptionReturnsInputValue() throws Rep when(child.getName()).thenThrow(new RepositoryException()); JsonValue input = mock(JsonValue.class); - JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, mock(Function.class)); + JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } @@ -225,7 +260,7 @@ public void processChildForAnswerChildReturnsInputValue() throws RepositoryExcep Node child = session.getNode(TEST_FORM_PATH + "/a1"); JsonValue input = mock(JsonValue.class); - JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, mock(Function.class)); + JsonValue jsonValue = this.bareProcessor.processChild(node, child, input, n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } @@ -238,12 +273,11 @@ public void leaveSerializesCreatedAndLastModifiedAndFileContent() throws Reposit Calendar date = Calendar.getInstance(); date.set(2023, Calendar.JANUARY, 1); - date.getTimeZone().getRawOffset(); mockCreatedAndLastModifiedDate(node, date); mockFileContent(node, getMockedDataProperty()); JsonObjectBuilder json = Json.createObjectBuilder(); - this.bareProcessor.leave(node, json, mock(Function.class)); + this.bareProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); verify(this.depth, times(3)).get(); @@ -265,14 +299,11 @@ public void leaveWithNonRootNodeDoesNotAddMetadata() throws RepositoryException Node node = mock(Node.class); when(this.depth.get()).thenReturn(2, 1); - Calendar date = Calendar.getInstance(); - date.set(2023, Calendar.JANUARY, 1); - date.getTimeZone().getRawOffset(); - mockCreatedAndLastModifiedDate(node, date); + // No date stubbing: at non-root depth the processor must not even look at the date properties mockFileContent(node, getMockedDataProperty()); JsonObjectBuilder json = Json.createObjectBuilder(); - this.bareProcessor.leave(node, json, mock(Function.class)); + this.bareProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); verify(this.depth, times(3)).get(); @@ -292,7 +323,6 @@ public void leaveCatchesIOException() throws RepositoryException, IOException Calendar date = Calendar.getInstance(); date.set(2023, Calendar.JANUARY, 1); - date.getTimeZone().getRawOffset(); // mocking data property with closed InputStream Property dataProperty = mock(Property.class); @@ -306,7 +336,7 @@ public void leaveCatchesIOException() throws RepositoryException, IOException mockFileContent(node, dataProperty); JsonObjectBuilder json = Json.createObjectBuilder(); - this.bareProcessor.leave(node, json, mock(Function.class)); + this.bareProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); verify(this.depth, times(3)).get(); @@ -329,7 +359,7 @@ public void leaveCatchesRepositoryException() throws RepositoryException when(this.depth.get()).thenReturn(1, 0); JsonObjectBuilder json = Json.createObjectBuilder(); - this.bareProcessor.leave(node, json, mock(Function.class)); + this.bareProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); verify(this.depth, times(3)).get(); @@ -342,8 +372,9 @@ public void leaveCatchesRepositoryException() throws RepositoryException } @Before - public void setUp() throws RepositoryException + public void setUp() throws IllegalAccessException, RepositoryException { + FieldUtils.writeField(this.bareProcessor, "depth", this.depth, true); this.context.build() .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage") .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage") diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java index 537d998e24..a90cbce3c9 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DeepProcessorTest.java @@ -16,23 +16,18 @@ */ package io.uhndata.cards.serialize.internal; -import java.util.function.Function; - import javax.jcr.Node; import javax.jcr.RepositoryException; -import javax.json.Json; -import javax.json.JsonValue; + +import jakarta.json.Json; +import jakarta.json.JsonValue; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -41,37 +36,38 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class DeepProcessorTest { private static final String TEST_FORM_PATH = "/Forms/f1"; private static final String NAME = "deep"; private static final int PRIORITY = 10; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - - @InjectMocks - private DeepProcessor deepProcessor; + private final DeepProcessor deepProcessor = new DeepProcessor(); @Test - public void getNameReturnDeep() + public void getNameReturnsDeep() { assertEquals(NAME, this.deepProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsTen() { assertEquals(PRIORITY, this.deepProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsFalse() { assertFalse(this.deepProcessor.isEnabledByDefault(mock(Resource.class))); } + @Test + public void getDescriptionIsNotEmpty() + { + assertFalse(this.deepProcessor.getDescription().isEmpty()); + } + @Test public void processChildForNullJsonValueInputReturnsSerializedChild() throws RepositoryException { @@ -88,7 +84,7 @@ public void processChildForNotNullJsonValueInputReturnsInputValue() { JsonValue input = mock(JsonValue.class); JsonValue jsonValue = this.deepProcessor.processChild(mock(Node.class), mock(Node.class), input, - mock(Function.class)); + node -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersParserTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersParserTest.java new file mode 100644 index 0000000000..d5632a687c --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersParserTest.java @@ -0,0 +1,92 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.List; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Before; +import org.junit.Test; + +import io.uhndata.cards.serialize.spi.DataFilter; +import io.uhndata.cards.serialize.spi.DataFilterFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DefaultDataFiltersParser}. + * + * @version $Id$ + */ +public class DefaultDataFiltersParserTest +{ + private final DefaultDataFiltersParser parser = new DefaultDataFiltersParser(); + + private final DataFilterFactory factory1 = mock(DataFilterFactory.class); + + private final DataFilterFactory factory2 = mock(DataFilterFactory.class); + + private final DataFilter filter1 = mock(DataFilter.class); + + private final DataFilter filter2 = mock(DataFilter.class); + + @Before + public void setUp() throws IllegalAccessException + { + FieldUtils.writeField(this.parser, "filterFactories", List.of(this.factory1, this.factory2), true); + } + + @Test + public void parseFiltersCollectsFiltersFromAllFactories() + { + when(this.factory1.parseFilters(anyList(), anyList())).thenReturn(List.of(this.filter1)); + when(this.factory2.parseFilters(anyList(), anyList())).thenReturn(List.of(this.filter2)); + + DefaultDataFilters result = this.parser.parseFilters("bare.dataFilter:status=DRAFT"); + assertEquals(List.of(this.filter1, this.filter2), result.getFilters()); + } + + @Test + public void parseFiltersPassesParsedOptionsAndSelectorsToFactories() + { + when(this.factory1.parseFilters(anyList(), anyList())).thenReturn(List.of()); + when(this.factory2.parseFilters(anyList(), anyList())).thenReturn(List.of()); + + this.parser.parseFilters("bare.dataFilter:status=DRAFT.dataFilter:createdAfter=2023-01-15"); + + verify(this.factory1).parseFilters( + List.of(Pair.of("status", "DRAFT"), Pair.of("createdAfter", "2023-01-15")), + List.of("bare", "dataFilter:status=DRAFT", "dataFilter:createdAfter=2023-01-15")); + } + + @Test + public void parseFiltersWithNoFactoryResultsReturnsEmptyFilters() + { + when(this.factory1.parseFilters(any(), any())).thenReturn(List.of()); + when(this.factory2.parseFilters(any(), any())).thenReturn(List.of()); + + DefaultDataFilters result = this.parser.parseFilters("bare"); + assertTrue(result.getFilters().isEmpty()); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersTest.java new file mode 100644 index 0000000000..00321a5773 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultDataFiltersTest.java @@ -0,0 +1,124 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import java.util.List; + +import org.junit.Test; + +import io.uhndata.cards.serialize.spi.DataFilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link DefaultDataFilters}. + * + * @version $Id$ + */ +public class DefaultDataFiltersTest +{ + @Test + public void getFiltersReturnsUnmodifiableList() + { + DataFilter filter = new TestFilter("status", " selector", " condition", false); + DefaultDataFilters filters = new DefaultDataFilters(List.of(filter)); + assertEquals(List.of(filter), filters.getFilters()); + assertThrows(UnsupportedOperationException.class, () -> filters.getFilters().clear()); + } + + @Test + public void getExtraQuerySelectorsSkipsRepeatedFilterNames() + { + DefaultDataFilters filters = new DefaultDataFilters(List.of( + new TestFilter("status", " join1", " c1", false), + new TestFilter("status", " join2", " c2", false), + new TestFilter("modified", " join3", " c3", false))); + assertEquals(" join1 join3", filters.getExtraQuerySelectors()); + } + + @Test + public void getExtraQuerySelectorsKeepsRepeatedPerInstanceFilters() + { + DefaultDataFilters filters = new DefaultDataFilters(List.of( + new TestFilter("status", " join1", " c1", true), + new TestFilter("status", " join2", " c2", true))); + assertEquals(" join1 join2", filters.getExtraQuerySelectors()); + } + + @Test + public void getExtraQueryConditionsConcatenatesAllConditions() + { + DefaultDataFilters filters = new DefaultDataFilters(List.of( + new TestFilter("status", " join1", " c1", false), + new TestFilter("status", " join2", " c2", false))); + assertEquals(" c1 c2", filters.getExtraQueryConditions()); + } + + @Test + public void emptyFiltersProduceEmptyStrings() + { + DefaultDataFilters filters = new DefaultDataFilters(List.of()); + assertTrue(filters.getFilters().isEmpty()); + assertEquals("", filters.getExtraQuerySelectors()); + assertEquals("", filters.getExtraQueryConditions()); + } + + private static final class TestFilter implements DataFilter + { + private final String name; + + private final String selectors; + + private final String conditions; + + private final boolean perInstance; + + TestFilter(final String name, final String selectors, final String conditions, final boolean perInstance) + { + this.name = name; + this.selectors = selectors; + this.conditions = conditions; + this.perInstance = perInstance; + } + + @Override + public String getName() + { + return this.name; + } + + @Override + public boolean areExtraSelectorsPerFilterInstance() + { + return this.perInstance; + } + + @Override + public String getExtraQuerySelectors(final String defaultSelectorName) + { + return this.selectors; + } + + @Override + public String getExtraQueryConditions(final String defaultSelectorName) + { + return this.conditions; + } + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultOptionsProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultOptionsProcessorTest.java new file mode 100644 index 0000000000..cfa4acae60 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DefaultOptionsProcessorTest.java @@ -0,0 +1,109 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import jakarta.json.Json; +import jakarta.json.JsonValue; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DefaultOptionsProcessor}. + * + * @version $Id$ + */ +public class DefaultOptionsProcessorTest +{ + private static final String DEFAULT_OPTIONS = "defaultOptions"; + + private final DefaultOptionsProcessor processor = new DefaultOptionsProcessor(); + + @Test + public void getNameReturnsIncludeDefaultOptions() + { + assertEquals("includeDefaultOptions", this.processor.getName()); + } + + @Test + public void getPriorityReturnsTen() + { + assertEquals(10, this.processor.getPriority()); + } + + @Test + public void getDescriptionIsNotEmpty() + { + assertNotNull(this.processor.getDescription()); + } + + @Test + public void processChildSerializesDefaultOptionsChild() throws RepositoryException + { + Node node = mock(Node.class); + Node child = mock(Node.class); + when(child.getName()).thenReturn(DEFAULT_OPTIONS); + + JsonValue serialized = Json.createValue("serialized"); + JsonValue result = this.processor.processChild(node, child, null, n -> serialized); + assertEquals(serialized, result); + } + + @Test + public void processChildSerializesChildrenOfDefaultOptions() throws RepositoryException + { + Node node = mock(Node.class); + Node child = mock(Node.class); + when(child.getName()).thenReturn("option1"); + when(node.getName()).thenReturn(DEFAULT_OPTIONS); + + JsonValue serialized = Json.createValue("serialized"); + JsonValue result = this.processor.processChild(node, child, null, n -> serialized); + assertEquals(serialized, result); + } + + @Test + public void processChildForOtherChildReturnsInput() throws RepositoryException + { + Node node = mock(Node.class); + Node child = mock(Node.class); + when(child.getName()).thenReturn("a1"); + when(node.getName()).thenReturn("f1"); + + JsonValue input = mock(JsonValue.class); + JsonValue result = this.processor.processChild(node, child, input, n -> JsonValue.NULL); + assertEquals(input, result); + } + + @Test + public void processChildCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Node child = mock(Node.class); + when(child.getName()).thenThrow(new RepositoryException()); + + JsonValue input = mock(JsonValue.class); + JsonValue result = this.processor.processChild(mock(Node.class), child, input, n -> JsonValue.NULL); + assertEquals(input, result); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java index d96a94fec8..ff3bd97b67 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/DereferenceProcessorTest.java @@ -17,7 +17,6 @@ package io.uhndata.cards.serialize.internal; import java.util.List; -import java.util.function.Function; import javax.jcr.Node; import javax.jcr.Property; @@ -25,10 +24,11 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonString; -import javax.json.JsonValue; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; import org.apache.sling.api.resource.Resource; import org.apache.sling.testing.mock.sling.ResourceResolverType; @@ -36,11 +36,9 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -51,7 +49,6 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class DereferenceProcessorTest { private static final String NODE_TYPE = "jcr:primaryType"; @@ -72,27 +69,32 @@ public class DereferenceProcessorTest @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - @InjectMocks - private DereferenceProcessor dereferenceProcessor; + private final DereferenceProcessor dereferenceProcessor = new DereferenceProcessor(); @Test - public void getNameReturnDereference() + public void getNameReturnsDereference() { assertEquals(NAME, this.dereferenceProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsTen() { assertEquals(PRIORITY, this.dereferenceProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsTrue() { assertTrue(this.dereferenceProcessor.isEnabledByDefault(mock(Resource.class))); } + @Test + public void getDescriptionIsNotEmpty() + { + assertFalse(this.dereferenceProcessor.getDescription().isEmpty()); + } + @Test public void processPropertyForMultiValueReferenceProperty() throws RepositoryException { @@ -145,6 +147,28 @@ public void processPropertyForMultiValuePathProperty() throws RepositoryExceptio assertEquals(Json.createValue(valueName), ((JsonArray) jsonValue).get(0)); } + @Test + public void processPropertyForMultiValueAbsolutePathProperty() throws RepositoryException + { + Session session = this.context.resourceResolver().adaptTo(Session.class); + Property property = mock(Property.class); + Value value = mock(Value.class); + + when(property.isMultiple()).thenReturn(true); + when(property.getName()).thenReturn("paths"); + when(property.getType()).thenReturn(PropertyType.PATH); + when(property.getValues()).thenReturn(new Value[] {value}); + when(value.getString()).thenReturn(TEST_FORM_PATH); + when(property.getSession()).thenReturn(session); + + JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, + mock(JsonValue.class), this::serializeNode); + assertNotNull(jsonValue); + assertTrue(jsonValue instanceof JsonArray); + assertEquals(1, ((JsonArray) jsonValue).size()); + assertEquals(Json.createValue("f1"), ((JsonArray) jsonValue).get(0)); + } + @Test public void processPropertyForMultiValueJcrProperty() throws RepositoryException { @@ -202,7 +226,7 @@ public void processPropertyCatchesRepositoryExceptionWhileSerializationMultiValu when(property.getSession()).thenThrow(new RepositoryException()); JsonValue json = Json.createValue("relatedSubjects"); JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, - mock(Function.class)); + n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(json, jsonValue); } @@ -241,7 +265,7 @@ public void processPropertyCatchesRepositoryExceptionWhileSerializationSingleVal when(property.getNode()).thenThrow(new RepositoryException()); JsonValue json = Json.createValue(QUESTIONNAIRE_PROPERTY); JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, - mock(Function.class)); + n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(json, jsonValue); } @@ -266,7 +290,7 @@ public void processPropertyCatchesRepositoryException() throws RepositoryExcepti when(property.isMultiple()).thenThrow(new RepositoryException()); JsonValue json = Json.createValue(FORM_TYPE); JsonValue jsonValue = this.dereferenceProcessor.processProperty(mock(Node.class), property, json, - mock(Function.class)); + n -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(json, jsonValue); } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ExcludeDefaultPropertiesProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ExcludeDefaultPropertiesProcessorTest.java new file mode 100644 index 0000000000..afd2dbdee5 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ExcludeDefaultPropertiesProcessorTest.java @@ -0,0 +1,256 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import jakarta.json.JsonValue; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.sling.api.resource.Resource; +import org.junit.Before; +import org.junit.Test; + +import io.uhndata.cards.forms.api.FormUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ExcludeDefaultPropertiesProcessor}. + * + * @version $Id$ + */ +public class ExcludeDefaultPropertiesProcessorTest +{ + private static final String STATUS = "status"; + + private final ExcludeDefaultPropertiesProcessor processor = new ExcludeDefaultPropertiesProcessor(); + + private final FormUtils formUtils = mock(FormUtils.class); + + private final Node node = mock(Node.class); + + private final NodeType nodeType = mock(NodeType.class); + + @Before + public void setUp() throws IllegalAccessException, RepositoryException + { + FieldUtils.writeField(this.processor, "formUtils", this.formUtils, true); + when(this.node.getPrimaryNodeType()).thenReturn(this.nodeType); + when(this.nodeType.getPropertyDefinitions()).thenReturn(new PropertyDefinition[0]); + } + + @Test + public void getNameReturnsExcludeDefaultProperties() + { + assertEquals("excludeDefaultProperties", this.processor.getName()); + } + + @Test + public void getPriorityReturnsFiftySix() + { + assertEquals(56, this.processor.getPriority()); + } + + @Test + public void getDescriptionIsNotEmpty() + { + assertNotNull(this.processor.getDescription()); + } + + @Test + public void processPropertyExcludesFalseBooleanWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.BOOLEAN); + when(property.getBoolean()).thenReturn(false); + + assertNull(this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsTrueBooleanWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.BOOLEAN); + when(property.getBoolean()).thenReturn(true); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyExcludesEmptyStringWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + when(property.getString()).thenReturn(""); + + assertNull(this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsNonEmptyStringWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + when(property.getString()).thenReturn("DRAFT"); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsNonBooleanNonStringPropertyWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.LONG); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsMultivaluedPropertyWithoutDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + when(property.isMultiple()).thenReturn(true); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyExcludesValueMatchingDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + mockDefaultValues(STATUS, "DRAFT"); + when(this.formUtils.getValue(property)).thenReturn("DRAFT"); + + assertNull(this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsValueDifferentFromDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + mockDefaultValues(STATUS, "DRAFT"); + when(this.formUtils.getValue(property)).thenReturn("SUBMITTED"); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyExcludesValuelessPropertyWithDefault() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + mockDefaultValues(STATUS, "DRAFT"); + when(this.formUtils.getValue(property)).thenReturn(null); + + assertNull(this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processPropertyExcludesMultipleValuesMatchingDefaults() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + when(property.isMultiple()).thenReturn(true); + mockDefaultValues(STATUS, "DRAFT", "SUBMITTED"); + when(this.formUtils.getValue(property)).thenReturn(new Object[] { "DRAFT", "SUBMITTED" }); + + assertNull(this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processPropertyKeepsMultipleValuesDifferentFromDefaults() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + when(property.isMultiple()).thenReturn(true); + mockDefaultValues(STATUS, "DRAFT", "SUBMITTED"); + when(this.formUtils.getValue(property)).thenReturn(new Object[] { "DRAFT" }); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyCachesDefaultsPerNodeType() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + mockDefaultValues(STATUS, "DRAFT"); + when(this.formUtils.getValue(property)).thenReturn("SUBMITTED"); + + this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL); + this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL); + // The property definitions are only read once, the second invocation uses the cached defaults + verify(this.nodeType, times(1)).getPropertyDefinitions(); + } + + @Test + public void endDiscardsTheCachedDefaults() throws RepositoryException + { + Property property = mockProperty(STATUS, PropertyType.STRING); + mockDefaultValues(STATUS, "DRAFT"); + when(this.formUtils.getValue(property)).thenReturn("SUBMITTED"); + + this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL); + this.processor.end(mock(Resource.class)); + this.processor.processProperty(this.node, property, mock(JsonValue.class), n -> JsonValue.NULL); + // The cache was discarded in between, so the property definitions are read again + verify(this.nodeType, times(2)).getPropertyDefinitions(); + } + + @Test + public void processPropertyCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenThrow(new RepositoryException()); + + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(this.node, property, input, n -> JsonValue.NULL)); + } + + private Property mockProperty(final String name, final int type) throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn(name); + when(property.getType()).thenReturn(type); + return property; + } + + private void mockDefaultValues(final String propertyName, final String... defaults) throws RepositoryException + { + PropertyDefinition definition = mock(PropertyDefinition.class); + when(definition.getName()).thenReturn(propertyName); + Value[] values = new Value[defaults.length]; + for (int i = 0; i < defaults.length; i++) { + Value value = mock(Value.class); + when(this.formUtils.getValue(value)).thenReturn(defaults[i]); + values[i] = value; + } + when(definition.getDefaultValues()).thenReturn(values); + when(this.nodeType.getPropertyDefinitions()).thenReturn(new PropertyDefinition[] { definition }); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java index ccc7ca63f5..76754d989c 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/IdentificationProcessorTest.java @@ -16,14 +16,14 @@ */ package io.uhndata.cards.serialize.internal; -import java.util.function.Function; - import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonObjectBuilder; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; import org.apache.sling.api.resource.Resource; import org.apache.sling.testing.mock.sling.ResourceResolverType; @@ -31,9 +31,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,7 +43,6 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class IdentificationProcessorTest { private static final String NODE_TYPE = "jcr:primaryType"; @@ -67,34 +63,39 @@ public class IdentificationProcessorTest @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - @InjectMocks - private IdentificationProcessor identificationProcessor; + private final IdentificationProcessor identificationProcessor = new IdentificationProcessor(); @Test - public void getNameReturnIdentify() + public void getNameReturnsIdentify() { assertEquals(NAME, this.identificationProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsTen() { assertEquals(PRIORITY, this.identificationProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsTrue() { assertTrue(this.identificationProcessor.isEnabledByDefault(mock(Resource.class))); } + @Test + public void getDescriptionIsNotEmpty() + { + assertFalse(this.identificationProcessor.getDescription().isEmpty()); + } + @Test public void leaveCatchesRepositoryException() throws RepositoryException { JsonObjectBuilder json = Json.createObjectBuilder(); Node node = mock(Node.class); when(node.getPath()).thenThrow(new RepositoryException()); - this.identificationProcessor.leave(node, json, mock(Function.class)); + this.identificationProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); assertTrue(jsonObject.isEmpty()); } @@ -105,7 +106,7 @@ public void leaveAddsPathAndNameParameters() throws RepositoryException Session session = this.context.resourceResolver().adaptTo(Session.class); Node node = session.getNode(TEST_FORM_PATH); JsonObjectBuilder json = Json.createObjectBuilder(); - this.identificationProcessor.leave(node, json, mock(Function.class)); + this.identificationProcessor.leave(node, json, n -> JsonValue.NULL); JsonObject jsonObject = json.build(); assertFalse(jsonObject.isEmpty()); assertTrue(jsonObject.containsKey("@path")); diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ImportableProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ImportableProcessorTest.java new file mode 100644 index 0000000000..417d3d4a45 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ImportableProcessorTest.java @@ -0,0 +1,196 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonValue; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ImportableProcessor}. + * + * @version $Id$ + */ +public class ImportableProcessorTest +{ + private static final String TEST_FORM_PATH = "/Forms/f1"; + + private final ImportableProcessor processor = new ImportableProcessor(); + + @Test + public void getNameReturnsImportable() + { + assertEquals("importable", this.processor.getName()); + } + + @Test + public void getPriorityReturnsOneHundred() + { + assertEquals(100, this.processor.getPriority()); + } + + @Test + public void getDescriptionIsNotEmpty() + { + assertNotNull(this.processor.getDescription()); + } + + @Test + public void processPropertyKeepsPrimaryType() throws RepositoryException + { + Property property = mockProperty("jcr:primaryType", PropertyType.NAME); + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(mock(Node.class), property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyRemovesOtherJcrProperties() throws RepositoryException + { + Property property = mockProperty("jcr:uuid", PropertyType.STRING); + assertNull(this.processor.processProperty(mock(Node.class), property, mock(JsonValue.class), + n -> JsonValue.NULL)); + } + + @Test + public void processPropertyRemovesSlingProperties() throws RepositoryException + { + Property property = mockProperty("sling:resourceType", PropertyType.STRING); + assertNull(this.processor.processProperty(mock(Node.class), property, mock(JsonValue.class), + n -> JsonValue.NULL)); + } + + @Test + public void processPropertyConvertsSingleReferenceToPath() throws RepositoryException + { + Property property = mockProperty("questionnaire", PropertyType.REFERENCE); + Node referenced = mock(Node.class); + when(property.getNode()).thenReturn(referenced); + when(referenced.getPath()).thenReturn("/Questionnaires/Test"); + + JsonValue result = this.processor.processProperty(mock(Node.class), property, mock(JsonValue.class), + n -> JsonValue.NULL); + assertEquals(Json.createValue("/Questionnaires/Test"), result); + } + + @Test + public void processPropertyConvertsMultipleWeakReferencesToPaths() throws RepositoryException + { + Property property = mockProperty("relatedSubjects", PropertyType.WEAKREFERENCE); + when(property.isMultiple()).thenReturn(true); + Session session = mock(Session.class); + Value value = mock(Value.class); + Node referenced = mock(Node.class); + when(property.getValues()).thenReturn(new Value[] { value }); + when(property.getSession()).thenReturn(session); + when(value.getString()).thenReturn("uuid-1"); + when(session.getNodeByIdentifier("uuid-1")).thenReturn(referenced); + when(referenced.getPath()).thenReturn("/Subjects/r1"); + + JsonValue result = this.processor.processProperty(mock(Node.class), property, mock(JsonValue.class), + n -> JsonValue.NULL); + assertEquals(1, ((JsonArray) result).size()); + assertEquals("/Subjects/r1", ((JsonArray) result).getString(0)); + } + + @Test + public void processPropertyKeepsOtherProperties() throws RepositoryException + { + Property property = mockProperty("label", PropertyType.STRING); + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(mock(Node.class), property, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenThrow(new RepositoryException()); + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processProperty(mock(Node.class), property, input, n -> JsonValue.NULL)); + } + + @Test + public void processChildRemovesLinks() throws RepositoryException + { + Node child = mock(Node.class); + when(child.isNodeType("cards:Links")).thenReturn(true); + assertNull(this.processor.processChild(mock(Node.class), child, mock(JsonValue.class), n -> JsonValue.NULL)); + } + + @Test + public void processChildKeepsOtherChildren() throws RepositoryException + { + Node child = mock(Node.class); + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processChild(mock(Node.class), child, input, n -> JsonValue.NULL)); + } + + @Test + public void processChildCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Node child = mock(Node.class); + when(child.isNodeType("cards:Links")).thenThrow(new RepositoryException()); + JsonValue input = mock(JsonValue.class); + assertEquals(input, this.processor.processChild(mock(Node.class), child, input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyNamePrefixesReferences() throws RepositoryException + { + Property property = mockProperty("questionnaire", PropertyType.REFERENCE); + assertEquals("jcr:reference:questionnaire", + this.processor.processPropertyName(mock(Node.class), property, "questionnaire")); + } + + @Test + public void processPropertyNameKeepsOtherNames() throws RepositoryException + { + Property property = mockProperty("label", PropertyType.STRING); + assertEquals("label", this.processor.processPropertyName(mock(Node.class), property, "label")); + } + + @Test + public void processPropertyNameCatchesRepositoryExceptionReturnsInput() throws RepositoryException + { + Property property = mock(Property.class); + when(property.getType()).thenThrow(new RepositoryException()); + assertEquals("label", this.processor.processPropertyName(mock(Node.class), property, "label")); + } + + private Property mockProperty(final String name, final int type) throws RepositoryException + { + Property property = mock(Property.class); + when(property.getName()).thenReturn(name); + when(property.getType()).thenReturn(type); + return property; + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java index f0674f8a76..9f1f207ea1 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/PropertiesProcessorTest.java @@ -16,28 +16,23 @@ */ package io.uhndata.cards.serialize.internal; -import java.util.function.Function; - import javax.jcr.Node; import javax.jcr.Property; -import javax.json.JsonValue; +import jakarta.json.JsonValue; + +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.junit.Rule; +import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; import io.uhndata.cards.forms.api.FormUtils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -46,39 +41,45 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class PropertiesProcessorTest { private static final String NAME = "properties"; private static final int PRIORITY = 0; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + private final PropertiesProcessor propertiesProcessor = new PropertiesProcessor(); - @InjectMocks - private PropertiesProcessor propertiesProcessor; + private final FormUtils formUtils = mock(FormUtils.class); - @Mock - private FormUtils formUtils; + @Before + public void setUp() throws IllegalAccessException + { + FieldUtils.writeField(this.propertiesProcessor, "formUtils", this.formUtils, true); + } @Test - public void getNameReturnProperties() + public void getNameReturnsProperties() { assertEquals(NAME, this.propertiesProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsZero() { assertEquals(PRIORITY, this.propertiesProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsTrue() { assertTrue(this.propertiesProcessor.isEnabledByDefault(mock(Resource.class))); } + @Test + public void getDescriptionIsNotEmpty() + { + assertFalse(this.propertiesProcessor.getDescription().isEmpty()); + } + @Test public void processPropertyForNullJsonValueInputReturnsSerializedProperty() { @@ -86,7 +87,7 @@ public void processPropertyForNullJsonValueInputReturnsSerializedProperty() when(this.formUtils.serializeProperty(any())).thenReturn(serializedProperty); JsonValue jsonValue = this.propertiesProcessor.processProperty(mock(Node.class), mock(Property.class), null, - mock(Function.class)); + node -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(serializedProperty, jsonValue); } @@ -96,7 +97,7 @@ public void processPropertyForNotNullJsonValueInputReturnsInputValue() { JsonValue input = mock(JsonValue.class); JsonValue jsonValue = this.propertiesProcessor.processProperty(mock(Node.class), mock(Property.class), input, - mock(Function.class)); + node -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ReferencedProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ReferencedProcessorTest.java new file mode 100644 index 0000000000..f7524117bc --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/ReferencedProcessorTest.java @@ -0,0 +1,102 @@ +/* + * 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 io.uhndata.cards.serialize.internal; + +import javax.jcr.Node; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ReferencedProcessor}. + * + * @version $Id$ + */ +public class ReferencedProcessorTest +{ + private static final String REFERENCED = "@referenced"; + + private final ReferencedProcessor processor = new ReferencedProcessor(); + + @Test + public void getNameReturnsReferenced() + { + assertEquals("referenced", this.processor.getName()); + } + + @Test + public void getPriorityReturnsTen() + { + assertEquals(10, this.processor.getPriority()); + } + + @Test + public void getDescriptionIsNotEmpty() + { + assertNotNull(this.processor.getDescription()); + } + + @Test + public void leaveMarksReferencedNode() throws RepositoryException + { + JsonObject result = leaveWithReferences(true); + assertTrue(result.getBoolean(REFERENCED)); + } + + @Test + public void leaveMarksUnreferencedNode() throws RepositoryException + { + JsonObject result = leaveWithReferences(false); + assertFalse(result.getBoolean(REFERENCED)); + } + + @Test + public void leaveCatchesRepositoryException() throws RepositoryException + { + Node node = mock(Node.class); + when(node.getReferences()).thenThrow(new RepositoryException()); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.processor.leave(node, json, n -> JsonValue.NULL); + assertFalse(json.build().containsKey(REFERENCED)); + } + + private JsonObject leaveWithReferences(final boolean hasReferences) throws RepositoryException + { + Node node = mock(Node.class); + PropertyIterator references = mock(PropertyIterator.class); + when(node.getReferences()).thenReturn(references); + when(references.hasNext()).thenReturn(hasReferences); + + JsonObjectBuilder json = Json.createObjectBuilder(); + this.processor.leave(node, json, n -> JsonValue.NULL); + return json.build(); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java index ef4f09cc3a..dd4408a19f 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/internal/SimpleProcessorTest.java @@ -16,23 +16,19 @@ */ package io.uhndata.cards.serialize.internal; -import java.util.function.Function; - import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.RepositoryException; -import javax.json.JsonValue; + +import jakarta.json.JsonValue; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -41,7 +37,6 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class SimpleProcessorTest { private static final String BASE_VERSION = "jcr:baseVersion"; @@ -50,35 +45,37 @@ public class SimpleProcessorTest private static final String NAME = "simple"; private static final int PRIORITY = 25; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); - - @InjectMocks - private SimpleProcessor simpleProcessor; + private final SimpleProcessor simpleProcessor = new SimpleProcessor(); @Test - public void getNameReturnSimple() + public void getNameReturnsSimple() { assertEquals(NAME, this.simpleProcessor.getName()); } @Test - public void getPriorityTest() + public void getPriorityReturnsTwentyFive() { assertEquals(PRIORITY, this.simpleProcessor.getPriority()); } @Test - public void isEnabledByDefaultTest() + public void isEnabledByDefaultReturnsFalse() { assertFalse(this.simpleProcessor.isEnabledByDefault(mock(Resource.class))); } + @Test + public void getDescriptionIsNotEmpty() + { + assertFalse(this.simpleProcessor.getDescription().isEmpty()); + } + @Test public void processPropertyForNullPropertyReturnsNull() { JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), null, mock(JsonValue.class), - mock(Function.class)); + node -> JsonValue.NULL); assertNull(jsonValue); } @@ -89,19 +86,19 @@ public void processPropertyForNotNullPropertyCatchesRepositoryExceptionReturnsIn when(property.getName()).thenThrow(new RepositoryException()); JsonValue input = mock(JsonValue.class); JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, input, - mock(Function.class)); + node -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } @Test - public void processPropertyReturnsInput() throws RepositoryException + public void processPropertyForKeptJcrPropertyReturnsInput() throws RepositoryException { Property property = mock(Property.class); when(property.getName()).thenReturn(CREATED); JsonValue input = mock(JsonValue.class); JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, input, - mock(Function.class)); + node -> JsonValue.NULL); assertNotNull(jsonValue); assertEquals(input, jsonValue); } @@ -112,7 +109,7 @@ public void processPropertyForJcrPropertyReturnsNull() throws RepositoryExceptio Property property = mock(Property.class); when(property.getName()).thenReturn(BASE_VERSION); JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), - mock(Function.class)); + node -> JsonValue.NULL); assertNull(jsonValue); } @@ -122,7 +119,7 @@ public void processPropertyForSlingPropertyReturnsNull() throws RepositoryExcept Property property = mock(Property.class); when(property.getName()).thenReturn(RESOURCE_TYPE); JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), - mock(Function.class)); + node -> JsonValue.NULL); assertNull(jsonValue); } @@ -132,8 +129,7 @@ public void processPropertyForFormPropertyReturnsNull() throws RepositoryExcepti Property property = mock(Property.class); when(property.getName()).thenReturn("form"); JsonValue jsonValue = this.simpleProcessor.processProperty(mock(Node.class), property, mock(JsonValue.class), - mock(Function.class)); + node -> JsonValue.NULL); assertNull(jsonValue); } - } diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/BaseFilterFactoryTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/BaseFilterFactoryTest.java new file mode 100644 index 0000000000..f052158e2a --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/BaseFilterFactoryTest.java @@ -0,0 +1,178 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import java.util.List; +import java.util.TreeSet; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for the parsing helpers in {@link BaseFilterFactory}. + * + * @version $Id$ + */ +public class BaseFilterFactoryTest +{ + private static final String CREATED_AFTER = "createdAfter"; + + private static final String STATUS = "status"; + + private static final String NO_STATUS = "statusNot"; + + private static final List> FILTERS = List.of( + Pair.of(CREATED_AFTER, "2023-01-15"), + Pair.of(STATUS, "DRAFT"), + Pair.of(STATUS, "with%20escapes"), + Pair.of(NO_STATUS, "SUBMITTED")); + + private final TestFilterFactory factory = new TestFilterFactory(); + + @Test + public void parseSingletonFilterReturnsFirstMatchingFilter() + { + List result = this.factory.parseSingletonFilter(FILTERS, List.of(), CREATED_AFTER, + TestFilter::new); + assertEquals(1, result.size()); + assertEquals("2023-01-15", result.get(0).getName()); + } + + @Test + public void parseSingletonFilterDecodesUrlEscapes() + { + List result = this.factory.parseSingletonFilter( + List.of(Pair.of(STATUS, "with%20escapes")), List.of(), STATUS, TestFilter::new); + assertEquals("with escapes", result.get(0).getName()); + } + + @Test + public void parseSingletonFilterWithBlankNameReturnsEmptyList() + { + assertTrue(this.factory.parseSingletonFilter(FILTERS, List.of(), " ", TestFilter::new).isEmpty()); + } + + @Test + public void parseSingletonFilterWithNoMatchReturnsEmptyList() + { + assertTrue(this.factory.parseSingletonFilter(FILTERS, List.of(), "missing", TestFilter::new).isEmpty()); + } + + @Test + public void parseMultipleSingletonFiltersReturnsAllMatchingFilters() + { + List result = this.factory.parseMultipleSingletonFilters(FILTERS, List.of(), STATUS, + TestFilter::new); + assertEquals(2, result.size()); + assertEquals("DRAFT", result.get(0).getName()); + assertEquals("with escapes", result.get(1).getName()); + } + + @Test + public void parseMultipleSingletonFiltersWithBlankNameReturnsEmptyList() + { + assertTrue(this.factory.parseMultipleSingletonFilters(FILTERS, List.of(), "", TestFilter::new).isEmpty()); + } + + @Test + public void parseSetFilterCollectsAllValuesIntoOneFilter() + { + List result = this.factory.parseSetFilter(FILTERS, List.of(), STATUS, + values -> new TestFilter(String.join(",", new TreeSet<>(values)))); + assertEquals(1, result.size()); + assertEquals("DRAFT,with escapes", result.get(0).getName()); + } + + @Test + public void parseSetFilterWithBlankNameReturnsEmptyList() + { + assertTrue(this.factory.parseSetFilter(FILTERS, List.of(), null, v -> new TestFilter("")).isEmpty()); + } + + @Test + public void parseSetFilterWithNoMatchReturnsEmptyList() + { + assertTrue(this.factory.parseSetFilter(FILTERS, List.of(), "missing", v -> new TestFilter("")).isEmpty()); + } + + @Test + public void parseDoubleSetFiltersReturnsPositiveAndNegativeFilters() + { + List result = this.factory.parseDoubleSetFilters(FILTERS, List.of(), STATUS, NO_STATUS, + (values, positive) -> new TestFilter((positive ? "+" : "-") + String.join(",", new TreeSet<>(values)))); + assertEquals(2, result.size()); + assertEquals("+DRAFT,with escapes", result.get(0).getName()); + assertEquals("-SUBMITTED", result.get(1).getName()); + } + + @Test + public void parseDoubleSetFiltersWithOnlyNegativeMatchesReturnsOneFilter() + { + List result = this.factory.parseDoubleSetFilters(FILTERS, List.of(), "missing", NO_STATUS, + (values, positive) -> new TestFilter((positive ? "+" : "-") + String.join(",", values))); + assertEquals(1, result.size()); + assertEquals("-SUBMITTED", result.get(0).getName()); + } + + @Test + public void parseDoubleSetFiltersWithBlankNamesReturnsEmptyList() + { + assertTrue(this.factory.parseDoubleSetFilters(FILTERS, List.of(), "", null, + (values, positive) -> new TestFilter("")).isEmpty()); + } + + private static final class TestFilterFactory extends BaseFilterFactory + { + @Override + public List parseFilters(final List> filters, final List selectors) + { + return List.of(); + } + + @Override + public List getFilterDetails() + { + return List.of(); + } + } + + /** A simple filter storing the parsed value in its name. */ + private static final class TestFilter implements DataFilter + { + private final String value; + + TestFilter(final String value) + { + this.value = value; + } + + @Override + public String getName() + { + return this.value; + } + + @Override + public String getExtraQueryConditions(final String defaultSelectorName) + { + return ""; + } + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/DataFilterTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/DataFilterTest.java new file mode 100644 index 0000000000..f182f447da --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/DataFilterTest.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 io.uhndata.cards.serialize.spi; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * Unit tests for the default methods of {@link DataFilter}. + * + * @version $Id$ + */ +public class DataFilterTest +{ + /** A filter relying on all the default methods of the interface. */ + private final DataFilter filter = new DataFilter() + { + @Override + public String getName() + { + return "status"; + } + + @Override + public String getExtraQueryConditions(final String defaultSelectorName) + { + return " and " + defaultSelectorName + ".status = 'DRAFT'"; + } + }; + + @Test + public void extraSelectorsAreSharedPerFilterNameByDefault() + { + assertFalse(this.filter.areExtraSelectorsPerFilterInstance()); + } + + @Test + public void getExtraQuerySelectorsReturnsEmptyStringByDefault() + { + assertEquals("", this.filter.getExtraQuerySelectors("f")); + } + + @Test + public void getExtraQuerySelectorsUsesTheDefaultSelectorName() + { + assertEquals("", this.filter.getExtraQuerySelectors()); + } + + @Test + public void getExtraQueryConditionsUsesTheDefaultSelectorName() + { + assertEquals(" and form.status = 'DRAFT'", this.filter.getExtraQueryConditions()); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceCSVProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceCSVProcessorTest.java new file mode 100644 index 0000000000..ea83954552 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceCSVProcessorTest.java @@ -0,0 +1,53 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import org.apache.sling.api.resource.Resource; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the default methods of {@link ResourceCSVProcessor}. + * + * @version $Id$ + */ +public class ResourceCSVProcessorTest +{ + /** A processor relying on the default methods of the interface. */ + private final ResourceCSVProcessor processor = new ResourceCSVProcessor() + { + @Override + public String serialize(final Resource resource) + { + return ""; + } + + @Override + public java.util.List getDetails() + { + return java.util.List.of(); + } + }; + + @Test + public void canProcessReturnsFalseByDefault() + { + assertFalse(this.processor.canProcess(mock(Resource.class))); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceJsonProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceJsonProcessorTest.java new file mode 100644 index 0000000000..88e0815868 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceJsonProcessorTest.java @@ -0,0 +1,122 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import javax.jcr.Node; +import javax.jcr.Property; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; + +import org.apache.sling.api.resource.Resource; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the default methods of {@link ResourceJsonProcessor}. + * + * @version $Id$ + */ +public class ResourceJsonProcessorTest +{ + private static final String NAME = "minimal"; + + private static final String DESCRIPTION = "A minimal processor"; + + /** A processor relying on all the default methods of the interface. */ + private final ResourceJsonProcessor processor = new ResourceJsonProcessor() + { + @Override + public String getName() + { + return NAME; + } + + @Override + public int getPriority() + { + return 0; + } + + @Override + public String getDescription() + { + return DESCRIPTION; + } + }; + + @Test + public void getDetailsCollectsNameDescriptionAndEnabledFlag() + { + SelectorDetails details = this.processor.getDetails(); + assertEquals(NAME, details.getName()); + assertEquals(DESCRIPTION, details.getDescription()); + assertFalse(details.isEnabledByDefault()); + } + + @Test + public void canProcessReturnsTrueByDefault() + { + assertTrue(this.processor.canProcess(mock(Resource.class))); + } + + @Test + public void isEnabledByDefaultReturnsFalseByDefault() + { + assertFalse(this.processor.isEnabledByDefault(mock(Resource.class))); + } + + @Test + public void lifecycleMethodsDoNothingByDefault() + { + JsonObjectBuilder json = Json.createObjectBuilder(); + this.processor.start(mock(Resource.class)); + this.processor.enter(mock(Node.class), json, n -> JsonValue.NULL); + this.processor.leave(mock(Node.class), json, n -> JsonValue.NULL); + this.processor.end(mock(Resource.class)); + JsonObject result = json.build(); + assertTrue(result.isEmpty()); + } + + @Test + public void processPropertyReturnsInputByDefault() + { + JsonValue input = mock(JsonValue.class); + assertEquals(input, + this.processor.processProperty(mock(Node.class), mock(Property.class), input, n -> JsonValue.NULL)); + } + + @Test + public void processPropertyNameReturnsInputByDefault() + { + assertEquals("label", this.processor.processPropertyName(mock(Node.class), mock(Property.class), "label")); + } + + @Test + public void processChildReturnsInputByDefault() + { + JsonValue input = mock(JsonValue.class); + assertEquals(input, + this.processor.processChild(mock(Node.class), mock(Node.class), input, n -> JsonValue.NULL)); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceMarkdownProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceMarkdownProcessorTest.java new file mode 100644 index 0000000000..96b69d0651 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceMarkdownProcessorTest.java @@ -0,0 +1,47 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import org.apache.sling.api.resource.Resource; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the default methods of {@link ResourceMarkdownProcessor}. + * + * @version $Id$ + */ +public class ResourceMarkdownProcessorTest +{ + /** A processor relying on the default methods of the interface. */ + private final ResourceMarkdownProcessor processor = new ResourceMarkdownProcessor() + { + @Override + public String serialize(final Resource resource) + { + return ""; + } + }; + + @Test + public void canProcessReturnsFalseByDefault() + { + assertFalse(this.processor.canProcess(mock(Resource.class))); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceTextProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceTextProcessorTest.java new file mode 100644 index 0000000000..6dd6b15065 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/ResourceTextProcessorTest.java @@ -0,0 +1,47 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import org.apache.sling.api.resource.Resource; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the default methods of {@link ResourceTextProcessor}. + * + * @version $Id$ + */ +public class ResourceTextProcessorTest +{ + /** A processor relying on the default methods of the interface. */ + private final ResourceTextProcessor processor = new ResourceTextProcessor() + { + @Override + public String serialize(final Resource resource) + { + return ""; + } + }; + + @Test + public void canProcessReturnsFalseByDefault() + { + assertFalse(this.processor.canProcess(mock(Resource.class))); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/SelectorDetailsTest.java b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/SelectorDetailsTest.java new file mode 100644 index 0000000000..0d19a1275a --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/serialize/spi/SelectorDetailsTest.java @@ -0,0 +1,115 @@ +/* + * 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 io.uhndata.cards.serialize.spi; + +import org.junit.Test; + +import io.uhndata.cards.serialize.spi.SelectorDetails.SelectorOption; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link SelectorDetails}. + * + * @version $Id$ + */ +public class SelectorDetailsTest +{ + private static final String NAME = "bare"; + + private static final String DESCRIPTION = "A selector"; + + @Test + public void nameAndDescriptionConstructorDisablesByDefault() + { + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION); + assertEquals(NAME, details.getName()); + assertEquals(DESCRIPTION, details.getDescription()); + assertFalse(details.isEnabledByDefault()); + assertEquals(0, details.getOptions().length); + } + + @Test + public void enabledByDefaultConstructorStoresFlag() + { + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, true); + assertTrue(details.isEnabledByDefault()); + assertEquals(0, details.getOptions().length); + } + + @Test + public void optionsArrayConstructorStoresOptions() + { + SelectorDetails source = new SelectorDetails(NAME, DESCRIPTION, "exclude", "What to exclude"); + SelectorOption[] options = source.getOptions(); + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, options); + assertFalse(details.isEnabledByDefault()); + assertEquals(1, details.getOptions().length); + assertEquals("exclude", details.getOptions()[0].getName()); + assertEquals("What to exclude", details.getOptions()[0].getDescription()); + } + + @Test + public void enabledByDefaultAndOptionsArrayConstructorStoresBoth() + { + SelectorOption[] options = new SelectorDetails(NAME, DESCRIPTION, "a", "b").getOptions(); + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, true, options); + assertTrue(details.isEnabledByDefault()); + assertEquals(1, details.getOptions().length); + } + + @Test + public void varargsConstructorPairsNamesAndDescriptions() + { + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, + "option1", "First option", "option2", "Second option"); + assertFalse(details.isEnabledByDefault()); + assertEquals(2, details.getOptions().length); + assertEquals("option1", details.getOptions()[0].getName()); + assertEquals("First option", details.getOptions()[0].getDescription()); + assertEquals("option2", details.getOptions()[1].getName()); + assertEquals("Second option", details.getOptions()[1].getDescription()); + } + + @Test + public void varargsConstructorIgnoresDanglingOption() + { + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, true, + "option1", "First option", "dangling"); + assertTrue(details.isEnabledByDefault()); + assertEquals(1, details.getOptions().length); + assertEquals("option1", details.getOptions()[0].getName()); + } + + @Test + public void varargsConstructorWithoutOptionsLeavesOptionsEmpty() + { + SelectorDetails details = new SelectorDetails(NAME, DESCRIPTION, true, new String[0]); + assertTrue(details.isEnabledByDefault()); + assertEquals(0, details.getOptions().length); + } + + @Test + public void singleArgumentOptionHasEmptyDescription() + { + SelectorOption option = new SelectorDetails(NAME, DESCRIPTION).new SelectorOption("exclude"); + assertEquals("exclude", option.getName()); + assertEquals("", option.getDescription()); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/utils/DateUtilsTest.java b/modules/utils/src/test/java/io/uhndata/cards/utils/DateUtilsTest.java new file mode 100644 index 0000000000..2be271232b --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/utils/DateUtilsTest.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 io.uhndata.cards.utils; + +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link DateUtils}. + * + * @version $Id$ + */ +public class DateUtilsTest +{ + @Test + public void parseCalendarWithNullReturnsNull() + { + assertNull(DateUtils.parseCalendar(null)); + } + + @Test + public void parseCalendarWithBlankReturnsNull() + { + assertNull(DateUtils.parseCalendar(" ")); + } + + @Test + public void parseCalendarWithUnsupportedFormatReturnsNull() + { + assertNull(DateUtils.parseCalendar("not a date")); + } + + @Test + public void parseCalendarWithPreferredFormat() + { + Calendar result = DateUtils.parseCalendar("1999-12-31T20:30:00.000+05:00"); + assertEquals(1999, result.get(Calendar.YEAR)); + assertEquals(Calendar.DECEMBER, result.get(Calendar.MONTH)); + assertEquals(31, result.get(Calendar.DAY_OF_MONTH)); + } + + @Test + public void parseCalendarWithDateOnlyFormat() + { + Calendar result = DateUtils.parseCalendar("2023-01-15"); + assertEquals(2023, result.get(Calendar.YEAR)); + assertEquals(Calendar.JANUARY, result.get(Calendar.MONTH)); + assertEquals(15, result.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, result.get(Calendar.HOUR_OF_DAY)); + } + + @Test + public void parseCalendarWithSlashesFormat() + { + Calendar result = DateUtils.parseCalendar("1/15/2023"); + assertEquals(2023, result.get(Calendar.YEAR)); + assertEquals(Calendar.JANUARY, result.get(Calendar.MONTH)); + assertEquals(15, result.get(Calendar.DAY_OF_MONTH)); + } + + @Test + public void parseDateTimeWithNullReturnsNull() + { + assertNull(DateUtils.parseDateTime(null)); + } + + @Test + public void parseDateTimeWithBlankReturnsNull() + { + assertNull(DateUtils.parseDateTime(" ")); + } + + @Test + public void parseDateTimeWithUnsupportedFormatReturnsNull() + { + assertNull(DateUtils.parseDateTime("not a date")); + } + + @Test + public void parseDateTimeWithTimezoneKeepsTimezone() + { + ZonedDateTime result = DateUtils.parseDateTime("1999-12-31T20:30:00.000+05:00"); + assertEquals(1999, result.getYear()); + assertEquals(12, result.getMonthValue()); + assertEquals(31, result.getDayOfMonth()); + assertEquals(20, result.getHour()); + assertEquals("+05:00", result.getOffset().getId()); + } + + @Test + public void parseDateTimeWithoutTimezoneUsesSystemDefault() + { + ZonedDateTime result = DateUtils.parseDateTime("2023-01-15T10:20:30"); + assertEquals(2023, result.getYear()); + assertEquals(10, result.getHour()); + assertEquals(ZoneId.systemDefault(), result.getZone()); + } + + @Test + public void parseDateTimeWithDateOnlyDefaultsToMidnight() + { + ZonedDateTime result = DateUtils.parseDateTime("2023-01-15"); + assertEquals(0, result.getHour()); + assertEquals(0, result.getMinute()); + } + + @Test + public void atMidnightForZonedDateTimeResetsTime() + { + ZonedDateTime date = ZonedDateTime.of(2023, 1, 15, 10, 20, 30, 400, ZoneId.systemDefault()); + ZonedDateTime result = DateUtils.atMidnight(date); + assertEquals(LocalDate.of(2023, 1, 15), result.toLocalDate()); + assertEquals(0, result.getHour()); + assertEquals(0, result.getMinute()); + assertEquals(0, result.getSecond()); + assertEquals(0, result.getNano()); + } + + @Test + public void atMidnightForCalendarResetsTime() + { + Calendar date = Calendar.getInstance(); + date.set(2023, Calendar.JANUARY, 15, 10, 20, 30); + Calendar result = DateUtils.atMidnight(date); + assertEquals(2023, result.get(Calendar.YEAR)); + assertEquals(15, result.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, result.get(Calendar.HOUR_OF_DAY)); + assertEquals(0, result.get(Calendar.MINUTE)); + assertEquals(0, result.get(Calendar.SECOND)); + assertEquals(0, result.get(Calendar.MILLISECOND)); + // The input calendar is not modified + assertEquals(10, date.get(Calendar.HOUR_OF_DAY)); + } + + @Test + public void toStringForNullCalendarReturnsNull() + { + assertNull(DateUtils.toString((Calendar) null)); + } + + @Test + public void toStringForCalendarUsesPreferredFormat() + { + Calendar date = DateUtils.parseCalendar("1999-12-31T20:30:00.000+05:00"); + // The parsed calendar is in the system timezone, so re-serializing yields an equivalent, not identical, string + assertEquals(date.getTimeInMillis(), DateUtils.parseCalendar(DateUtils.toString(date)).getTimeInMillis()); + } + + @Test + public void toStringForUnformattableCalendarReturnsNull() + { + Calendar broken = Calendar.getInstance(); + broken.setLenient(false); + broken.set(Calendar.MONTH, 42); + assertNull(DateUtils.toString(broken)); + } + + @Test + public void toStringForNullTemporalAccessorReturnsNull() + { + assertNull(DateUtils.toString((java.time.temporal.TemporalAccessor) null)); + } + + @Test + public void toStringForZonedDateTimeUsesPreferredFormat() + { + ZonedDateTime date = ZonedDateTime.of(1999, 12, 31, 20, 30, 0, 0, ZoneId.of("+05:00")); + assertEquals("1999-12-31T20:30:00.000+05:00", DateUtils.toString(date)); + } + + @Test + public void toStringForUnsupportedTemporalAccessorReturnsNull() + { + // A MonthDay doesn't hold enough fields for the preferred datetime format + assertNull(DateUtils.toString(MonthDay.of(12, 31))); + } + + @Test + public void normalizeCompletesPartialDate() + { + String result = DateUtils.normalize("2023-01-15"); + ZonedDateTime expected = LocalDate.of(2023, 1, 15).atStartOfDay(ZoneId.systemDefault()); + assertEquals(DateUtils.toString(expected), result); + } + + @Test + public void normalizeWithUnsupportedFormatReturnsNull() + { + assertNull(DateUtils.normalize("not a date")); + } +} diff --git a/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java index 9a6c820839..4941fba50c 100644 --- a/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java +++ b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/DenyScriptsSlingPostProcessorTest.java @@ -18,23 +18,16 @@ import java.util.List; -import org.apache.sling.api.request.builder.impl.SlingHttpServletRequestImpl; +import org.apache.sling.api.SlingJakartaHttpServletRequest; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceMetadata; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.servlets.post.Modification; import org.apache.sling.servlets.post.ModificationType; -import org.apache.sling.testing.mock.sling.ResourceResolverType; -import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; -import static org.mockito.Matchers.eq; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,155 +36,116 @@ * * @version $Id$ */ -@RunWith(MockitoJUnitRunner.class) public class DenyScriptsSlingPostProcessorTest { + private static final String SOURCE_PATH = "/Forms/f1"; - @Rule - public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK); + private static final String DESTINATION_PATH = "/Forms/f2"; - @InjectMocks - private DenyScriptsSlingPostProcessor denyScriptsSlingPostProcessor; + private final DenyScriptsSlingPostProcessor processor = new DenyScriptsSlingPostProcessor(); + + private final SlingJakartaHttpServletRequest request = mock(SlingJakartaHttpServletRequest.class); + + private final ResourceResolver resourceResolver = mock(ResourceResolver.class); + + private final List changes = + List.of(new Modification(ModificationType.COPY, SOURCE_PATH, DESTINATION_PATH)); @Test - public void processAllowsResourceWithNullResourceMetadata() + public void processAllowsResourceWithNullResourceMetadata() throws Exception { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); Resource resource = mock(Resource.class); - when(request.getResourceResolver()).thenReturn(resourceResolver); - when(resourceResolver.getResource("/Forms/f1")).thenReturn(resource); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + when(this.resourceResolver.getResource(SOURCE_PATH)).thenReturn(resource); when(resource.getResourceMetadata()).thenReturn(null); - Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) - .doesNotThrowAnyException(); + this.processor.process(this.request, this.changes); } @Test - public void processAllowsResourceWithNullContentType() + public void processAllowsResourceWithNullContentType() throws Exception { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, null); - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", null); - - Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) - .doesNotThrowAnyException(); + this.processor.process(this.request, this.changes); } @Test public void processScriptResourceThrowsException() { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); - - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "text/script;charset=UTF-8"); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, "text/script;charset=UTF-8"); - Assert.assertThrows("Script files are not allowed", Exception.class, - () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + Exception e = assertThrows(Exception.class, () -> this.processor.process(this.request, this.changes)); + assertEquals("Script files are not allowed", e.getMessage()); } @Test public void processHtmlResourceThrowsException() { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); - - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "text/html;charset=UTF-8"); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, "text/html;charset=UTF-8"); - Assert.assertThrows("HTML files are not allowed", Exception.class, - () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + Exception e = assertThrows(Exception.class, () -> this.processor.process(this.request, this.changes)); + assertEquals("HTML files are not allowed", e.getMessage()); } @Test public void processScriptResourceIgnoresCase() { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, "application/TypeScript"); - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "application/TypeScript"); - - Assert.assertThrows("Script files are not allowed", Exception.class, - () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + Exception e = assertThrows(Exception.class, () -> this.processor.process(this.request, this.changes)); + assertEquals("Script files are not allowed", e.getMessage()); } @Test public void processHtmlResourceIgnoresCase() { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); - - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "application/XHTML"); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, "application/XHTML"); - Assert.assertThrows("HTML files are not allowed", Exception.class, - () -> this.denyScriptsSlingPostProcessor.process(request, changes)); + Exception e = assertThrows(Exception.class, () -> this.processor.process(this.request, this.changes)); + assertEquals("HTML files are not allowed", e.getMessage()); } @Test - public void processAllowsOtherContentTypeResource() + public void processAllowsOtherContentTypeResource() throws Exception { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); - - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "text/plain;charset=UTF-8"); - mockRecourseContentType(resourceResolver, "/Forms/f2", "text/plain;charset=UTF-8"); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + mockResourceContentType(SOURCE_PATH, "text/plain;charset=UTF-8"); - Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) - .doesNotThrowAnyException(); + this.processor.process(this.request, this.changes); } @Test - public void processAllowsAllForAdminUser() + public void processAllowsAllForAdminUser() throws Exception { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); + // Even a forbidden content type is accepted when the admin is uploading it + mockResourceContentType(SOURCE_PATH, "application/TypeScript"); - when(request.getResourceResolver()).thenReturn(resourceResolver); - mockRecourseContentType(resourceResolver, "/Forms/f1", "application/XHTML"); - mockRecourseContentType(resourceResolver, "/Forms/f2", "application/TypeScript"); - - when(request.getRemoteUser()).thenReturn("admin"); - Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) - .doesNotThrowAnyException(); + when(this.request.getRemoteUser()).thenReturn("admin"); + this.processor.process(this.request, this.changes); } @Test - public void processAllowsNullResource() + public void processAllowsNullResource() throws Exception { - SlingHttpServletRequestImpl request = mock(SlingHttpServletRequestImpl.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - List changes = List.of(new Modification(ModificationType.COPY, "/Forms/f1", "/Forms/f2")); - - when(request.getResourceResolver()).thenReturn(resourceResolver); - when(resourceResolver.getResource("/Forms/f1")).thenReturn(null); - when(resourceResolver.getResource("/Forms/f2")).thenReturn(null); + when(this.request.getResourceResolver()).thenReturn(this.resourceResolver); + when(this.resourceResolver.getResource(SOURCE_PATH)).thenReturn(null); - Assertions.assertThatCode(() -> this.denyScriptsSlingPostProcessor.process(request, changes)) - .doesNotThrowAnyException(); + this.processor.process(this.request, this.changes); } - private void mockRecourseContentType(ResourceResolver resourceResolver, String resourcePath, String contentType) + private void mockResourceContentType(String resourcePath, String contentType) { Resource resource = mock(Resource.class); ResourceMetadata metadata = mock(ResourceMetadata.class); - when(resourceResolver.getResource(eq(resourcePath))).thenReturn(resource); + when(this.resourceResolver.getResource(resourcePath)).thenReturn(resource); when(resource.getResourceMetadata()).thenReturn(metadata); when(metadata.getContentType()).thenReturn(contentType); } - } diff --git a/modules/utils/src/test/java/io/uhndata/cards/utils/internal/PreventVersionOverrideServletFilterTest.java b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/PreventVersionOverrideServletFilterTest.java new file mode 100644 index 0000000000..31fd25fcd7 --- /dev/null +++ b/modules/utils/src/test/java/io/uhndata/cards/utils/internal/PreventVersionOverrideServletFilterTest.java @@ -0,0 +1,139 @@ +/* + * 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 io.uhndata.cards.utils.internal; + +import java.io.IOException; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingJakartaHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.junit.Test; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link PreventVersionOverrideServletFilter}. + * + * @version $Id$ + */ +public class PreventVersionOverrideServletFilterTest +{ + private static final String BASE_VERSION_PARAMETER = ":baseVersion"; + + private static final String CURRENT_VERSION_PATH = "/jcr:system/jcr:versionStorage/f1/1.0"; + + private final PreventVersionOverrideServletFilter filter = new PreventVersionOverrideServletFilter(); + + private final SlingJakartaHttpServletRequest request = mock(SlingJakartaHttpServletRequest.class); + + private final ServletResponse response = mock(ServletResponse.class); + + private final FilterChain chain = mock(FilterChain.class); + + @Test + public void initAndDestroyDoNothing() throws ServletException + { + this.filter.init(null); + this.filter.destroy(); + } + + @Test + public void doFilterContinuesForNonSlingRequest() throws IOException, ServletException + { + ServletRequest plainRequest = mock(ServletRequest.class); + when(plainRequest.getParameter(BASE_VERSION_PARAMETER)).thenReturn(CURRENT_VERSION_PATH); + this.filter.doFilter(plainRequest, this.response, this.chain); + verify(this.chain).doFilter(plainRequest, this.response); + } + + @Test + public void doFilterContinuesWithoutBaseVersionParameter() throws IOException, ServletException + { + when(this.request.getParameter(BASE_VERSION_PARAMETER)).thenReturn(null); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.chain).doFilter(this.request, this.response); + } + + @Test + public void doFilterContinuesForNonVersionableResource() throws IOException, RepositoryException, ServletException + { + mockResource(null); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.chain).doFilter(this.request, this.response); + } + + @Test + public void doFilterContinuesWhenBaseVersionMatches() throws IOException, RepositoryException, ServletException + { + Node node = mockVersionedNode(CURRENT_VERSION_PATH); + mockResource(node); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.chain).doFilter(this.request, this.response); + } + + @Test + public void doFilterRejectsOutdatedBaseVersion() throws IOException, RepositoryException, ServletException + { + Node node = mockVersionedNode("/jcr:system/jcr:versionStorage/f1/2.0"); + mockResource(node); + assertThrows(ServletException.class, () -> this.filter.doFilter(this.request, this.response, this.chain)); + verify(this.request).setAttribute("jakarta.servlet.error.status_code", HttpServletResponse.SC_CONFLICT); + verify(this.chain, never()).doFilter(this.request, this.response); + } + + @Test + public void doFilterContinuesOnRepositoryException() throws IOException, RepositoryException, ServletException + { + Node node = mock(Node.class); + when(node.getProperty("jcr:baseVersion")).thenThrow(new RepositoryException()); + mockResource(node); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.chain).doFilter(this.request, this.response); + } + + private void mockResource(final Node node) + { + when(this.request.getParameter(BASE_VERSION_PARAMETER)).thenReturn(CURRENT_VERSION_PATH); + Resource resource = mock(Resource.class); + when(this.request.getResource()).thenReturn(resource); + when(resource.adaptTo(Node.class)).thenReturn(node); + } + + private Node mockVersionedNode(final String baseVersionPath) throws RepositoryException + { + Node node = mock(Node.class); + Property baseVersion = mock(Property.class); + Node versionNode = mock(Node.class); + when(node.getProperty("jcr:baseVersion")).thenReturn(baseVersion); + when(baseVersion.getNode()).thenReturn(versionNode); + when(versionNode.getPath()).thenReturn(baseVersionPath); + return node; + } +}