diff --git a/src/e2eTest/kotlin/AssertionUtil.kt b/src/e2eTest/kotlin/AssertionUtil.kt new file mode 100644 index 00000000..4d559624 --- /dev/null +++ b/src/e2eTest/kotlin/AssertionUtil.kt @@ -0,0 +1,18 @@ +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Users +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll + +object AssertionUtil { + + fun assertUserExist(username: String, domain: String): Boolean { + val selectAll = Users.selectAll() + println(selectAll.fetchSize) + + println(selectAll.toList().size) + + selectAll.map { "@${it[Users.name]}@${it[Users.domain]}" }.forEach { println(it) } + + return Users.select { Users.name eq username and (Users.domain eq domain) }.empty().not() + } +} diff --git a/src/e2eTest/kotlin/KarateUtil.kt b/src/e2eTest/kotlin/KarateUtil.kt index e78e8510..c71cd44b 100644 --- a/src/e2eTest/kotlin/KarateUtil.kt +++ b/src/e2eTest/kotlin/KarateUtil.kt @@ -9,4 +9,20 @@ object KarateUtil { .karateEnv("dev") } } + + fun e2eTest(path: String, scenario: String = "", properties: Map, clazz: Class<*>): Karate { + val run = Karate.run(path) + + val karate = if (scenario.isEmpty()) { + run + } else { + run.scenarioName(scenario) + } + + var relativeTo = karate.relativeTo(clazz) + + properties.map { relativeTo = relativeTo.systemProperty(it.key, it.value) } + + return relativeTo.karateEnv("dev") + } } diff --git a/src/e2eTest/kotlin/federation/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt new file mode 100644 index 00000000..59bc4e14 --- /dev/null +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -0,0 +1,91 @@ +package federation + +import AssertionUtil +import KarateUtil +import com.intuit.karate.core.MockServer +import com.intuit.karate.junit5.Karate +import dev.usbharu.hideout.SpringApplication +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.transaction.annotation.Transactional +import java.net.MalformedURLException +import java.net.URL + +@SpringBootTest( + classes = [SpringApplication::class], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +@Transactional +class InboxCommonTest { + @LocalServerPort + private var port = "" + + @Karate.Test + @TestFactory + fun `inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く`(): Karate { + return KarateUtil.e2eTest( + "InboxCommonTest", + "inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く", + mapOf( + "karate.port" to port, + "karate.remotePort" to _remotePort + ), + javaClass + ) + } + + companion object { + + lateinit var server: MockServer + lateinit var _remotePort: String + + @JvmStatic + fun assertUserExist(username: String, domain: String) = runBlocking { + val s = try { + val url = URL(domain) + url.host + ":" + url.port.toString().takeIf { it != "-1" }.orEmpty() + } catch (e: MalformedURLException) { + domain + } + + var check = false + + repeat(10) { + delay(1000) + check = AssertionUtil.assertUserExist(username, s) or check + if (check) { + return@repeat + } + } + + assertTrue(check, "User @$username@$s not exist.") + } + + @JvmStatic + fun getRemotePort(): String = _remotePort + + @BeforeAll + @JvmStatic + fun beforeAll(@Autowired flyway: Flyway) { + server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build() + _remotePort = server.port.toString() + + flyway.clean() + flyway.migrate() + } + + @AfterAll + @JvmStatic + fun afterAll() { + server.stop() + } + } +} diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml index ad806350..1013de15 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -1,6 +1,6 @@ hideout: url: "https://localhost:8080" - use-mongodb: true + use-mongodb: false security: jwt: generate: true @@ -22,7 +22,7 @@ spring: clean-disabled: false datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true" + url: "jdbc:h2:./e2e-test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4" username: "" password: data: diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature new file mode 100644 index 00000000..7d102607 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -0,0 +1,22 @@ +Feature: Inbox Common Test + + Background: + * url baseUrl + + Scenario: inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く + + * def inbox = + """ + { "type": "Follow" } + """ + + Given path `/inbox` + And request inbox +# And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target)", signature="a"' + And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="' + When method post + Then status 202 + + * def assertInbox = Java.type(`federation.InboxCommonTest`) + + And assertInbox.assertUserExist('test-user',remoteUrl) diff --git a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature new file mode 100644 index 00000000..601dcf83 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -0,0 +1,126 @@ +Feature: InboxCommonMockServer + + Background: + * def assertInbox = Java.type(`federation.InboxCommonTest`) + + Scenario: pathMatches('/users/test-user') && methodIs('get') + * def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort() + * def userUrl = remoteUrl + '/users/test-user' + + * def person = + """ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "featuredTags": { + "@id": "toot:featuredTags", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "discoverable": "toot:discoverable", + "Device": "toot:Device", + "Ed25519Signature": "toot:Ed25519Signature", + "Ed25519Key": "toot:Ed25519Key", + "Curve25519Key": "toot:Curve25519Key", + "EncryptedMessage": "toot:EncryptedMessage", + "publicKeyBase64": "toot:publicKeyBase64", + "deviceId": "toot:deviceId", + "claim": { + "@type": "@id", + "@id": "toot:claim" + }, + "fingerprintKey": { + "@type": "@id", + "@id": "toot:fingerprintKey" + }, + "identityKey": { + "@type": "@id", + "@id": "toot:identityKey" + }, + "devices": { + "@type": "@id", + "@id": "toot:devices" + }, + "messageFranking": "toot:messageFranking", + "messageType": "toot:messageType", + "cipherText": "toot:cipherText", + "suspended": "toot:suspended", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": #(userUrl), + "type": "Person", + "following": #(userUrl + '/following'), + "followers": "https://mastodon.social/users/Gargron/followers", + "inbox": "https://mastodon.social/users/Gargron/inbox", + "outbox": "https://mastodon.social/users/Gargron/outbox", + "featured": "https://mastodon.social/users/Gargron/collections/featured", + "featuredTags": "https://mastodon.social/users/Gargron/collections/tags", + "preferredUsername": "test-user", + "name": "test-user", + "summary": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e", + "url": "https://mastodon.social/@Gargron", + "manuallyApprovesFollowers": false, + "discoverable": true, + "published": "2016-03-16T00:00:00Z", + "devices": "https://mastodon.social/users/Gargron/collections/devices", + "alsoKnownAs": [ + "https://tooting.ai/users/Gargron" + ], + "publicKey": { + "id": "https://mastodon.social/users/Gargron#main-key", + "owner": "https://mastodon.social/users/Gargron", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [ + { + "type": "PropertyValue", + "name": "Patreon", + "value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + }, + { + "type": "PropertyValue", + "name": "GitHub", + "value": "\u003ca href=\"https://github.com/Gargron\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/Gargron\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + } + ], + "endpoints": { + "sharedInbox": "https://mastodon.social/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg" + }, + "image": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg" + } +} + + """ + + * def response = person diff --git a/src/e2eTest/resources/karate-config.js b/src/e2eTest/resources/karate-config.js index 9fd5dd1e..a83c2bb4 100644 --- a/src/e2eTest/resources/karate-config.js +++ b/src/e2eTest/resources/karate-config.js @@ -7,18 +7,23 @@ function fn() { } let config; if (env === 'test') { + let remotePort = karate.properties['karate.remotePort'] || '8081' config = { - baseUrl: 'https://test-hideout.usbharu.dev' + baseUrl: 'https://test-hideout.usbharu.dev', + remoteUrl: 'http://localhost:' + remotePort } } else if (env === 'dev') { let port = karate.properties['karate.port'] || '8080' + let remotePort = karate.properties['karate.remotePort'] || '8081' config = { - baseUrl: 'http://localhost:' + port + baseUrl: 'http://localhost:' + port, + remoteUrl: 'http://localhost:' + remotePort } } else { throw 'Unknown environment [' + env + '].' } // don't waste time waiting for a connection or if servers don't respond within 0,3 seconds + karate.configure('connectTimeout', 1000); karate.configure('readTimeout', 1000); return config;