From e0e241ae3b7479247b15bcda5a42502bb515eb7f Mon Sep 17 00:00:00 2001 From: Willy Mehling Date: Mon, 13 Apr 2026 17:25:46 +0200 Subject: [PATCH 1/2] kvin: add explicit success/error envelopes, improve http error transparency, and expand contract tests --- .../core/kvin/http/KvinHttp.java | 29 +++- .../linkedfactory/service/KvinService.scala | 55 +++++-- .../linkedfactory/core/kvin/KvinHttpTest.java | 21 +++ .../service/KvinServiceTest.scala | 147 +++++++++++++----- 4 files changed, 195 insertions(+), 57 deletions(-) diff --git a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java index f6fb9c4f..749eddeb 100644 --- a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java +++ b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java @@ -30,12 +30,14 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; @@ -202,8 +204,13 @@ protected IExtendedIterator fetchInternal(List items, List } response = this.httpClient.execute(request); HttpEntity entity = response.getEntity(); - if (response.getStatusLine().getStatusCode() != 200) { - return NiceIterator.emptyIterator(); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + if (status == 404) { + return NiceIterator.emptyIterator(); + } + String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; + throw new RuntimeException("HTTP " + status + " while fetching values: " + body); } // converting json to kvin tuples // TODO directly read from stream with pooled HTTP client @@ -262,8 +269,13 @@ private IExtendedIterator descendantsInternal(URI item, URI context, Long l HttpGet httpGet = createHttpGet(getRequestUri.toString()); HttpResponse response = this.httpClient.execute(httpGet); HttpEntity entity = response.getEntity(); - if (response.getStatusLine().getStatusCode() != 200) { - return NiceIterator.emptyIterator(); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + if (status == 404) { + return NiceIterator.emptyIterator(); + } + String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; + throw new RuntimeException("HTTP " + status + " while fetching descendants: " + body); } // converting json to URI return new NiceIterator<>() { @@ -329,8 +341,13 @@ public IExtendedIterator properties(URI item, URI context) { HttpGet httpGet = createHttpGet(getRequestUri.toString()); HttpResponse response = this.httpClient.execute(httpGet); HttpEntity entity = response.getEntity(); - if (response.getStatusLine().getStatusCode() != 200) { - return NiceIterator.emptyIterator(); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + if (status == 404) { + return NiceIterator.emptyIterator(); + } + String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; + throw new RuntimeException("HTTP " + status + " while fetching properties: " + body); } // converting json to URI return new NiceIterator<>() { diff --git a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala index 3ad60e50..bd2b6429 100644 --- a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala +++ b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala @@ -24,7 +24,7 @@ import net.enilink.komma.core.{URI, URIs} import net.liftweb.common.Box.box2Iterable import net.liftweb.common._ import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{BadRequestResponse, InMemoryResponse, JsonResponse, LiftResponse, OkResponse, OutputStreamResponse, PlainTextResponse, Req, S} +import net.liftweb.http.{InMemoryResponse, JsonResponse, LiftResponse, OutputStreamResponse, Req, S} import net.liftweb.json.Extraction.decompose import net.liftweb.json.JsonAST._ import net.liftweb.json.JsonDSL._ @@ -48,7 +48,35 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga def responseHeaders: List[(String, String)] = CORS_HEADERS ::: S.getResponseHeaders(Nil) object FailureResponse { - def apply(msg: String): PlainTextResponse = PlainTextResponse(msg, Nil, 400) + def apply(msg: String): LiftResponse = createErrorResponse(400, "INVALID_PAYLOAD", msg) + } + + def createErrorResponse(status: Int, code: String, message: String, details: Box[String] = Empty): LiftResponse = { + val body = details.filter(_.nonEmpty).map { d => + ("status" -> status) ~ ("code" -> code) ~ ("message" -> message) ~ ("details" -> d) + } openOr { + ("status" -> status) ~ ("code" -> code) ~ ("message" -> message) + } + JsonResponse(body, responseHeaders, S.responseCookies, status) + } + + def createSuccessResponse(code: String = "OK", message: Box[String] = Empty, data: JObject = JObject(Nil)): LiftResponse = { + val baseFields = List( + JField("success", JBool(true)), + JField("status", JInt(200)), + JField("code", JString(code)) + ) ::: message.map(m => JField("message", JString(m))).toList + JsonResponse(JObject(baseFields ::: data.obj), responseHeaders, S.responseCookies, 200) + } + + private def withServerErrorResponse(code: String)(f: => LiftResponse): LiftResponse = { + try { + f + } catch { + case e: Exception => + logger.error(s"Request failed with code $code", e) + createErrorResponse(500, code, "Internal server error", Full(Option(e.getMessage).getOrElse(e.getClass.getSimpleName))) + } } protected def csvResponse_?(r: Req): Boolean = { @@ -87,12 +115,21 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } result match { case Failure(msg, _, _) => FailureResponse(msg) - case _ => OkResponse() + case _ => createSuccessResponse(message = Full("Values stored.")) + } + case list Get _ if list.endsWith("properties" :: Nil) => + withServerErrorResponse("PROPERTIES_QUERY_FAILED") { + createJsonResponse(getProperties(path ++ list.dropRight(1))) + } + case list Get _ if list.endsWith("**" :: Nil) => + withServerErrorResponse("DESCENDANTS_QUERY_FAILED") { + createJsonResponse(getDescendants(path ++ list.dropRight(1))) } - case list Get _ if list.endsWith("properties" :: Nil) => createJsonResponse(getProperties(path ++ list.dropRight(1))) - case list Get _ if list.endsWith("**" :: Nil) => createJsonResponse(getDescendants(path ++ list.dropRight(1))) - case list Delete _ if list.endsWith("values" :: Nil) => createJsonResponse(deleteValues(path ++ list.dropRight(1))) + case list Delete _ if list.endsWith("values" :: Nil) => + withServerErrorResponse("DELETE_VALUES_FAILED") { + createSuccessResponse(data = deleteValues(path ++ list.dropRight(1))) + } // case list Get _ => // TODO return RDF description }) @@ -100,7 +137,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga val limit = S.param("limit") flatMap (v => tryo(v.toLong)) filter (_ > 0) openOr 10000L if (limit > MAX_LIMIT) { - FailureResponse("The maximum limit is " + MAX_LIMIT + ". Please use multiple request if you require more data points.") + createErrorResponse(400, "LIMIT_TOO_LARGE", "The maximum limit is " + MAX_LIMIT + ". Please use multiple request if you require more data points.") } else { def filename(defaultExt: String) = S.param("filename") openOr "values." + defaultExt @@ -222,8 +259,8 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } OutputStreamResponse(streamer, -1, ("Content-Type", "text/csv; charset=utf-8") :: ("Content-Disposition", s"""inline; filename=${filename("csv")}""") :: responseHeaders, S.responseCookies, 200) - case _ => BadRequestResponse() - } openOr BadRequestResponse() + case _ => createErrorResponse(400, "UNSUPPORTED_RESPONSE_TYPE", "Unsupported response type. Use application/json or text/csv.") + } openOr createErrorResponse(400, "UNSUPPORTED_RESPONSE_TYPE", "Unsupported response type. Use application/json or text/csv.") response } } diff --git a/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java b/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java index 2fc740fd..ad81742b 100644 --- a/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java +++ b/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java @@ -280,6 +280,27 @@ public void shouldReturnPropertiesForItem() { } } + @Test + public void shouldThrowOnNon404FetchError() throws Exception { + doReturn(mockedResponse("{\"code\":\"INTERNAL\"}", 500)).when(httpClient).execute(any()); + + try { + kvinHttp.fetch(URIs.createURI("http://example.org/item1"), null, null, 10).toList(); + Assert.fail("Expected RuntimeException for non-404 HTTP error"); + } catch (RuntimeException e) { + String message = e.getMessage() != null ? e.getMessage() : String.valueOf(e.getCause()); + Assert.assertTrue(message.contains("500") || (e.getCause() != null && String.valueOf(e.getCause().getMessage()).contains("500"))); + } + } + + @Test + public void shouldReturnEmptyOn404Properties() throws Exception { + doReturn(mockedResponse("", 404)).when(httpClient).execute(any()); + + var properties = kvinHttp.properties(URIs.createURI("http://example.org/item1"), null).toList(); + Assert.assertTrue(properties.isEmpty()); + } + private static List generateTuples(int numberOfItems, int numberOfProperties, int numberOfValues) { return new KvinTupleGenerator() .setStartTime(System.currentTimeMillis()) diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala index 310ec709..15019532 100644 --- a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala +++ b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala @@ -106,6 +106,23 @@ class KvinServiceTest { kvinService(req) } + private def responseBody(response: LiftResponse): String = response match { + case r: OutputStreamResponse => + val rStream = new ByteArrayOutputStream() + r.out(rStream) + rStream.toString() + case r: InMemoryResponse => + new String(r.data) + case _ => + throw new RuntimeException("Invalid response type") + } + + private def responseContentType(response: LiftResponse): String = response.toResponse match { + case r: OutputStreamResponse => r.headers.find(_._1.equalsIgnoreCase("Content-Type")).map(_._2).getOrElse("") + case r: InMemoryResponse => r.headers.find(_._1.equalsIgnoreCase("Content-Type")).map(_._2).getOrElse("") + case _ => "" + } + val baseUrl = "http://foo.com/linkedfactory/values" def toReq(httpRequest: HttpServletRequest): Req = { @@ -128,7 +145,12 @@ class KvinServiceTest { method = "POST" body_=(TestData.item1, "application/json") } - assertEquals(Full(200), kvinRest(toReq(postReq))().map(_.toResponse.code)) + val postResponse = kvinRest(toReq(postReq))().map(_.toResponse).openOr(null) + assertEquals(200, postResponse.code) + val postBody = responseBody(postResponse) + assertTrue(postBody.contains("\"success\":true")) + assertTrue(postBody.contains("\"code\":\"OK\"")) + assertTrue(postBody.contains("Values stored")) // support get request val getReq = new MockHttpServletRequest(baseUrl) { @@ -138,6 +160,48 @@ class KvinServiceTest { assertEquals(Full(200), kvinRest(toReq(getReq))().map(_.toResponse.code)) } + @Test + def explicitEnvelopeResponsesUseJsonContentType(): Unit = { + val postReq = new MockHttpServletRequest(baseUrl) { + method = "POST" + body_=(TestData.item1, "application/json") + } + val postResponse = kvinRest(toReq(postReq))().map(_.toResponse).openOr(null) + assertEquals(200, postResponse.code) + assertTrue(responseContentType(postResponse).toLowerCase.contains("application/json")) + + val invalidPostReq = new MockHttpServletRequest(baseUrl) { + method = "POST" + body_=("""{ "item" : [false, 1]} }""", "application/json") + } + val invalidResponse = kvinRest(toReq(invalidPostReq))().map(_.toResponse).openOr(null) + assertEquals(400, invalidResponse.code) + assertTrue(responseContentType(invalidResponse).toLowerCase.contains("application/json")) + } + + @Test + def deleteValuesReturnsExplicitSuccessEnvelope(): Unit = { + val postReq = new MockHttpServletRequest(baseUrl) { + method = "POST" + body_=(TestData.itemSet, "application/json") + } + assertEquals(200, kvinRest(toReq(postReq))().map(_.toResponse).openOr(null).code) + + val deleteReq = new MockHttpServletRequest(baseUrl) { + method = "DELETE" + parameters = List(("item", "http://example.org/item2"), ("property", "http://example.org/properties/p2")) + headers = (("Accept", "application/json" :: Nil) :: Nil).toMap + } + + val response = kvinRest(toReq(deleteReq))().map(_.toResponse).openOr(null) + assertEquals(200, response.code) + + val body = responseBody(response) + assertTrue(body.contains("\"success\":true")) + assertTrue(body.contains("\"code\":\"OK\"")) + assertTrue(body.contains("\"deleted\":")) + } + @Test def postRequestInvalidData(): Unit = { // reject post request with invalid data @@ -145,7 +209,42 @@ class KvinServiceTest { method = "POST" body_=("""{ "item" : [false, 1]} }""", "application/json") } - assertEquals(Full(400), kvinRest(toReq(invalidPostReq))().map(_.toResponse.code)) + val response = kvinRest(toReq(invalidPostReq))().map(_.toResponse).openOr(null) + assertEquals(400, response.code) + + val body = responseBody(response) + assertTrue(body.contains("\"code\":\"INVALID_PAYLOAD\"")) + assertTrue(body.contains("\"message\":")) + } + + @Test + def queryDataWithTooLargeLimitReturnsExplicitError(): Unit = { + val getReq = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List(("limit", "500001")) + headers = (("Accept", "application/json" :: Nil) :: Nil).toMap + } + + val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) + assertEquals(400, response.code) + + val body = responseBody(response) + assertTrue(body.contains("\"code\":\"LIMIT_TOO_LARGE\"")) + assertTrue(body.contains("maximum limit")) + } + + @Test + def queryDataWithUnsupportedResponseTypeReturnsExplicitError(): Unit = { + val getReq = new MockHttpServletRequest(baseUrl) { + method = "GET" + headers = (("Accept", "application/xml" :: Nil) :: Nil).toMap + } + + val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) + assertEquals(400, response.code) + + val body = responseBody(response) + assertTrue(body.contains("\"code\":\"UNSUPPORTED_RESPONSE_TYPE\"")) } @Test @@ -165,16 +264,7 @@ class KvinServiceTest { //var response = kvinRest(toReq(getReq))().toList.map((response) => response.toString) val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) - val stringResponse: String = response match { - case r: OutputStreamResponse => - val rStream = new ByteArrayOutputStream() - r.out(rStream) - rStream.toString() - case r: InMemoryResponse => - r.data.toString - case _ => - throw new RuntimeException("Invalid response type") - } + val stringResponse: String = responseBody(response) val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() while (kvinTuples.hasNext) { @@ -202,16 +292,7 @@ class KvinServiceTest { } val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) - val stringResponse: String = response match { - case r: OutputStreamResponse => - val rStream = new ByteArrayOutputStream() - r.out(rStream) - rStream.toString() - case r: InMemoryResponse => - r.data.toString - case _ => - throw new RuntimeException("Invalid response type") - } + val stringResponse: String = responseBody(response) val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() assertEquals(kvinTuples.toList.size(), 2) @@ -234,16 +315,7 @@ class KvinServiceTest { //var response = kvinRest(toReq(getReq))().toList.map((response) => response.toString) val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) - val stringResponse: String = response match { - case r: OutputStreamResponse => - val rStream = new ByteArrayOutputStream() - r.out(rStream) - rStream.toString() - case r: InMemoryResponse => - r.data.toString - case _ => - throw new RuntimeException("Invalid response type") - } + val stringResponse: String = responseBody(response) val kvinTuples: NiceIterator[KvinTuple] = new JsonFormatParser(new ByteArrayInputStream(stringResponse.getBytes())).parse() var count = 0 @@ -276,16 +348,7 @@ class KvinServiceTest { } val response = kvinRest(toReq(getReq))().map(_.toResponse).openOr(null) - val stringResponse: String = response match { - case r: OutputStreamResponse => - val rStream = new ByteArrayOutputStream() - r.out(rStream) - rStream.toString() - case r: InMemoryResponse => - new String(r.data) - case _ => - throw new RuntimeException("Invalid response type") - } + val stringResponse: String = responseBody(response) val mapper: ObjectMapper = new ObjectMapper() val node: JsonNode = mapper.readTree(stringResponse) From 91324631b3f57ca666f9e9260342ee453d0c6262 Mon Sep 17 00:00:00 2001 From: Willy Mehling Date: Tue, 14 Apr 2026 09:39:40 +0200 Subject: [PATCH 2/2] kvin: align API responses with review feedback (POST/DELETE simplification, plain errors, UncheckedIOException) --- .../core/kvin/http/KvinHttp.java | 13 ++++++++++--- .../linkedfactory/service/KvinService.scala | 10 +++++----- .../linkedfactory/core/kvin/KvinHttpTest.java | 5 +++-- .../service/KvinServiceTest.scala | 19 +++++-------------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java index 749eddeb..15387d20 100644 --- a/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java +++ b/bundles/io.github.linkedfactory.core/src/main/java/io/github/linkedfactory/core/kvin/http/KvinHttp.java @@ -37,6 +37,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; @@ -210,13 +211,15 @@ protected IExtendedIterator fetchInternal(List items, List return NiceIterator.emptyIterator(); } String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; - throw new RuntimeException("HTTP " + status + " while fetching values: " + body); + throw new UncheckedIOException(new IOException("HTTP " + status + " while fetching values: " + body)); } // converting json to kvin tuples // TODO directly read from stream with pooled HTTP client content = entity.getContent(); JsonFormatParser jsonParser = new JsonFormatParser(new ByteArrayInputStream(ByteStreams.toByteArray(content))); return jsonParser.parse(); + } catch (UncheckedIOException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } finally { @@ -275,7 +278,7 @@ private IExtendedIterator descendantsInternal(URI item, URI context, Long l return NiceIterator.emptyIterator(); } String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; - throw new RuntimeException("HTTP " + status + " while fetching descendants: " + body); + throw new UncheckedIOException(new IOException("HTTP " + status + " while fetching descendants: " + body)); } // converting json to URI return new NiceIterator<>() { @@ -324,6 +327,8 @@ public void close() { } } }; + } catch (UncheckedIOException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } @@ -347,7 +352,7 @@ public IExtendedIterator properties(URI item, URI context) { return NiceIterator.emptyIterator(); } String body = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; - throw new RuntimeException("HTTP " + status + " while fetching properties: " + body); + throw new UncheckedIOException(new IOException("HTTP " + status + " while fetching properties: " + body)); } // converting json to URI return new NiceIterator<>() { @@ -400,6 +405,8 @@ public void close() { } } }; + } catch (UncheckedIOException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala index bd2b6429..ca0edd9d 100644 --- a/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala +++ b/bundles/io.github.linkedfactory.service/src/main/scala/io/github/linkedfactory/service/KvinService.scala @@ -24,7 +24,7 @@ import net.enilink.komma.core.{URI, URIs} import net.liftweb.common.Box.box2Iterable import net.liftweb.common._ import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{InMemoryResponse, JsonResponse, LiftResponse, OutputStreamResponse, Req, S} +import net.liftweb.http.{InMemoryResponse, JsonResponse, LiftResponse, OkResponse, OutputStreamResponse, Req, S} import net.liftweb.json.Extraction.decompose import net.liftweb.json.JsonAST._ import net.liftweb.json.JsonDSL._ @@ -53,9 +53,9 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga def createErrorResponse(status: Int, code: String, message: String, details: Box[String] = Empty): LiftResponse = { val body = details.filter(_.nonEmpty).map { d => - ("status" -> status) ~ ("code" -> code) ~ ("message" -> message) ~ ("details" -> d) + ("code" -> code) ~ ("message" -> message) ~ ("details" -> d) } openOr { - ("status" -> status) ~ ("code" -> code) ~ ("message" -> message) + ("code" -> code) ~ ("message" -> message) } JsonResponse(body, responseHeaders, S.responseCookies, status) } @@ -115,7 +115,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga } result match { case Failure(msg, _, _) => FailureResponse(msg) - case _ => createSuccessResponse(message = Full("Values stored.")) + case _ => OkResponse() } case list Get _ if list.endsWith("properties" :: Nil) => withServerErrorResponse("PROPERTIES_QUERY_FAILED") { @@ -128,7 +128,7 @@ class KvinService(path: List[String], store: Kvin) extends RestHelper with Logga case list Delete _ if list.endsWith("values" :: Nil) => withServerErrorResponse("DELETE_VALUES_FAILED") { - createSuccessResponse(data = deleteValues(path ++ list.dropRight(1))) + createJsonResponse(deleteValues(path ++ list.dropRight(1))) } // case list Get _ => // TODO return RDF description }) diff --git a/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java b/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java index ad81742b..b5b35eda 100644 --- a/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java +++ b/bundles/io.github.linkedfactory.service/src/test/java/io/github/linkedfactory/core/kvin/KvinHttpTest.java @@ -51,6 +51,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -286,8 +287,8 @@ public void shouldThrowOnNon404FetchError() throws Exception { try { kvinHttp.fetch(URIs.createURI("http://example.org/item1"), null, null, 10).toList(); - Assert.fail("Expected RuntimeException for non-404 HTTP error"); - } catch (RuntimeException e) { + Assert.fail("Expected UncheckedIOException for non-404 HTTP error"); + } catch (UncheckedIOException e) { String message = e.getMessage() != null ? e.getMessage() : String.valueOf(e.getCause()); Assert.assertTrue(message.contains("500") || (e.getCause() != null && String.valueOf(e.getCause().getMessage()).contains("500"))); } diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala index 15019532..e510ab87 100644 --- a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala +++ b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/KvinServiceTest.scala @@ -148,9 +148,7 @@ class KvinServiceTest { val postResponse = kvinRest(toReq(postReq))().map(_.toResponse).openOr(null) assertEquals(200, postResponse.code) val postBody = responseBody(postResponse) - assertTrue(postBody.contains("\"success\":true")) - assertTrue(postBody.contains("\"code\":\"OK\"")) - assertTrue(postBody.contains("Values stored")) + assertTrue(postBody.trim.isEmpty) // support get request val getReq = new MockHttpServletRequest(baseUrl) { @@ -162,14 +160,6 @@ class KvinServiceTest { @Test def explicitEnvelopeResponsesUseJsonContentType(): Unit = { - val postReq = new MockHttpServletRequest(baseUrl) { - method = "POST" - body_=(TestData.item1, "application/json") - } - val postResponse = kvinRest(toReq(postReq))().map(_.toResponse).openOr(null) - assertEquals(200, postResponse.code) - assertTrue(responseContentType(postResponse).toLowerCase.contains("application/json")) - val invalidPostReq = new MockHttpServletRequest(baseUrl) { method = "POST" body_=("""{ "item" : [false, 1]} }""", "application/json") @@ -180,7 +170,7 @@ class KvinServiceTest { } @Test - def deleteValuesReturnsExplicitSuccessEnvelope(): Unit = { + def deleteValuesReturnsDeletedCountWithoutEnvelope(): Unit = { val postReq = new MockHttpServletRequest(baseUrl) { method = "POST" body_=(TestData.itemSet, "application/json") @@ -197,9 +187,9 @@ class KvinServiceTest { assertEquals(200, response.code) val body = responseBody(response) - assertTrue(body.contains("\"success\":true")) - assertTrue(body.contains("\"code\":\"OK\"")) assertTrue(body.contains("\"deleted\":")) + assertFalse(body.contains("\"success\":")) + assertFalse(body.contains("\"code\":")) } @Test @@ -215,6 +205,7 @@ class KvinServiceTest { val body = responseBody(response) assertTrue(body.contains("\"code\":\"INVALID_PAYLOAD\"")) assertTrue(body.contains("\"message\":")) + assertFalse(body.contains("\"status\":")) } @Test