From 405f8068c30a5e76b35744492ca33474249aab9c Mon Sep 17 00:00:00 2001 From: jobulcke <127748878+jobulcke@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:51:43 +0200 Subject: [PATCH] Feat/increase coverage (#11) * feat: first tests added * feat: all missing tests added * feat: last fixes added * feat: last small increase --- spring-boot-backend/pom.xml | 8 ++ .../application/valueobjects/Triple.java | 20 ++++ .../triple/infra/TripleRepositoryImpl.java | 11 +-- .../triple/rest/TripleExceptionHandler.java | 23 +++++ .../triple/rest/TriplesController.java | 3 +- .../src/main/resources/application.yaml | 22 +++++ .../services/TripleServiceImplTest.java | 91 +++++++++++++++++++ .../application/valueobjects/TripleTest.java | 52 +++++++++++ .../infra/TripleRepositoryImplTest.java | 76 ++++++++++++++++ .../triple/rest/TriplesControllerTest.java | 73 +++++++++++++++ vue-frontend/src/components/map/useMarkers.js | 7 -- vue-frontend/vite.config.js | 1 - 12 files changed, 370 insertions(+), 17 deletions(-) create mode 100644 spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TripleExceptionHandler.java create mode 100644 spring-boot-backend/src/main/resources/application.yaml create mode 100644 spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/services/TripleServiceImplTest.java create mode 100644 spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/TripleTest.java create mode 100644 spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImplTest.java create mode 100644 spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesControllerTest.java diff --git a/spring-boot-backend/pom.xml b/spring-boot-backend/pom.xml index a132b11..4511e75 100644 --- a/spring-boot-backend/pom.xml +++ b/spring-boot-backend/pom.xml @@ -26,6 +26,7 @@ 5.10.0 5.4.0 7.13.0 + 2.35.0 4.3.3 @@ -147,6 +148,13 @@ junit-platform-suite ${junit-platform-suite.version} + + com.github.tomakehurst + wiremock-jre8-standalone + ${wiremock.version} + test + + diff --git a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/Triple.java b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/Triple.java index b0db2ac..ee064c5 100644 --- a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/Triple.java +++ b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/Triple.java @@ -11,6 +11,26 @@ public Triple(String subject, String predicate, String object) { this.object = object; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Triple triple = (Triple) o; + + if (!subject.equals(triple.subject)) return false; + if (!predicate.equals(triple.predicate)) return false; + return object.equals(triple.object); + } + + @Override + public int hashCode() { + int result = subject.hashCode(); + result = 31 * result + predicate.hashCode(); + result = 31 * result + object.hashCode(); + return result; + } + public String getSubject() { return subject; } diff --git a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImpl.java b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImpl.java index 0a6f4ba..55cf72e 100644 --- a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImpl.java +++ b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImpl.java @@ -12,13 +12,13 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.impl.TreeModelFactory; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.Rio; import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @Repository public class TripleRepositoryImpl implements TripleRepository { @@ -42,13 +42,10 @@ public MemberDescription getById(String id) { //Execute and get the response. HttpResponse response = httpclient.execute(httppost); - HttpEntity entity = response.getEntity(); + HttpEntity entity = Objects.requireNonNull(response.getEntity()); - if (entity != null) { - Model model = Rio.parse(entity.getContent(), RDFFormat.NTRIPLES); - return new MemberDescription(id, model); - } - return new MemberDescription(id, new TreeModelFactory().createEmptyModel()); + Model model = Rio.parse(entity.getContent(), RDFFormat.NTRIPLES); + return new MemberDescription(id, model); } catch (Exception e) { throw new TripleFetchFailedException(id, e); } diff --git a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TripleExceptionHandler.java b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TripleExceptionHandler.java new file mode 100644 index 0000000..36130f7 --- /dev/null +++ b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TripleExceptionHandler.java @@ -0,0 +1,23 @@ +package be.informatievlaanderen.vsds.demonstrator.triple.rest; + +import be.informatievlaanderen.vsds.demonstrator.triple.infra.exceptions.TripleFetchFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class TripleExceptionHandler extends ResponseEntityExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(TripleExceptionHandler.class); + + @ExceptionHandler(value = TripleFetchFailedException.class) + protected ResponseEntity handleNotFound(RuntimeException e, WebRequest request) { + log.error(e.getMessage()); + return handleExceptionInternal(e, e.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND, request); + } +} diff --git a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesController.java b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesController.java index 468344c..63bffd7 100644 --- a/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesController.java +++ b/spring-boot-backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesController.java @@ -22,8 +22,7 @@ public TriplesController(TripleService tripleService) { @CrossOrigin(origins = "*", allowedHeaders = "*") public List retrieveTriplesOfNode(HttpServletRequest request) { String requestURL = request.getRequestURL().toString(); - String memberId = requestURL.split("/triples/")[1]; return tripleService.getTriplesById(memberId); } -} +} \ No newline at end of file diff --git a/spring-boot-backend/src/main/resources/application.yaml b/spring-boot-backend/src/main/resources/application.yaml new file mode 100644 index 0000000..3ef4bbd --- /dev/null +++ b/spring-boot-backend/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:25432/test + username: postgres + password: test + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: update + +ldes: + streams: + - + member-type: "https://data.vlaanderen.be/ns/mobiliteit#Mobiliteitshinder" + timestamp-path: "http://www.w3.org/ns/prov#generatedAtTime" +graphdb: + url: "http://localhost:8080/rdf4j-server/repositories/" + repositoryId: "test" +server: + port: 8084 \ No newline at end of file diff --git a/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/services/TripleServiceImplTest.java b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/services/TripleServiceImplTest.java new file mode 100644 index 0000000..d7a83fc --- /dev/null +++ b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/services/TripleServiceImplTest.java @@ -0,0 +1,91 @@ +package be.informatievlaanderen.vsds.demonstrator.triple.application.services; + +import be.informatievlaanderen.vsds.demonstrator.triple.application.valueobjects.Triple; +import be.informatievlaanderen.vsds.demonstrator.triple.domain.entities.MemberDescription; +import be.informatievlaanderen.vsds.demonstrator.triple.domain.repositories.TripleRepository; +import be.informatievlaanderen.vsds.demonstrator.triple.infra.exceptions.TripleFetchFailedException; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.impl.TreeModelFactory; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.util.ResourceUtils; +import wiremock.com.google.common.io.Files; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TripleServiceImplTest { + private static final String MEMBER_ID = "https://private-api.gipod.beta-vlaanderen.be/api/v1/mobility-hindrances/10810464/#ID"; + @Mock + private TripleRepository repository; + private TripleService service; + + @BeforeEach + void setUp() { + service = new TripleServiceImpl(repository); + } + + @Test + void when_MemberIsPresent_then_RetrieveTriples() throws IOException { + when(repository.getById(MEMBER_ID)).thenReturn(new MemberDescription(MEMBER_ID, readModelFromFile())); + + var triples = readTriplesFromFile(); + var fetchedTriples = service.getTriplesById(MEMBER_ID); + + Predicate subjectPredicate = triple -> triple.getSubject().equals(MEMBER_ID); + assertEquals(triples.size(), fetchedTriples.size()); + assertEquals(triples.stream().filter(subjectPredicate).count(), fetchedTriples.stream().filter(subjectPredicate).count()); + verify(repository).getById(MEMBER_ID); + } + + @Test + void when_MemberHasEmptyModel_then_RetrieveEmptyList() { + when(repository.getById(MEMBER_ID)).thenReturn(new MemberDescription(MEMBER_ID, new TreeModelFactory().createEmptyModel())); + + var fetchedTriples = service.getTriplesById(MEMBER_ID); + + assertEquals(0, fetchedTriples.size()); + verify(repository).getById(MEMBER_ID); + } + + @Test + void when_MemberCannotBeFetched_then_RetrieveEmptyList() { + when(repository.getById(MEMBER_ID)).thenThrow(new TripleFetchFailedException(MEMBER_ID, new RuntimeException())); + + assertThrows(TripleFetchFailedException.class, () -> service.getTriplesById(MEMBER_ID)); + verify(repository).getById(MEMBER_ID); + } + + + private Model readModelFromFile() throws IOException { + File file = ResourceUtils.getFile("classpath:members/mobility-hindrance.nq"); + return Rio.parse(new FileInputStream(file), RDFFormat.NQUADS); + } + + private List readTriplesFromFile() throws IOException { + File file = ResourceUtils.getFile("classpath:members/mobility-hindrance.nq"); + Function transformer = str -> str.length() > 1 ? str.substring(1, str.length() - 1) : str; + return Files.readLines(file, Charset.defaultCharset()).stream() + .map(line -> line.split(" ")) + .map(strings -> Arrays.stream(strings).map(transformer).toList()) + .map(strings -> new Triple(strings.get(0), strings.get(1), strings.get(2))) + .toList(); + } +} \ No newline at end of file diff --git a/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/TripleTest.java b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/TripleTest.java new file mode 100644 index 0000000..8d1e9e5 --- /dev/null +++ b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/application/valueobjects/TripleTest.java @@ -0,0 +1,52 @@ +package be.informatievlaanderen.vsds.demonstrator.triple.application.valueobjects; + +import org.antlr.v4.runtime.tree.Tree; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class TripleTest { + private static final String SUBJECT = "my-subject"; + private static final String PREDICATE = "my-predicate"; + private static final String OBJECT = "my-object"; + private static final Triple triple = new Triple(SUBJECT, PREDICATE, OBJECT); + + @Test + void test_equality() { + final Triple other = new Triple(SUBJECT, PREDICATE, OBJECT); + + assertEquals(triple.hashCode(), triple.hashCode()); + assertEquals(triple.hashCode(), other.hashCode()); + + assertEquals(triple, triple); + assertEquals(triple, other); + assertEquals(other, triple); + } + + @ParameterizedTest + @ArgumentsSource(TripleArgumentsProvider.class) + void test_inequality(Object other) { + assertNotEquals(triple, other); + } + + static class TripleArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) throws Exception { + return Stream.of( + new Triple("false-subject", "false-predicate", "false-object"), + new Triple("other-subject", PREDICATE, OBJECT), + new Triple(SUBJECT, "other-predicate", OBJECT), + new Triple(SUBJECT, PREDICATE, "other-object"), + null, + new Object() + ).map(Arguments::of); + } + } +} \ No newline at end of file diff --git a/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImplTest.java b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImplTest.java new file mode 100644 index 0000000..c4110b2 --- /dev/null +++ b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/infra/TripleRepositoryImplTest.java @@ -0,0 +1,76 @@ +package be.informatievlaanderen.vsds.demonstrator.triple.infra; + +import be.informatievlaanderen.vsds.demonstrator.triple.domain.entities.MemberDescription; +import be.informatievlaanderen.vsds.demonstrator.triple.domain.repositories.TripleRepository; +import be.informatievlaanderen.vsds.demonstrator.triple.infra.exceptions.TripleFetchFailedException; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.util.ResourceUtils; +import wiremock.com.fasterxml.jackson.databind.JsonNode; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +@WireMockTest(httpPort = 8080) +class TripleRepositoryImplTest { + private static final String MEMBER_ID = "https://private-api.gipod.beta-vlaanderen.be/api/v1/mobility-hindrances/10810464/#ID"; + private static final String ENDPOINT = "/rdf4j-server/repositories/test"; + private static final GraphDBConfig graphDbConfig = new GraphDBConfig(); + private TripleRepository repo; + + @BeforeAll + static void beforeAll() { + graphDbConfig.setUrl("http://localhost:8080/rdf4j-server/repositories/"); + graphDbConfig.setRepositoryId("test"); + } + + @BeforeEach + void setUp() { + repo = new TripleRepositoryImpl(graphDbConfig); + } + + @Test + void when_ExistingTriplesAreRequested_then_MemberDescriptionIsExpected() throws IOException { + stubFor(post(ENDPOINT).willReturn(ok().withBody(readDataFromFile()))); + + MemberDescription memberDescription = repo.getById(MEMBER_ID); + + assertEquals(MEMBER_ID, memberDescription.getMemberId()); + assertFalse(memberDescription.getModel().isEmpty()); + verify(postRequestedFor(urlEqualTo(ENDPOINT))); + } + + @Test + void when_BadRequestIsSent_then_EmptyModelIsExpected() { + stubFor(post(ENDPOINT).willReturn(badRequest())); + + MemberDescription memberDescription = repo.getById(MEMBER_ID); + + assertEquals(MEMBER_ID, memberDescription.getMemberId()); + assertTrue(memberDescription.getModel().isEmpty()); + verify(postRequestedFor(urlEqualTo(ENDPOINT))); + } + + @Test + void when_RequestFails_then_ExceptionIsExpected() { + stubFor(post(ENDPOINT).willReturn(badRequest().withFault(Fault.EMPTY_RESPONSE))); + + assertThrows(TripleFetchFailedException.class, () -> repo.getById(MEMBER_ID)); + verify(postRequestedFor(urlEqualTo(ENDPOINT))); + } + + private byte[] readDataFromFile() throws IOException { + Path path = ResourceUtils.getFile("classpath:members/mobility-hindrance.nq").toPath(); + return Files.readAllBytes(path); + } +} \ No newline at end of file diff --git a/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesControllerTest.java b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesControllerTest.java new file mode 100644 index 0000000..c169810 --- /dev/null +++ b/spring-boot-backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/triple/rest/TriplesControllerTest.java @@ -0,0 +1,73 @@ +package be.informatievlaanderen.vsds.demonstrator.triple.rest; + +import be.informatievlaanderen.vsds.demonstrator.triple.application.services.TripleService; +import be.informatievlaanderen.vsds.demonstrator.triple.application.valueobjects.Triple; +import be.informatievlaanderen.vsds.demonstrator.triple.infra.exceptions.TripleFetchFailedException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.ResourceUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest +@ContextConfiguration(classes = {TriplesController.class, TripleExceptionHandler.class}) +class TriplesControllerTest { + private static final String ID = "https://private-api.gipod.beta-vlaanderen.be/api/v1/mobility-hindrances/10810464"; + + @MockBean + private TripleService tripleService; + + @Autowired + private MockMvc mockMvc; + + @Test + void when_triplesFetchingSucceeded_then_ReturnStatus200() throws Exception { + List triples = readTriplesFromFile(); + String json = new ObjectMapper().writeValueAsString(triples); + when(tripleService.getTriplesById(ID)).thenReturn(triples); + + mockMvc.perform(get(URI.create("/triples/" + ID))) + .andExpect(status().isOk()) + .andExpect(content().json(json)); + + verify(tripleService).getTriplesById(ID); + } + + @Test + void when_triplesFetchingFails_then_ReturnStatus404() throws Exception { + when(tripleService.getTriplesById(ID)).thenThrow(new TripleFetchFailedException(ID, new RuntimeException())); + + mockMvc.perform(get(URI.create("/triples/" + ID))) + .andExpect(status().isNotFound()); + + verify(tripleService).getTriplesById(ID); + } + + private List readTriplesFromFile() throws IOException { + Path path = ResourceUtils.getFile("classpath:members/mobility-hindrance.nq").toPath(); + Function transformer = str -> str.length() > 1 ? str.substring(1, str.length() - 1) : str; + + return Files.readAllLines(path).stream() + .map(str -> str.split(" ")) + .map(strings -> Arrays.stream(strings).map(transformer).toList()) + .map(strings -> new Triple(strings.get(0), strings.get(1), strings.get(2))) + .toList(); + } +} \ No newline at end of file diff --git a/vue-frontend/src/components/map/useMarkers.js b/vue-frontend/src/components/map/useMarkers.js index bf2b58f..4c2cca3 100644 --- a/vue-frontend/src/components/map/useMarkers.js +++ b/vue-frontend/src/components/map/useMarkers.js @@ -83,13 +83,6 @@ export function useMarkers(memberGeometries) { .append("circle") .attr("class", "node") .attr("r", 8) - // .on("click", async function (d) { - // let triples = await axios - // .get('http://localhost:8080/'+d.name) - // await this.visualizeTriples(triples.data); - // // alert("You clicked on node " + d.name); - // } - // .bind(this)) .call(drag); function ticked() { diff --git a/vue-frontend/vite.config.js b/vue-frontend/vite.config.js index 00247f8..04fce13 100644 --- a/vue-frontend/vite.config.js +++ b/vue-frontend/vite.config.js @@ -18,7 +18,6 @@ export default defineConfig({ origin: "*" }, proxy: { - // string shorthand: http://localhost:5173/foo -> http://localhost:8084/foo '/in-rectangle': 'http://localhost:8084', '/triples': 'http://localhost:8084' }