From 3f99cea6ad62fd4e13fd859fae1ef54c2ce25e1f Mon Sep 17 00:00:00 2001 From: Willy Mehling Date: Mon, 27 Apr 2026 14:56:43 +0200 Subject: [PATCH 1/2] Add testing of enilink sparql error codes --- .../service/EnilinkSparqlContractTest.scala | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala new file mode 100644 index 0000000..0833ffd --- /dev/null +++ b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala @@ -0,0 +1,176 @@ +package io.github.linkedfactory.service + +import com.google.inject.Guice +import net.enilink.komma.core.{KommaModule, Statement, URIs} +import net.enilink.komma.model._ +import net.enilink.platform.lift.util.Globals +import net.enilink.platform.web.rest.SparqlRest +import net.liftweb.common.{Box, Full} +import net.liftweb.http.provider.servlet.HTTPRequestServlet +import net.liftweb.http.{BasicResponse, CurrentReq, InMemoryResponse, LiftResponse, OutputStreamResponse, Req} +import org.junit.Assert._ +import org.junit.{AfterClass, BeforeClass, Test} + +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import javax.servlet.http.HttpServletRequest + +/** + * Thin compatibility tests that verify the SPARQL error contract as consumed + * by linkedfactory-pod from the enilink dependency. + */ +object EnilinkSparqlContractTest { + var modelSet: IModelSet = _ + val sensorModel = MODELS.NAMESPACE_URI.appendFragment("sensor-data") + + @BeforeClass + def setup(): Unit = { + val module: KommaModule = ModelPlugin.createModelSetModule(classOf[ModelPlugin].getClassLoader) + val factory: IModelSetFactory = Guice.createInjector(new ModelSetModule(module)) + .getInstance(classOf[IModelSetFactory]) + + modelSet = factory.createModelSet(MODELS.NAMESPACE_URI.appendFragment("MemoryModelSet")) + Globals.contextModelSet.default.set(Full(modelSet)) + + val model = modelSet.createModel(sensorModel) + val em = model.getManager + em.add(new Statement( + URIs.createURI("http://example.org/sensors/temp-01"), + URIs.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + URIs.createURI("http://example.org/vocab/Sensor") + )) + } + + @AfterClass + def tearDown(): Unit = { + if (modelSet != null) { + modelSet.dispose() + modelSet = null + } + } +} + +class EnilinkSparqlContractTest { + private val sparqlService = new SparqlRest() { + override def apply(in: Req): () => Box[LiftResponse] = { + try { + Globals.contextModelSet.vend.map(_.getUnitOfWork.begin) + CurrentReq.doWith(in) { + super.apply(in) + } + } finally { + Globals.contextModelSet.vend.map(_.getUnitOfWork.end) + } + } + } + + private val baseUrl = "http://foo.com/sparql" + + private def toReq(httpRequest: HttpServletRequest): Req = { + Req(new HTTPRequestServlet(httpRequest, null), Nil, System.nanoTime) + } + + private def execute(req: HttpServletRequest): BasicResponse = { + sparqlService(toReq(req))().map(_.toResponse) + .openOr(throw new AssertionError("Expected SPARQL response but got empty result")) + } + + @Test + def missingModelReturns400(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List("query" -> "SELECT ?s WHERE { ?s ?p ?o }") + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MISSING_MODEL:") + } + + @Test + def missingQueryReturns400(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List("model" -> EnilinkSparqlContractTest.sensorModel.toString) + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MISSING_QUERY:") + } + + @Test + def unknownModelReturns404(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List( + "query" -> "SELECT ?s WHERE { ?s ?p ?o }", + "model" -> "http://example.org/models/does-not-exist" + ) + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 404, "MODEL_NOT_FOUND:") + } + + @Test + def unsupportedAcceptReturns406(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List( + "query" -> "SELECT ?s WHERE { ?s ?p ?o }", + "model" -> EnilinkSparqlContractTest.sensorModel.toString + ) + headers = Map("Accept" -> List("application/vnd.custom+xml")) + } + assertErrorContract(execute(req), 406, "UNSUPPORTED_ACCEPT:") + } + + @Test + def malformedSparqlReturns400(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "GET" + parameters = List( + "query" -> "SELECTT ?s WHERE { ?s ?p ?o }", + "model" -> EnilinkSparqlContractTest.sensorModel.toString + ) + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MALFORMED_QUERY:") + } + + @Test + def invalidQueryRequestReturns400(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "POST" + parameters = List( + "query" -> "SELECT ?s WHERE { ?s ?p ?o }", + "update" -> "INSERT DATA { }", + "model" -> EnilinkSparqlContractTest.sensorModel.toString + ) + contentType = "application/x-www-form-urlencoded" + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "INVALID_QUERY_REQUEST:") + } + + private def assertErrorContract(response: BasicResponse, expectedStatus: Int, expectedPrefix: String): Unit = { + assertNotNull("Response was null", response) + assertEquals("HTTP status", expectedStatus, response.code) + + val contentType = response.headers + .find(_._1.equalsIgnoreCase("Content-Type")) + .map(_._2) + .getOrElse("") + assertTrue(s"Expected text/plain content type but got: '$contentType'", contentType.toLowerCase.startsWith("text/plain")) + + val body = responseBody(response) + assertTrue(s"Expected prefix '$expectedPrefix' but got: $body", body.startsWith(expectedPrefix)) + } + + private def responseBody(response: BasicResponse): String = response match { + case r: InMemoryResponse => new String(r.data, StandardCharsets.UTF_8) + case r: OutputStreamResponse => + val out = new ByteArrayOutputStream() + r.out(out) + out.toString(StandardCharsets.UTF_8.name) + case _ => + fail("Unexpected response type") + "" + } +} \ No newline at end of file From 4aaef045b444f26782453a7c6542c9f8a9298ffb Mon Sep 17 00:00:00 2001 From: Willy Mehling Date: Thu, 30 Apr 2026 14:19:13 +0200 Subject: [PATCH 2/2] Add tests for handling empty SPARQL query and update parameters / Adapt to enilink SPARQL update Co-authored-by: Copilot --- .../service/EnilinkSparqlContractTest.scala | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala index 0833ffd..cc96c53 100644 --- a/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala +++ b/bundles/io.github.linkedfactory.service/src/test/scala/io/github/linkedfactory/service/EnilinkSparqlContractTest.scala @@ -95,6 +95,44 @@ class EnilinkSparqlContractTest { assertErrorContract(execute(req), 400, "MISSING_QUERY:") } + @Test + def emptySparqlQueryBodyReturnsMissingQuery(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "POST" + parameters = List("model" -> EnilinkSparqlContractTest.sensorModel.toString) + contentType = "application/sparql-query" + body = Array.emptyByteArray + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MISSING_QUERY: The request body is empty. Expected a SPARQL query.") + } + + @Test + def emptyUpdateParamReturnsMissingUpdate(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "POST" + parameters = List( + "update" -> "", + "model" -> EnilinkSparqlContractTest.sensorModel.toString + ) + contentType = "application/x-www-form-urlencoded" + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MISSING_UPDATE:") + } + + @Test + def emptySparqlUpdateBodyReturnsMissingUpdate(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "POST" + parameters = List("model" -> EnilinkSparqlContractTest.sensorModel.toString) + contentType = "application/sparql-update" + body = Array.emptyByteArray + headers = Map("Accept" -> List("application/sparql-results+json")) + } + assertErrorContract(execute(req), 400, "MISSING_UPDATE: The request body is empty. Expected a SPARQL update.") + } + @Test def unknownModelReturns404(): Unit = { val req = new MockHttpServletRequest(baseUrl) { @@ -135,7 +173,7 @@ class EnilinkSparqlContractTest { } @Test - def invalidQueryRequestReturns400(): Unit = { + def conflictingQueryAndUpdateReturns400(): Unit = { val req = new MockHttpServletRequest(baseUrl) { method = "POST" parameters = List( @@ -146,6 +184,17 @@ class EnilinkSparqlContractTest { contentType = "application/x-www-form-urlencoded" headers = Map("Accept" -> List("application/sparql-results+json")) } + assertErrorContract(execute(req), 400, "CONFLICTING_PARAMETERS:") + } + + @Test + def unrecognizedPostShapeReturnsInvalidQueryRequest(): Unit = { + val req = new MockHttpServletRequest(baseUrl) { + method = "POST" + parameters = List("model" -> EnilinkSparqlContractTest.sensorModel.toString) + contentType = "application/x-www-form-urlencoded" + headers = Map("Accept" -> List("application/sparql-results+json")) + } assertErrorContract(execute(req), 400, "INVALID_QUERY_REQUEST:") }