This is an automated email from the ASF dual-hosted git repository. sblackmon pushed a commit to branch feat-5-graphdb in repository https://gitbox.apache.org/repos/asf/streams-activitypub.git
commit 1df84c3d698cd800a7606c3b40ffe626628fe465 Author: Steve Blackmon <sblack...@apache.org> AuthorDate: Sun Feb 18 20:52:17 2024 -0600 feat: integrate with fuseki (graph database+api) #5 resolves #5 : **Add a graph module and a Webfinger implementation using jena graphdb binding** - adds a real implementation of webfinger protocol which responds only to requests for entities that exist in the configured fuseki triple-store - tests capabilities at the WebfingerApi Graph implementation level, and within the packaged webapp - apache/streams-activitypub docker image now runs streams-activitypub-webapp and jena-fuseki2-webapp Signed-off-by: Steve Blackmon <sblack...@apache.org> --- pom.xml | 48 +++++++- streams-activitypub-api/pom.xml | 5 +- .../api/test/WebfingerApiTestImpl.scala | 15 +++ streams-activitypub-dist/Dockerfile | 5 +- streams-activitypub-dist/pom.xml | 57 +++++++++- .../pom.xml | 103 +++++++++++++---- .../graph/config/WebfingerGraphImplConfig.json | 20 ++++ .../queries/webfingerAskByResource.sparql | 4 + .../queries/webfingerGetByResource.sparql | 9 ++ .../src/main/resources/reference.conf | 3 + .../graph/impl/WebfingerGraphImpl.scala | 92 +++++++++++++++ .../ActivityPubGraphTestSuiteExtensionConfig.json | 25 ++++ .../src/test/resources/application.conf | 9 ++ .../graph/test/ActivityPubGraphTestSuite.scala | 29 +++++ .../test/ActivityPubGraphTestSuiteExtension.scala | 82 ++++++++++++++ .../cases/GraphDatabaseServerAvailableTest.scala | 40 +++++++ .../graph/test/cases/WebfingerGraphImplTest.scala | 126 +++++++++++++++++++++ streams-activitypub-remote/pom.xml | 20 ++-- streams-activitypub-servlets/pom.xml | 7 +- .../activitypub/servlets/WebfingerServlet.scala | 118 ++++++++----------- .../pom.xml | 53 +++------ .../util/AcctPrefixResourceToResourceURISwap.scala | 39 +++++++ .../test/scala/AcctPrefixResourceURISwapTest.scala | 34 ++++++ streams-activitypub-webapp/pom.xml | 60 ++++++++++ .../src/test/resources/application.conf | 9 ++ .../webapp/test/ActivityPubWebappTestSuite.scala | 11 +- .../test/ActivityPubWebappTestSuiteExtension.scala | 22 +--- .../webapp/test/cases/RootServletTest.scala | 17 ++- .../test/cases/ServletRegistrationTest.scala | 27 +++-- ...eTest.scala => WebappServerAvailableTest.scala} | 24 ++-- .../webapp/test/cases/WebfingerServletTest.scala | 41 +++++-- 31 files changed, 937 insertions(+), 217 deletions(-) diff --git a/pom.xml b/pom.xml index 5940755..6e4325c 100644 --- a/pom.xml +++ b/pom.xml @@ -365,6 +365,8 @@ <jackson.version>2.16.0</jackson.version> <jakarta.version>6.0.0</jakarta.version> <jakarta-ws.version>3.1.0</jakarta-ws.version> + <jena.version>5.0.0-rc1</jena.version> + <jetty.version>12.0.6</jetty.version> <joda-convert.version>1.8.1</joda-convert.version> <joda-time.version>2.9.9</joda-time.version> <json-path.version>2.8.0</json-path.version> @@ -417,14 +419,22 @@ <module>streams-activitypub-actors</module> <module>streams-activitypub-api</module> <module>streams-activitypub-dist</module> + <module>streams-activitypub-graph</module> <module>streams-activitypub-remote</module> <module>streams-activitypub-servlets</module> + <module>streams-activitypub-utils</module> <module>streams-activitypub-webapp</module> </modules> <packaging>pom</packaging> <build> + <testResources> + <testResource> + <directory>${project.basedir}</directory> + <filtering>true</filtering> + </testResource> + </testResources> <extensions> <extension> <groupId>org.apache.maven.wagon</groupId> @@ -553,19 +563,34 @@ <artifactId>jsonschema2pojo-maven-plugin</artifactId> <version>${jsonschema2pojo.version}</version> <configuration> + <addCompileSourceRoot>true</addCompileSourceRoot> <annotationStyle>none</annotationStyle> <customAnnotator>org.apache.streams.plugins.JuneauPojoAnnotator</customAnnotator> <generateBuilders>true</generateBuilders> <includeGeneratedAnnotation>false</includeGeneratedAnnotation> - <sourceDirectory>${project.basedir}/src/main/jsonschema</sourceDirectory> - <outputDirectory>${project.build.directory}/generated-sources/pojo</outputDirectory> </configuration> <executions> <execution> - <phase>generate-sources</phase> + <id>generate-sources</id> + <phase>none</phase> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <sourceDirectory>${project.basedir}/src/main/jsonschema</sourceDirectory> + <outputDirectory>${project.build.directory}/generated-sources/pojo</outputDirectory> + </configuration> + </execution> + <execution> + <id>generate-test-sources</id> + <phase>none</phase> <goals> <goal>generate</goal> </goals> + <configuration> + <sourceDirectory>${project.basedir}/src/test/jsonschema</sourceDirectory> + <outputDirectory>${project.build.directory}/generated-test-sources/pojo</outputDirectory> + </configuration> </execution> </executions> <dependencies> @@ -628,6 +653,18 @@ </sources> </configuration> </execution> + <execution> + <id>add-generated-test-sources</id> + <phase>generate-test-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>target/generated-test-sources/pojo</source> + </sources> + </configuration> + </execution> </executions> </plugin> <plugin> @@ -1467,6 +1504,11 @@ <argLine>--add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED</argLine> <skipTests>${skipTests}</skipTests> <skipExec>${skipExec}</skipExec> + <properties> + <configurationParameters> + junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation + </configurationParameters> + </properties> </configuration> </plugin> </plugins> diff --git a/streams-activitypub-api/pom.xml b/streams-activitypub-api/pom.xml index a759d5f..3ffca62 100755 --- a/streams-activitypub-api/pom.xml +++ b/streams-activitypub-api/pom.xml @@ -81,13 +81,10 @@ <plugin> <groupId>org.jsonschema2pojo</groupId> <artifactId>jsonschema2pojo-maven-plugin</artifactId> - <version>1.2.1</version> + <version>${jsonschema2pojo.version}</version> <executions> <execution> <id>generate-sources</id> - <goals> - <goal>generate</goal> - </goals> <phase>generate-sources</phase> </execution> </executions> diff --git a/streams-activitypub-api/src/test/scala/org/apache/streams/activitypub/api/test/WebfingerApiTestImpl.scala b/streams-activitypub-api/src/test/scala/org/apache/streams/activitypub/api/test/WebfingerApiTestImpl.scala new file mode 100755 index 0000000..a01db5a --- /dev/null +++ b/streams-activitypub-api/src/test/scala/org/apache/streams/activitypub/api/test/WebfingerApiTestImpl.scala @@ -0,0 +1,15 @@ +package org.apache.streams.activitypub.api.test + +import org.apache.streams.activitypub.api.WebfingerApi +import org.apache.streams.activitypub.api.pojo.WebfingerQueryRequest +import org.apache.streams.activitypub.api.pojo.WebfingerQueryResponse + +class WebfingerApiTestImpl extends WebfingerApi { + + def webfingerQuery(request: WebfingerQueryRequest): WebfingerQueryResponse = { + + new WebfingerQueryResponse().withSubject(request.getResource) + + } + +} diff --git a/streams-activitypub-dist/Dockerfile b/streams-activitypub-dist/Dockerfile index 28f42c0..8534e27 100644 --- a/streams-activitypub-dist/Dockerfile +++ b/streams-activitypub-dist/Dockerfile @@ -2,4 +2,7 @@ FROM tomcat:jre17-temurin MAINTAINER d...@streams.apache.org LABEL Description="apache-streams-activitypub-dist" WORKDIR / -ADD target/exploded /usr/local/tomcat/webapps/ROOT +RUN mkdir -p /etc/fuseki +ADD target/lib/* /usr/local/tomcat/lib/ +ADD target/webapps/streams-activitypub.war /usr/local/tomcat/webapps/ROOT.war +ADD target/webapps/jena-fuseki.war /usr/local/tomcat/webapps/fuseki.war diff --git a/streams-activitypub-dist/pom.xml b/streams-activitypub-dist/pom.xml index 3616898..a2274ef 100755 --- a/streams-activitypub-dist/pom.xml +++ b/streams-activitypub-dist/pom.xml @@ -42,25 +42,74 @@ under the License. </dependencies> <build> + <resources> + <resource> + <directory>src/main/resources</directory> + </resource> + </resources> + <testResources> + <testResource> + <directory>src/test/resources</directory> + </testResource> + </testResources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> - <id>unpack</id> - <phase>prepare-package</phase> + <id>copy</id> + <phase>package</phase> <goals> - <goal>unpack</goal> + <goal>copy</goal> </goals> <configuration> - <outputDirectory>${project.build.directory}/exploded</outputDirectory> <artifactItems> <artifactItem> <groupId>org.apache.streams.activitypub</groupId> <artifactId>streams-activitypub-webapp</artifactId> <version>${project.version}</version> <type>war</type> + <outputDirectory>${project.build.directory}/webapps</outputDirectory> + <overWrite>true</overWrite> + <destFileName>streams-activitypub.war</destFileName> + </artifactItem> + <artifactItem> + <groupId>org.apache.jena</groupId> + <artifactId>jena-fuseki-war</artifactId> + <version>${jena.version}</version> + <type>war</type> + <outputDirectory>${project.build.directory}/webapps</outputDirectory> + <overWrite>true</overWrite> + <destFileName>jena-fuseki.war</destFileName> + </artifactItem> + <!-- the fuseki war excludes some needed dependencies --> + <artifactItem> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-http</artifactId> + <version>${jetty.version}</version> + <type>jar</type> + <outputDirectory>${project.build.directory}/lib</outputDirectory> + <overWrite>true</overWrite> + <destFileName>jetty-http.jar</destFileName> + </artifactItem> + <artifactItem> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-util</artifactId> + <version>${jetty.version}</version> + <type>jar</type> + <outputDirectory>${project.build.directory}/lib</outputDirectory> + <overWrite>true</overWrite> + <destFileName>jetty-util.jar</destFileName> + </artifactItem> + <artifactItem> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j.version}</version> + <type>jar</type> + <outputDirectory>${project.build.directory}/lib</outputDirectory> + <overWrite>true</overWrite> + <destFileName>slf4j-api.jar</destFileName> </artifactItem> </artifactItems> </configuration> diff --git a/streams-activitypub-api/pom.xml b/streams-activitypub-graph/pom.xml old mode 100755 new mode 100644 similarity index 52% copy from streams-activitypub-api/pom.xml copy to streams-activitypub-graph/pom.xml index a759d5f..f340313 --- a/streams-activitypub-api/pom.xml +++ b/streams-activitypub-graph/pom.xml @@ -28,26 +28,52 @@ <relativePath>../pom.xml</relativePath> </parent> - <artifactId>streams-activitypub-api</artifactId> - <description>Apache Streams ActivityPub Server APIs</description> + <artifactId>streams-activitypub-graph</artifactId> + <description>Apache Streams Graph</description> <name>${project.artifactId}</name> <packaging>jar</packaging> <dependencies> <dependency> - <groupId>org.apache.streams</groupId> - <artifactId>streams-config</artifactId> + <groupId>org.apache.streams.activitypub</groupId> + <artifactId>streams-activitypub-api</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.streams.activitypub</groupId> + <artifactId>streams-activitypub-utils</artifactId> + <version>${project.version}</version> + </dependency> + + <!-- streams-graph only dependencies --> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-arq</artifactId> + <version>${jena.version}</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-rdfconnection</artifactId> + <version>${jena.version}</version> + <type>jar</type> + </dependency> - <!-- streams-api only dependencies --> + <!-- streams-graph test dependencies --> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-fuseki-main</artifactId> + <version>${jena.version}</version> + <type>jar</type> + <scope>test</scope> + </dependency> <dependency> <groupId>org.apache.juneau</groupId> - <artifactId>juneau-rest-common</artifactId> + <artifactId>juneau-rest-client</artifactId> <version>${juneau.version}</version> + <scope>test</scope> </dependency> - </dependencies> <build> <sourceDirectory>src/main/scala</sourceDirectory> @@ -64,37 +90,72 @@ </testResources> <plugins> <plugin> - <groupId>net.alchim31.maven</groupId> - <artifactId>scala-maven-plugin</artifactId> - <configuration> - <source>${project.basedir}/src/main/scala</source> - <scalaVersion>${scala.version}</scalaVersion> - </configuration> + <groupId>org.jsonschema2pojo</groupId> + <artifactId>jsonschema2pojo-maven-plugin</artifactId> + <version>${jsonschema2pojo.version}</version> + <executions> + <execution> + <id>generate-sources</id> + <phase>generate-sources</phase> + </execution> + <execution> + <id>generate-test-sources</id> + <phase>generate-test-sources</phase> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> <executions> <execution> <goals> - <goal>compile</goal> + <goal>jar</goal> + <goal>test-jar</goal> </goals> </execution> </executions> </plugin> <plugin> - <groupId>org.jsonschema2pojo</groupId> - <artifactId>jsonschema2pojo-maven-plugin</artifactId> - <version>1.2.1</version> + <groupId>net.alchim31.maven</groupId> + <artifactId>scala-maven-plugin</artifactId> <executions> <execution> - <id>generate-sources</id> + <id>scala-compile</id> + <phase>compile</phase> <goals> - <goal>generate</goal> + <goal>compile</goal> </goals> - <phase>generate-sources</phase> </execution> </executions> + <configuration> + <addScalacArgs>--explain</addScalacArgs> + <scalaVersion>${scala.version}</scalaVersion> + </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>copy-resources</id> + <phase>process-test-resources</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${project.build.testOutputDirectory}</outputDirectory> + <resources> + <resource> + <directory>..</directory> + <includes> + <include>*.ttl</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + </executions> </plugin> </plugins> </build> diff --git a/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json new file mode 100755 index 0000000..51250f8 --- /dev/null +++ b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "id": "#", + "type": "object", + "javaType": "org.apache.streams.activitypub.graph.config.WebfingerGraphImplConfig", + "javaInterfaces": [ + "java.io.Serializable" + ], + "properties": { + "fusekiEndpointURI": { + "type": "string", + "format": "uri", + "required": true + }, + "defaultDatasetId": { + "type": "string", + "required": false + } + } +} diff --git a/streams-activitypub-graph/src/main/resources/queries/webfingerAskByResource.sparql b/streams-activitypub-graph/src/main/resources/queries/webfingerAskByResource.sparql new file mode 100644 index 0000000..72c64dc --- /dev/null +++ b/streams-activitypub-graph/src/main/resources/queries/webfingerAskByResource.sparql @@ -0,0 +1,4 @@ +BASE <> +ASK { + ?resourceParam ?p ?o +} diff --git a/streams-activitypub-graph/src/main/resources/queries/webfingerGetByResource.sparql b/streams-activitypub-graph/src/main/resources/queries/webfingerGetByResource.sparql new file mode 100644 index 0000000..75d98da --- /dev/null +++ b/streams-activitypub-graph/src/main/resources/queries/webfingerGetByResource.sparql @@ -0,0 +1,9 @@ +BASE <> +PREFIX r: <classpath://WebfingerQueryResponse.json> +CONSTRUCT { + <r:response> <#subject> ?resource . + <r:response> <#alias> ?alias +} WHERE { + BIND( ?resourceParam as ?resource ) + +} diff --git a/streams-activitypub-graph/src/main/resources/reference.conf b/streams-activitypub-graph/src/main/resources/reference.conf new file mode 100644 index 0000000..b60d46c --- /dev/null +++ b/streams-activitypub-graph/src/main/resources/reference.conf @@ -0,0 +1,3 @@ +WebfingerGraphImpl = { + fusekiEndpointURI = "http://localhost:8080/fuseki/" +} diff --git a/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala new file mode 100644 index 0000000..6237e16 --- /dev/null +++ b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala @@ -0,0 +1,92 @@ +package org.apache.streams.activitypub.graph.impl + +import org.apache.http.client.utils.URIBuilder +import org.apache.jena.query.ParameterizedSparqlString +import org.apache.jena.rdf.model.Model +import org.apache.jena.riot.system.PrefixMapStd +import org.apache.jena.sparql.exec.http.QueryExecutionHTTP +import org.apache.jena.sparql.exec.http.QueryExecutionHTTPBuilder +import org.apache.streams.activitypub.api.WebfingerApi +import org.apache.streams.activitypub.api.pojo.WebfingerQueryRequest +import org.apache.streams.activitypub.api.pojo.WebfingerQueryResponse +import org.apache.streams.activitypub.graph.config.WebfingerGraphImplConfig +import org.apache.streams.activitypub.util.AcctPrefixResourceToResourceURISwap +import org.apache.streams.config.ComponentConfigurator + +import java.net.URI +import scala.io.Source; + +object WebfingerGraphImpl { + + private final val configurator: ComponentConfigurator[WebfingerGraphImplConfig] = new ComponentConfigurator(classOf[WebfingerGraphImplConfig]) + final val config: WebfingerGraphImplConfig = configurator.detectConfiguration() + final val DEFAULT: WebfingerGraphImpl = new WebfingerGraphImpl(config) + +} + +class WebfingerGraphImpl(config: WebfingerGraphImplConfig) extends WebfingerApi { + + private def serverUri = new URIBuilder(config.getFusekiEndpointURI).build() + + private def datasetQueryUri = new URIBuilder(serverUri).setPath(s"${config.getDefaultDatasetId}/query").build() + + final val sparqlBuilder: QueryExecutionHTTPBuilder = QueryExecutionHTTP.service(datasetQueryUri.toString).postQuery() + + val prefixMap = new PrefixMapStd() + + /** + * Query the dataset for the requested resource. + * @param request + * @return + */ + override def webfingerQuery(request: WebfingerQueryRequest): WebfingerQueryResponse = { + + val resourceParamURI: URI = request.getResource match { + case s"acct:$x" => AcctPrefixResourceToResourceURISwap.doUnswap(x) + case _ => new URI(request.getResource) + } + + val askQueryBody: String = Source.fromResource("queries/webfingerAskByResource.sparql").getLines.mkString + val askQuery: ParameterizedSparqlString = new ParameterizedSparqlString(askQueryBody) + askQuery.setIri("resourceParam", resourceParamURI.toString) + val askExecution: QueryExecutionHTTP = sparqlBuilder.query(askQuery.asQuery()).build() + val askResult = askExecution.execAsk() + if( !askResult ) throw new Exception("Requested resource not found in dataset.") + + val constructQueryBody: String = Source.fromResource("queries/webfingerGetByResource.sparql").getLines.mkString + val constructQuery: ParameterizedSparqlString = new ParameterizedSparqlString(constructQueryBody) + constructQuery.setIri("resourceParam", resourceParamURI.toString) + val constructExecution: QueryExecutionHTTP = sparqlBuilder.query(constructQuery.asQuery()).build() + val model: Model = constructExecution.execConstruct() + + // val result = writer.write(System.out, constructed, prefixMap, RDFFormat.JSONLD_FRAME_PRETTY, context); + if( checkModelContainsSubject( resourceParamURI, model ) ) { + generateResponse(request, model, resourceParamURI) + } else throw new Exception("Error generating response.") + } + + /** + * Check if the model returned from the query contains the requested resource + * and necessary details to generate a valid response. + * @param request + * @param model + * @return + */ + def checkModelContainsSubject(resourceParamURI: URI, model: Model): Boolean = { + model.containsResource(model.getResource(resourceParamURI.toString)) + } + + /** + * Translate the model into a webfinger response object. + * @param request + * @param model + * @return + */ + def generateResponse(request: WebfingerQueryRequest, model: Model, resourceParamURI: URI): WebfingerQueryResponse = { + val subject = model.getResource(resourceParamURI.toString) + val result = new WebfingerQueryResponse() + .withSubject(subject.getURI) + result + } + +} diff --git a/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json b/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json new file mode 100755 index 0000000..a1e2606 --- /dev/null +++ b/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "id": "#", + "type": "object", + "javaType": "org.apache.streams.activitypub.graph.test.config.ActivityPubGraphTestSuiteExtensionConfig", + "javaInterfaces": [ + "java.io.Serializable" + ], + "properties": { + "fusekiEndpointURI": { + "type": "string", + "format": "uri", + "required": true + }, + "testDatasetResource": { + "type": "string", + "format": "uri", + "required": true + }, + "testDatasetId": { + "type": "string", + "required": true + } + } +} diff --git a/streams-activitypub-graph/src/test/resources/application.conf b/streams-activitypub-graph/src/test/resources/application.conf new file mode 100644 index 0000000..fde8a62 --- /dev/null +++ b/streams-activitypub-graph/src/test/resources/application.conf @@ -0,0 +1,9 @@ +ActivityPubGraphTestSuiteExtensionConfig = { + fusekiEndpointURI = "http://localhost:13330/" + testDatasetResource = "doap.ttl" + testDatasetId = "doap.ttl" +} +WebfingerGraphImplConfig = { + fusekiEndpointURI = "http://localhost:13330/" + defaultDatasetId = "doap.ttl" +} diff --git a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala new file mode 100755 index 0000000..e8b32c5 --- /dev/null +++ b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala @@ -0,0 +1,29 @@ +package org.apache.streams.activitypub.graph.test + +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.platform.suite.api.SelectClasses +import org.junit.platform.suite.api.Suite +import org.junit.platform.suite.api.SuiteDisplayName; + +/** + * ActivityPubServletsTestSuite + */ + +object ActivityPubGraphTestSuite { + + @RegisterExtension + given fuseki: ActivityPubGraphTestSuiteExtension = new ActivityPubGraphTestSuiteExtension() + +} + +@Suite +@SuiteDisplayName("ActivityPub Graph Integration Tests") +@Order(Order.DEFAULT) +@SelectClasses(Array( + classOf[org.apache.streams.activitypub.graph.test.cases.GraphDatabaseServerAvailableTest], + classOf[org.apache.streams.activitypub.graph.test.cases.WebfingerGraphImplTest] +)) +class ActivityPubGraphTestSuite { + +} diff --git a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala new file mode 100644 index 0000000..553c729 --- /dev/null +++ b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala @@ -0,0 +1,82 @@ +package org.apache.streams.activitypub.graph.test + +import org.apache.http.client.utils.URIBuilder +import org.apache.http.impl.NoConnectionReuseStrategy +import org.apache.jena.fuseki.main.FusekiServer +import org.apache.jena.query.Dataset +import org.apache.jena.query.DatasetFactory +import org.apache.jena.riot.RDFDataMgr +import org.apache.juneau.rest.client.RestClient +import org.apache.streams.activitypub.graph.config.WebfingerGraphImplConfig +import org.apache.streams.activitypub.graph.impl.WebfingerGraphImpl +import org.apache.streams.activitypub.graph.test.config.ActivityPubGraphTestSuiteExtensionConfig +import org.apache.streams.config.ComponentConfigurator +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.net.URL + +object ActivityPubGraphTestSuiteExtension { + private final val configurator: ComponentConfigurator[ActivityPubGraphTestSuiteExtensionConfig] = new ComponentConfigurator(classOf[ActivityPubGraphTestSuiteExtensionConfig]) + final val config: ActivityPubGraphTestSuiteExtensionConfig = configurator.detectConfiguration() +} + +class ActivityPubGraphTestSuiteExtension extends ParameterResolver with BeforeAllCallback with AfterAllCallback { + + import ActivityPubGraphTestSuiteExtension.config + + private final val LOGGER: Logger = LoggerFactory.getLogger(classOf[ActivityPubGraphTestSuiteExtension]) + + private val dataset : Dataset = DatasetFactory.create() + private val serverBuilder : FusekiServer.Builder = FusekiServer.create() + + RDFDataMgr.read(dataset, config.getTestDatasetResource.toString) + + private val server: FusekiServer = serverBuilder + .add(config.getTestDatasetId, dataset) + .enablePing(true) + .loopback(true) + .port(config.getFusekiEndpointURI.getPort) + .verbose(true) + .build(); + server.start(); + + val serverUrl = new URL(server.serverURL()) + val datasetUrl = new URL(server.datasetURL(config.getTestDatasetId)) + val serverUrlBuilder = new URIBuilder(serverUrl.toURI) + val datasetUrlBuilder = new URIBuilder(datasetUrl.toURI) + + def restClientBuilder: RestClient.Builder = RestClient.create() + .connectionReuseStrategy(new NoConnectionReuseStrategy()) + .debug() + .disableAutomaticRetries() + .disableCookieManagement() + .disableRedirectHandling() + .maxConnTotal(10) + .rootUrl(serverUrl) + + def restClient: RestClient = restClientBuilder + .pooled() + .build(); + + override def beforeAll(context : ExtensionContext) : Unit = { + LOGGER.info("beforeAll()"); + } + + override def afterAll(extensionContext: ExtensionContext): Unit = { + LOGGER.info("afterAll()") + } + + override def supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean = { + parameterContext.getParameter.getType.equals(classOf[ActivityPubGraphTestSuiteExtension]) + } + + override def resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): AnyRef = { + this + } +} diff --git a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/GraphDatabaseServerAvailableTest.scala b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/GraphDatabaseServerAvailableTest.scala new file mode 100755 index 0000000..3e8eb79 --- /dev/null +++ b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/GraphDatabaseServerAvailableTest.scala @@ -0,0 +1,40 @@ +package org.apache.streams.activitypub.graph.test.cases + +import org.apache.http.HttpStatus +import org.apache.streams.activitypub.graph.test.ActivityPubGraphTestSuiteExtension +import org.awaitility.Awaitility.await +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.slf4j.LoggerFactory + +import scala.concurrent.duration.MINUTES +import scala.concurrent.duration.SECONDS +import scala.util.Try; + +/** + * ActivityPubServletsTestSuite + */ +@ExtendWith(Array(classOf[ActivityPubGraphTestSuiteExtension])) +class GraphDatabaseServerAvailableTest { + + private final val LOGGER = LoggerFactory.getLogger(classOf[GraphDatabaseServerAvailableTest]); + + @Test + @DisplayName("Verify Fuseki Online") + @Order(0) + def testGraphDatabaseServerOnline(using helper : ActivityPubGraphTestSuiteExtension): Unit = { + val testAttempt = Try { + await atMost(1, MINUTES) pollInterval(1, SECONDS) until { + () => Try { + val request = helper.restClient.get("/$/ping") + val response = request.run() + response.getStatusCode == HttpStatus.SC_OK + }.isSuccess + } + } + Assertions.assertTrue(testAttempt.isSuccess, "Graph Database Server is available") + } +} diff --git a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala new file mode 100755 index 0000000..da05f3a --- /dev/null +++ b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala @@ -0,0 +1,126 @@ +package org.apache.streams.activitypub.graph.test.cases + +import org.apache.http.HttpStatus +import org.apache.http.client.utils.URIBuilder +import org.apache.http.entity.ContentType +import org.apache.streams.activitypub.api.pojo.WebfingerQueryRequest +import org.apache.streams.activitypub.api.pojo.WebfingerQueryResponse +import org.apache.streams.activitypub.graph.impl.WebfingerGraphImpl +import org.apache.streams.activitypub.graph.test.ActivityPubGraphTestSuite +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory + +import java.nio.charset.Charset +import scala.util.Try; + +class WebfingerGraphImplTest { + + private final val LOGGER = LoggerFactory.getLogger(classOf[GraphDatabaseServerAvailableTest]); + + val primaryTestResourceURI = "https://mastodon.social/users/steveblackmon" + + /** + * Test webfinger lookup of a known resource, using abbreviated 'acct:' prefix form + * @throws java.lang.Exception + */ + @Test + @DisplayName("WebfingerQuery For Known Abbreviated Resource") + @Order(1) + def testWebfingerQueryForKnownAbbreviatedResource() : Unit = { + val knownAbbreviatedResource = "acct:steveblackmon@mastodon.social" + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource(knownAbbreviatedResource) + val response: WebfingerQueryResponse = testWebfingerGraphImpl.webfingerQuery(request) + Assertions.assertNotNull(response) + Assertions.assertTrue(response.getSubject.equals(primaryTestResourceURI)) + } + + /** + * Test webfinger lookup of a known resource, using official indexed URI form + * + * @throws java.lang.Exception + */ + @Test + @DisplayName("WebfingerQuery For Known URI Resource") + @Order(1) + def testWebfingerQueryForKnownUriResource(): Unit = { + val knownPrimaryResourceUri = primaryTestResourceURI + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource(knownPrimaryResourceUri) + val response: WebfingerQueryResponse = testWebfingerGraphImpl.webfingerQuery(request) + Assertions.assertNotNull(response) + Assertions.assertTrue(response.getSubject.equals(primaryTestResourceURI)) + } + + /** + * Test webfinger lookup of a known resource, using common alternative non-indexed URI form + * This test requires the ask and construct queries to be able to match the alias form, + * without the alias URI being in the graph. + * @throws java.lang.Exception + */ + @Disabled("alias inference not yet implemented") + @Test + @DisplayName("WebfingerQuery For Known Alias") + @Order(1) + def testWebfingerQueryForKnownAlias(): Unit = { + val knownAliasUri = "https://mastodon.social/@steveblackmon" + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource(knownAliasUri) + val response: WebfingerQueryResponse = testWebfingerGraphImpl.webfingerQuery(request) + Assertions.assertNotNull(response) + Assertions.assertTrue(response.getSubject.equals(primaryTestResourceURI)) + } + + /** + * Test that webfinger lookup of a non-present resource fails with exception + * @throws java.lang.Exception + */ + @Test + @DisplayName("WebfingerQuery For Missing URI Resource") + @Order(2) + def testWebfingerQueryForMissingUriResource(): Unit = { + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource("https://mastodon.social/users/joeschmo") + val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request)) + Assertions.assertTrue(attempt.isFailure) + } + + /** + * Test that webfinger lookup of a non-present abbreviated form resource fails with exception + * @throws java.lang.Exception + */ + @Test + @DisplayName("WebfingerQuery For Missing Abbreviated Resource") + @Order(2) + def testWebfingerQueryForMissingAbbreviatedResource(): Unit = { + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource("acct:joeschmo@mastodon.social") + val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request)) + Assertions.assertTrue(attempt.isFailure) + } + + /** + * Test that webfinger lookup with a malformed abbreviated prefix resource fails with exception + * + * @throws java.lang.Exception + */ + @Test + @DisplayName("WebfingerQuery For Atypical Prefixed Resource") + @Order(2) + def testWebfingerQueryForAtypicalPrefixedResource(): Unit = { + val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT + val request = new WebfingerQueryRequest() + request.setResource("account:steveblackmon@mastodon.social") + val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request)) + Assertions.assertTrue(attempt.isFailure) + } +} diff --git a/streams-activitypub-remote/pom.xml b/streams-activitypub-remote/pom.xml index 19ffa4f..0b876f7 100755 --- a/streams-activitypub-remote/pom.xml +++ b/streams-activitypub-remote/pom.xml @@ -58,16 +58,16 @@ </dependencies> <build> <sourceDirectory>src/main/scala</sourceDirectory> -<!-- <resources>--> -<!-- <resource>--> -<!-- <directory>src/main/resources</directory>--> -<!-- </resource>--> -<!-- </resources>--> -<!-- <testResources>--> -<!-- <testResource>--> -<!-- <directory>src/test/resources</directory>--> -<!-- </testResource>--> -<!-- </testResources>--> + <resources> + <resource> + <directory>src/main/resources</directory> + </resource> + </resources> + <testResources> + <testResource> + <directory>src/test/resources</directory> + </testResource> + </testResources> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> diff --git a/streams-activitypub-servlets/pom.xml b/streams-activitypub-servlets/pom.xml index 1d67334..b64edf5 100755 --- a/streams-activitypub-servlets/pom.xml +++ b/streams-activitypub-servlets/pom.xml @@ -29,7 +29,7 @@ <artifactId>streams-activitypub-servlets</artifactId> <name>${project.artifactId}</name> - <description>Apache Streams Resource Classes (Servlets)</description> + <description>Apache Streams Servlet Classes</description> <packaging>jar</packaging> @@ -49,6 +49,11 @@ <artifactId>streams-activitypub-remote</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.streams.activitypub</groupId> + <artifactId>streams-activitypub-graph</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.juneau</groupId> <artifactId>juneau-rest-server</artifactId> diff --git a/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala b/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala index cf7fb7d..a38277f 100755 --- a/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala +++ b/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala @@ -1,33 +1,21 @@ package org.apache.streams.activitypub.servlets; import jakarta.servlet.ServletConfig -import org.apache.commons.collections4.CollectionUtils -import org.apache.commons.lang3.StringUtils +import jakarta.servlet.http.HttpServletResponse +import jakarta.ws.rs.core.MediaType import org.apache.juneau.html.HtmlSerializer -import org.apache.juneau.http.annotation.Query import org.apache.juneau.json.JsonSerializer import org.apache.juneau.rest.RestRequest import org.apache.juneau.rest.RestResponse import org.apache.juneau.rest.annotation.Rest import org.apache.juneau.rest.annotation.RestGet -import org.apache.juneau.rest.matcher.RestMatcher import org.apache.juneau.rest.servlet.BasicRestServlet -import jakarta.ws.rs.core.MediaType -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.apache.http.client.utils.URIBuilder -import org.apache.juneau.rest.httppart.RequestQueryParams -import org.apache.streams.activitypub.api.pojo.Link -import org.apache.streams.activitypub.api.pojo.Properties +import org.apache.streams.activitypub.api.WebfingerApi import org.apache.streams.activitypub.api.pojo.WebfingerQueryRequest import org.apache.streams.activitypub.api.pojo.WebfingerQueryResponse +import org.apache.streams.activitypub.graph.impl.WebfingerGraphImpl import org.apache.streams.activitypub.remote.WebfingerRest - -import java.util.ArrayList -import java.net.URI -import java.util.Optional -import scala.collection.mutable.Buffer -import scala.util.Try +import org.apache.streams.activitypub.servlets.WebfingerServlet.webfinger /** * org.apache.streams.activitypub.servlets.WebfingerResource response to inquires about any URI, typically an item in one of the published feeds, or one of the @@ -42,6 +30,11 @@ import scala.util.Try * as the default when no Accept is supplied, so that casual use through a */ +object WebfingerServlet { + given webfinger : WebfingerApi = WebfingerGraphImpl.DEFAULT + +} + @Rest( title = Array("Webfinger Microservice"), description = Array("Webfinger Microservice"), @@ -59,68 +52,47 @@ import scala.util.Try ) class WebfingerServlet extends BasicRestServlet with WebfingerRest { - override def init(servletConfig: ServletConfig): Unit = { - super.init(servletConfig) - } - -// class WebfingerAcceptMatcher extends RestMatcher { -// -// private final val ACCEPTABLE = Set( -// "application/jrd+json", -// MediaType.APPLICATION_JSON, -// MediaType.TEXT_HTML -// ) -// -// def matches( httpServletRequest: HttpServletRequest ) : Boolean = { -// val acceptHeaderString: String = httpServletRequest.getHeader("Accept") -// val acceptHeaders = StringUtils.split(acceptHeaderString, ',').toSet -// val matches = ACCEPTABLE.intersect(acceptHeaders).nonEmpty -// matches -// } + override def init(servletConfig: ServletConfig): Unit = { + super.init(servletConfig) + } -// } - - def webfingerQueryRequestIsValid( request: WebfingerQueryRequest) : Boolean = { - !request.getResource().isEmpty() - } + def webfingerQueryRequestIsValid( request: WebfingerQueryRequest) : Boolean = { + !request.getResource().isEmpty() + } - @RestGet( - path = Array("/*"), - summary = "Lookup resources by URI", - description = Array("Lookup resources by URI"), - //matchers = Array(classOf[WebfingerAcceptMatcher]) - ) - def webfingerQueryGet(restRequest: RestRequest, - restResponse: RestResponse ) = { + @RestGet( + path = Array("/*"), + summary = "Lookup resources by URI", + description = Array("Lookup resources by URI"), + //matchers = Array(classOf[WebfingerAcceptMatcher]) + ) + def webfingerQueryGet(restRequest: RestRequest, + restResponse: RestResponse ) = { - val resource = restRequest.getParameter("resource") + val resource = restRequest.getParameter("resource") - val webfingerQueryRequest = new WebfingerQueryRequest() - .withResource(resource); + val webfingerQueryRequest = new WebfingerQueryRequest() + .withResource(resource); - if (webfingerQueryRequestIsValid(webfingerQueryRequest)) { - try { - restResponse.setContent(webfingerQuery(webfingerQueryRequest)); - restResponse.setStatus(HttpServletResponse.SC_OK); - } catch { - case e : Exception => { - restResponse.setException(e); - restResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - } else { - restResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + if (webfingerQueryRequestIsValid(webfingerQueryRequest)) { + try { + restResponse.setContent(webfingerQuery(webfingerQueryRequest)); + restResponse.setStatus(HttpServletResponse.SC_OK); + } catch { + case e : Exception => { + restResponse.setException(e); + restResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } - + } + } else { + restResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); } - def webfingerQuery( request : WebfingerQueryRequest) : WebfingerQueryResponse = { - val response = new WebfingerQueryResponse() - .withSubject(request.getResource()) - .withProperties( - new Properties() - .withAdditionalProperty("propertyKey", "propertyValue") - ) - response; - } + } + + def doWebfingerQuery( request : WebfingerQueryRequest )( using webfingerApi : WebfingerApi ) : WebfingerQueryResponse = { + webfingerApi.webfingerQuery(request); + } + + override def webfingerQuery(request: WebfingerQueryRequest): WebfingerQueryResponse = doWebfingerQuery(request) } diff --git a/streams-activitypub-api/pom.xml b/streams-activitypub-utils/pom.xml similarity index 64% copy from streams-activitypub-api/pom.xml copy to streams-activitypub-utils/pom.xml index a759d5f..51fcf92 100755 --- a/streams-activitypub-api/pom.xml +++ b/streams-activitypub-utils/pom.xml @@ -18,36 +18,34 @@ --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - - <modelVersion>4.0.0</modelVersion> - <parent> <artifactId>apache-streams-activitypub</artifactId> <groupId>org.apache.streams.activitypub</groupId> <version>0.8.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> + <modelVersion>4.0.0</modelVersion> - <artifactId>streams-activitypub-api</artifactId> - <description>Apache Streams ActivityPub Server APIs</description> + <artifactId>streams-activitypub-utils</artifactId> <name>${project.artifactId}</name> + + <description>Apache Streams Utility Classes</description> + <packaging>jar</packaging> <dependencies> - <dependency> - <groupId>org.apache.streams</groupId> - <artifactId>streams-config</artifactId> - <version>${project.version}</version> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>${httpcomponents.client.version}</version> </dependency> - - <!-- streams-api only dependencies --> + <!-- streams-activitypub-utils only dependencies --> <dependency> <groupId>org.apache.juneau</groupId> - <artifactId>juneau-rest-common</artifactId> + <artifactId>juneau-marshall</artifactId> <version>${juneau.version}</version> </dependency> - + <!-- streams-activitypub-utils only test dependencies --> </dependencies> <build> <sourceDirectory>src/main/scala</sourceDirectory> @@ -64,38 +62,17 @@ </testResources> <plugins> <plugin> - <groupId>net.alchim31.maven</groupId> - <artifactId>scala-maven-plugin</artifactId> - <configuration> - <source>${project.basedir}/src/main/scala</source> - <scalaVersion>${scala.version}</scalaVersion> - </configuration> - <executions> - <execution> - <goals> - <goal>compile</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.jsonschema2pojo</groupId> - <artifactId>jsonschema2pojo-maven-plugin</artifactId> - <version>1.2.1</version> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> <executions> <execution> - <id>generate-sources</id> <goals> - <goal>generate</goal> + <goal>jar</goal> + <goal>test-jar</goal> </goals> - <phase>generate-sources</phase> </execution> </executions> </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - </plugin> </plugins> </build> </project> diff --git a/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/util/AcctPrefixResourceToResourceURISwap.scala b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/util/AcctPrefixResourceToResourceURISwap.scala new file mode 100644 index 0000000..4c30cce --- /dev/null +++ b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/util/AcctPrefixResourceToResourceURISwap.scala @@ -0,0 +1,39 @@ +package org.apache.streams.activitypub.util + +import org.apache.http.client.utils.URIBuilder +import org.apache.juneau.BeanSession +import org.apache.juneau.ClassMeta +import org.apache.juneau.swap.StringSwap + +import java.net.URI + +object AcctPrefixResourceToResourceURISwap { + def doSwap(uri: URI): String = { + val domain = uri.getHost + val preferredName = uri.getPath.split("/").last + s"acct:${preferredName}@${domain}" + } + def doUnswap(string: String): URI = { + val preferredName = string.split("@")(0) + val domain = string.split("@")(1) + val uriBuilder = new URIBuilder() + .setScheme("https") + .setHost(domain) + .setPath(s"/users/${preferredName}") + uriBuilder.build() + } +} + +class AcctPrefixResourceToResourceURISwap extends StringSwap[URI] { + + //@throws(classOf[Exception]) + override def swap(session: BeanSession, uri: URI) : String = { + AcctPrefixResourceToResourceURISwap.doSwap(uri) + } + + //@throws(classOf[Exception]) + override def unswap(session: BeanSession, in: String, hint: ClassMeta[Any]): URI = { + AcctPrefixResourceToResourceURISwap.doUnswap(in); + } + +} diff --git a/streams-activitypub-utils/src/test/scala/AcctPrefixResourceURISwapTest.scala b/streams-activitypub-utils/src/test/scala/AcctPrefixResourceURISwapTest.scala new file mode 100644 index 0000000..2dbfc9f --- /dev/null +++ b/streams-activitypub-utils/src/test/scala/AcctPrefixResourceURISwapTest.scala @@ -0,0 +1,34 @@ +package org.apache.streams.activitypub.util.test.cases + +import org.apache.http.client.utils.URIBuilder +import org.apache.streams.activitypub.util.AcctPrefixResourceToResourceURISwap +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +import java.net.URI + +class AcctPrefixResourceURISwapTest { + + private final val swapper = new AcctPrefixResourceToResourceURISwap() + + @Test + @DisplayName("Test Acct Prefix Swap") + def testSwap() = { + val testInput: URI = new URIBuilder() + .setScheme("https") + .setHost("mastodon.social") + .setPath("/users/steveblackmon") + .build() + val testOutput: String = "acct:steveblackmon@mastodon.social" + Assertions.assertEquals(testOutput, AcctPrefixResourceToResourceURISwap.doSwap(testInput)) + } + + @Test + @DisplayName("Test Acct Prefix Un-Swap") + def testUnswap() = { + val testInput: String = "acct:steveblackmon@mastodon.social" + val testOutput: URI = new URIBuilder("https://mastodon.social/users/steveblackmon").build() + Assertions.assertEquals(testOutput, AcctPrefixResourceToResourceURISwap.doUnswap(testInput)) + } +} diff --git a/streams-activitypub-webapp/pom.xml b/streams-activitypub-webapp/pom.xml index 4fbdbed..fc4ca26 100755 --- a/streams-activitypub-webapp/pom.xml +++ b/streams-activitypub-webapp/pom.xml @@ -51,6 +51,11 @@ <artifactId>streams-activitypub-actors</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.streams.activitypub</groupId> + <artifactId>streams-activitypub-graph</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.streams.activitypub</groupId> <artifactId>streams-activitypub-servlets</artifactId> @@ -68,6 +73,16 @@ <!-- <version>${tomcat.version}</version>--> <!-- <scope>provided</scope>--> <!-- </dependency>--> + + <!-- streams-activitypub-webapp test dependencies --> + <dependency> + <groupId>org.apache.streams.activitypub</groupId> + <artifactId>streams-activitypub-graph</artifactId> + <version>${project.version}</version> + <scope>test</scope> + <type>test-jar</type> + </dependency> + <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> @@ -104,6 +119,27 @@ <version>${tomcat.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-fuseki-main</artifactId> + <version>${jena.version}</version> + <type>jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-arq</artifactId> + <version>${jena.version}</version> + <type>jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.jena</groupId> + <artifactId>jena-rdfconnection</artifactId> + <version>${jena.version}</version> + <type>jar</type> + <scope>test</scope> + </dependency> </dependencies> <build> <finalName>streams-activitypub-webapp</finalName> @@ -164,6 +200,30 @@ </webResources> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>copy-resources</id> + <phase>process-test-resources</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${project.build.testOutputDirectory}</outputDirectory> + <resources> + <resource> + <directory>..</directory> + <includes> + <include>*.ttl</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> </plugins> </build> </project> diff --git a/streams-activitypub-webapp/src/test/resources/application.conf b/streams-activitypub-webapp/src/test/resources/application.conf new file mode 100644 index 0000000..fde8a62 --- /dev/null +++ b/streams-activitypub-webapp/src/test/resources/application.conf @@ -0,0 +1,9 @@ +ActivityPubGraphTestSuiteExtensionConfig = { + fusekiEndpointURI = "http://localhost:13330/" + testDatasetResource = "doap.ttl" + testDatasetId = "doap.ttl" +} +WebfingerGraphImplConfig = { + fusekiEndpointURI = "http://localhost:13330/" + defaultDatasetId = "doap.ttl" +} diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuite.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuite.scala index 87c736a..8efba57 100755 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuite.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuite.scala @@ -1,6 +1,6 @@ package org.apache.streams.activitypub.webapp.test -import org.junit.jupiter.api.Order +import org.apache.streams.activitypub.graph.test.ActivityPubGraphTestSuiteExtension import org.junit.jupiter.api.extension.RegisterExtension import org.junit.platform.suite.api.SelectClasses import org.junit.platform.suite.api.Suite @@ -13,15 +13,18 @@ import org.junit.platform.suite.api.SuiteDisplayName; object ActivityPubWebappTestSuite { @RegisterExtension - val helper: ActivityPubWebappTestSuiteExtension = new ActivityPubWebappTestSuiteExtension() + given webapp: ActivityPubWebappTestSuiteExtension = new ActivityPubWebappTestSuiteExtension() + + @RegisterExtension + given graph: ActivityPubGraphTestSuiteExtension = new ActivityPubGraphTestSuiteExtension() } @Suite @SuiteDisplayName("ActivityPub Webapp Integration Tests") -@Order(Order.DEFAULT) @SelectClasses(Array( - classOf[org.apache.streams.activitypub.webapp.test.cases.ServerAvailableTest], + classOf[org.apache.streams.activitypub.graph.test.cases.GraphDatabaseServerAvailableTest], + classOf[org.apache.streams.activitypub.webapp.test.cases.WebappServerAvailableTest], classOf[org.apache.streams.activitypub.webapp.test.cases.ServletRegistrationTest], classOf[org.apache.streams.activitypub.webapp.test.cases.RootServletTest], classOf[org.apache.streams.activitypub.webapp.test.cases.WebfingerServletTest] diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuiteExtension.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuiteExtension.scala index 40ce03b..1266955 100644 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuiteExtension.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/ActivityPubWebappTestSuiteExtension.scala @@ -1,10 +1,7 @@ package org.apache.streams.activitypub.webapp.test import org.apache.catalina.Context -import org.apache.catalina.Loader import org.apache.catalina.Server -import org.apache.catalina.loader.WebappClassLoaderBase -import org.apache.catalina.loader.WebappLoader import org.apache.catalina.startup.Tomcat import org.apache.http.client.utils.URIBuilder import org.apache.http.impl.NoConnectionReuseStrategy @@ -17,11 +14,9 @@ import org.junit.jupiter.api.extension.ParameterResolver import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.File import java.net.URI import java.nio.file.Path import java.nio.file.Paths -import java.nio.file.Files class ActivityPubWebappTestSuiteExtension extends ParameterResolver with BeforeAllCallback with AfterAllCallback { @@ -36,7 +31,7 @@ class ActivityPubWebappTestSuiteExtension extends ParameterResolver with BeforeA case "streams-activitypub-webapp" => Paths.get(basePathDir, "target/streams-activitypub-webapp") } private val webapp: String = webappPath.toAbsolutePath.toString - + private val tomcat: Tomcat = new Tomcat() tomcat.enableNaming() tomcat.setBaseDir(basePathDir) @@ -81,24 +76,13 @@ class ActivityPubWebappTestSuiteExtension extends ParameterResolver with BeforeA override def afterAll(extensionContext: ExtensionContext): Unit = { LOGGER.info("afterAll()") - try { - tomcat.stop(); - } catch { - case e: Exception => LOGGER.error("Exception while stopping tomcat", e); - } finally { - try { - tomcat.destroy(); - } catch { - case e: Exception => LOGGER.error("Exception while destoying tomcat", e); - } - } } override def supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean = { - false + parameterContext.getParameter.getType.equals(this.getClass) } override def resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): AnyRef = { - null + this } } diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/RootServletTest.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/RootServletTest.scala index f095b80..c57e3a3 100755 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/RootServletTest.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/RootServletTest.scala @@ -3,19 +3,24 @@ package org.apache.streams.activitypub.webapp.test.cases import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.slf4j.LoggerFactory; +import org.slf4j.LoggerFactory +@ExtendWith(Array(classOf[ActivityPubWebappTestSuiteExtension])) class RootServletTest { private final val LOGGER = LoggerFactory.getLogger(classOf[RootServletTest]); - @Test + /** + * Test that the root resource is available + */ @DisplayName("Root Resource Available") - @throws(classOf[Exception]) - def testRootResourceAvailable() : Unit = { - val response = ActivityPubWebappTestSuite.helper.restClient.get().json().run(); - assert(response.getStatusCode() == 200); + @Order(2) + @Test + def testRootResourceAvailable(using helper : ActivityPubWebappTestSuiteExtension) : Unit = { + val response = helper.restClient.get().json().run(); + assert(response.getStatusCode == 200); } } diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServletRegistrationTest.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServletRegistrationTest.scala index 538a50b..1ee50ee 100755 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServletRegistrationTest.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServletRegistrationTest.scala @@ -1,23 +1,30 @@ package org.apache.streams.activitypub.webapp.test.cases -import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite +import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory; +import org.junit.jupiter.api.extension.ExtendWith +import org.slf4j.LoggerFactory + +@ExtendWith(Array(classOf[ActivityPubWebappTestSuiteExtension])) class ServletRegistrationTest { private final val LOGGER = LoggerFactory.getLogger(classOf[ServletRegistrationTest]); - @Test + /** + * Tests that all servlets are registered + */ @DisplayName("All Servlets are registered") - @throws(classOf[Exception]) - def testAllServletsRegistered() : Unit = { - val servletRegistations = ActivityPubWebappTestSuite.helper.context.getServletContext.getServletRegistrations() - Assertions.assertNotNull(servletRegistations) - Assertions.assertEquals(2, servletRegistations.size()) - Assertions.assertTrue(servletRegistations.containsKey("root")) - Assertions.assertTrue(servletRegistations.containsKey("webfinger")) + @Order(3) + @Test + def testAllServletsRegistered(using helper: ActivityPubWebappTestSuiteExtension) : Unit = { + val servletRegistrations = helper.context.getServletContext.getServletRegistrations() + Assertions.assertNotNull(servletRegistrations) + Assertions.assertEquals(2, servletRegistrations.size()) + Assertions.assertTrue(servletRegistrations.containsKey("root")) + Assertions.assertTrue(servletRegistrations.containsKey("webfinger")) } } diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServerAvailableTest.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala similarity index 53% rename from streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServerAvailableTest.scala rename to streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala index 8eedc05..a2af02c 100755 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/ServerAvailableTest.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala @@ -1,31 +1,33 @@ package org.apache.streams.activitypub.webapp.test.cases +import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension import org.awaitility.Awaitility.await -import org.awaitility.Awaitility.waitAtMost -import org.awaitility.core.ConditionTimeoutException -import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite import org.awaitility.scala.AwaitilitySupport -import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.slf4j.LoggerFactory import scala.concurrent.duration.MINUTES -import scala.concurrent.duration.SECONDS import scala.util.Try; -class ServerAvailableTest extends AwaitilitySupport { +@ExtendWith(Array(classOf[ActivityPubWebappTestSuiteExtension])) +class WebappServerAvailableTest extends AwaitilitySupport { - private final val LOGGER = LoggerFactory.getLogger(classOf[ServerAvailableTest]); + private final val LOGGER = LoggerFactory.getLogger(classOf[WebappServerAvailableTest]); - @Test + /** + * Test that the server is available + */ @DisplayName("Test server is available") - @throws(classOf[Exception]) - def testServerAvailable() : Unit = { + @Order(0) + @Test + def testWebappServerAvailable(using helper: ActivityPubWebappTestSuiteExtension) : Unit = { val testAttempt = Try { await atMost(5, MINUTES) untilAsserted { - ActivityPubWebappTestSuite.helper.server.getStateName.matches("STARTED") + helper.server.getStateName.matches("STARTED") } } Assertions.assertTrue(testAttempt.isSuccess) diff --git a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala index 2436980..65a8c86 100755 --- a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala +++ b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala @@ -3,36 +3,53 @@ package org.apache.streams.activitypub.webapp.test.cases import org.apache.http.HttpStatus import org.apache.http.client.utils.URIBuilder import org.apache.http.entity.ContentType -import org.apache.juneau.rest.client.RestResponse -import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.charset.Charset; -class WebfingerServletTest { +@ExtendWith(Array(classOf[ActivityPubWebappTestSuiteExtension])) +class WebfingerServletTest(using helper: ActivityPubWebappTestSuiteExtension) { private final val LOGGER = LoggerFactory.getLogger(classOf[WebfingerServletTest]); - private val uriBuilder : URIBuilder = ActivityPubWebappTestSuite.helper.uriBuilder + private val uriBuilder : URIBuilder = helper.uriBuilder .setCharset(Charset.defaultCharset()) .setPath(".well-known/webfinger") - @Test + /** + * Test the WebfingerServlet with a valid abbreviated-form esource parameter + */ @DisplayName("WebfingerQuery With Abbreviated Resource Parameter") - @throws(classOf[Exception]) - def testWebfingerQueryWithAbbreviatedResourceParameter() : Unit = { - val request = ActivityPubWebappTestSuite.helper.restClientBuilder - .accept(ContentType.APPLICATION_JSON.getMimeType) + @Order(4) + @Test + def testWebfingerQueryWithAbbreviatedResourceParameter(using helper: ActivityPubWebappTestSuiteExtension) : Unit = { + val request = helper.restClientBuilder + .accept(ContentType.APPLICATION_JSON.getMimeType) + .json() + .build() + .get(uriBuilder.setParameter("resource", "acct:steveblackmon@mastodon.social").build()) + val response = request.run(); + response.assertStatus(HttpStatus.SC_OK) + } + + /** + * Test the WebfingerServlet with a valid URI-form resource parameter + */ + @DisplayName("WebfingerQuery With URI Resource Parameter") + @Order(4) + @Test + def testWebfingerQueryWithUriResourceParameter(using helper: ActivityPubWebappTestSuiteExtension): Unit = { + val request = helper.restClientBuilder + .accept(ContentType.APPLICATION_JSON.getMimeType) .json() .build() - .get(uriBuilder.setParameter("resource", "acct:sblackmon@asf.social").build()) + .get(uriBuilder.setParameter("resource", "https://mastodon.social/users/steveblackmon").build()) val response = request.run(); response.assertStatus(HttpStatus.SC_OK) } } -