Merge pull request #44 from usbharu/feature/delete-ktor

Feature/delete ktor
This commit is contained in:
usbharu 2023-09-23 00:05:21 +09:00 committed by GitHub
commit 34b19829f4
36 changed files with 4 additions and 3501 deletions

View File

@ -1,4 +1,3 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
@ -11,7 +10,6 @@ val koin_version: String by project
plugins { plugins {
kotlin("jvm") version "1.8.21" kotlin("jvm") version "1.8.21"
id("io.ktor.plugin") version "2.3.0"
id("org.graalvm.buildtools.native") version "0.9.21" id("org.graalvm.buildtools.native") version "0.9.21"
id("io.gitlab.arturbosch.detekt") version "1.23.1" id("io.gitlab.arturbosch.detekt") version "1.23.1"
id("com.google.devtools.ksp") version "1.8.21-1.0.11" id("com.google.devtools.ksp") version "1.8.21-1.0.11"
@ -27,12 +25,6 @@ apply {
group = "dev.usbharu" group = "dev.usbharu"
version = "0.0.1" version = "0.0.1"
application {
mainClass.set("dev.usbharu.hideout.SpringApplicationKt")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
@ -51,47 +43,11 @@ tasks.withType<KotlinCompile> {
mustRunAfter("openApiGenerateMastodonCompatibleApi") mustRunAfter("openApiGenerateMastodonCompatibleApi")
} }
tasks.withType<ShadowJar> {
manifest {
attributes(
"Implementation-Version" to project.version.toString()
)
}
}
tasks.clean { tasks.clean {
delete += listOf("$rootDir/src/main/resources/static") delete += listOf("$rootDir/src/main/resources/static")
} }
//tasks.create<GenerateTask>("openApiGenerateServer", GenerateTask::class) {
// generatorName.set("kotlin-spring")
// inputSpec.set("$rootDir/src/main/resources/openapi/api.yaml")
// outputDir.set("$buildDir/generated/sources/openapi")
// apiPackage.set("dev.usbharu.hideout.controller.generated")
// modelPackage.set("dev.usbharu.hideout.domain.model.generated")
// configOptions.put("interfaceOnly", "true")
// configOptions.put("useSpringBoot3", "true")
// additionalProperties.put("useTags", "true")
// schemaMappings.putAll(
// mapOf(
// "ReactionResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse",
// "Account" to "dev.usbharu.hideout.domain.model.hideout.dto.Account",
// "JwtToken" to "dev.usbharu.hideout.domain.model.hideout.dto.JwtToken",
// "PostRequest" to "dev.usbharu.hideout.domain.model.hideout.form.Post",
// "PostResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.PostResponse",
// "Reaction" to "dev.usbharu.hideout.domain.model.hideout.form.Reaction",
// "RefreshToken" to "dev.usbharu.hideout.domain.model.hideout.form.RefreshToken",
// "UserLogin" to "dev.usbharu.hideout.domain.model.hideout.form.UserLogin",
// "UserResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.UserResponse",
// "UserCreate" to "dev.usbharu.hideout.domain.model.hideout.form.UserCreate",
// "Visibility" to "dev.usbharu.hideout.domain.model.hideout.entity.Visibility",
// )
// )
//
//// importMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
//// typeMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
//}
tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask::class) { tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask::class) {
generatorName.set("kotlin-spring") generatorName.set("kotlin-spring")
inputSpec.set("$rootDir/src/main/resources/openapi/mastodon.yaml") inputSpec.set("$rootDir/src/main/resources/openapi/mastodon.yaml")
@ -128,31 +84,21 @@ sourceSets.main {
} }
dependencies { dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-auth-jwt:$ktor_version")
implementation("io.ktor:ktor-server-sessions-jvm:$ktor_version")
implementation("io.ktor:ktor-server-auto-head-response-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version")
implementation("io.ktor:ktor-server-default-headers-jvm:$ktor_version")
implementation("io.ktor:ktor-server-forwarded-header-jvm:$ktor_version")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-jackson:$ktor_version") implementation("io.ktor:ktor-serialization-jackson:$ktor_version")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:$h2_version") implementation("com.h2database:h2:$h2_version")
implementation("org.xerial:sqlite-jdbc:3.40.1.0") implementation("org.xerial:sqlite-jdbc:3.40.1.0")
implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cio-jvm:$ktor_version")
implementation("io.ktor:ktor-server-compression:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("com.auth0:java-jwt:4.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("io.insert-koin:koin-core:$koin_version") implementation("io.insert-koin:koin-core:$koin_version")
implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("io.insert-koin:koin-annotations:1.2.0") implementation("io.insert-koin:koin-annotations:1.2.0")
implementation("io.ktor:ktor-server-compression-jvm:2.3.0")
implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-actuator")
ksp("io.insert-koin:koin-ksp-compiler:1.2.0") ksp("io.insert-koin:koin-ksp-compiler:1.2.0")
@ -178,10 +124,7 @@ dependencies {
implementation("org.springframework.security:spring-security-oauth2-jose") implementation("org.springframework.security:spring-security-oauth2-jose")
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
implementation("io.ktor:ktor-server-status-pages-jvm:$ktor_version")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
@ -197,53 +140,11 @@ dependencies {
implementation("org.drewcarlson:kjob-core:0.6.0") implementation("org.drewcarlson:kjob-core:0.6.0")
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation("org.slf4j:slf4j-simple:2.0.7") testImplementation("org.slf4j:slf4j-simple:2.0.7")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0")
} }
jib {
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
dockerClient.environment = mapOf(
"DOCKER_HOST" to "localhost:2375"
)
}
}
ktor {
docker {
localImageName.set("hideout")
}
}
graalvmNative {
binaries {
named("main") {
fallback.set(false)
verbose.set(true)
agent {
enabled.set(false)
}
buildArgs.add("--initialize-at-build-time=io.ktor,kotlin,kotlinx")
// buildArgs.add("--trace-class-initialization=ch.qos.logback.classic.Logger")
// buildArgs.add("--trace-object-instantiation=ch.qos.logback.core.AsyncAppenderBase"+"$"+"Worker")
// buildArgs.add("--trace-object-instantiation=ch.qos.logback.classic.Logger")
buildArgs.add("--initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback")
// buildArgs.add("--trace-object-instantiation=kotlinx.coroutines.channels.ArrayChannel")
buildArgs.add("--initialize-at-build-time=kotlinx.coroutines.channels.ArrayChannel")
buildArgs.add("-H:+InstallExitHandlers")
buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime")
buildArgs.add("-H:+ReportExceptionStackTraces")
runtimeArgs.add("-config=$buildDir/resources/main/application-native.conf")
imageName.set("graal-server")
}
}
}
detekt { detekt {
parallel = true parallel = true
config = files("detekt.yml") config = files("detekt.yml")

View File

@ -1,172 +0,0 @@
package dev.usbharu.hideout
import com.auth0.jwk.JwkProvider
import com.auth0.jwk.JwkProviderBuilder
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.*
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.register
import dev.usbharu.hideout.service.ap.APService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.service.api.PostApiService
import dev.usbharu.hideout.service.api.UserApiService
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.core.*
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.KJobJobQueueParentService
import dev.usbharu.hideout.service.user.UserService
import dev.usbharu.kjob.exposed.ExposedKJob
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.logging.*
import io.ktor.server.application.*
import kjob.core.kjob
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Database
import org.koin.ksp.generated.module
import org.koin.ktor.ext.inject
import java.util.concurrent.TimeUnit
@Deprecated("Ktor is deprecated")
fun main(args: Array<String>): Unit = io.ktor.server.cio.EngineMain.main(args)
val Application.property: Application.(propertyName: String) -> String
get() = {
environment.config.property(it).getString()
}
val Application.propertyOrNull: Application.(propertyName: String) -> String?
get() = {
environment.config.propertyOrNull(it)?.getString()
}
// application.conf references the main function. This annotation prevents the IDE from marking it as unused.
@Deprecated("Ktor is deprecated")
@Suppress("unused", "LongMethod")
fun Application.parent() {
Config.configData = ConfigData(
url = property("hideout.url"),
objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false),
characterLimit = CharacterLimit(
general = CharacterLimit.General.of(
url = propertyOrNull("hideout.character-limit.general.url")?.toIntOrNull(),
domain = propertyOrNull("hideout.character-limit.general.domain")?.toIntOrNull(),
publicKey = propertyOrNull("hideout.character-limit.general.publicKey")?.toIntOrNull(),
privateKey = propertyOrNull("hideout.character-limit.general.privateKey")?.toIntOrNull()
)
)
)
val module = org.koin.dsl.module {
single<Database> {
Database.connect(
url = property("hideout.database.url"),
driver = property("hideout.database.driver"),
user = property("hideout.database.username"),
password = property("hideout.database.password")
)
}
single<JobQueueParentService> {
val kJobJobQueueService = KJobJobQueueParentService(get())
kJobJobQueueService.init(emptyList())
kJobJobQueueService
}
single<HttpClient> {
HttpClient(CIO).config {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
}
install(httpSignaturePlugin) {
keyMap = KtorKeyMap(get(), get())
}
expectSuccess = true
}
}
single<IdGenerateService> { TwitterSnowflakeIdGenerateService }
single<JwkProvider> {
JwkProviderBuilder(Config.configData.url).cached(
10,
24,
TimeUnit.HOURS
)
.rateLimited(10, 1, TimeUnit.MINUTES).build()
}
}
configureKoin(module, HideoutModule().module)
configureStatusPages()
runBlocking {
inject<ServerInitialiseService>().value.init()
}
configureCompression()
configureHTTP()
configureStaticRouting()
configureMonitoring()
configureSerialization()
register(inject<UserApiService>().value)
configureSecurity(
inject<JwkProvider>().value,
inject<MetaService>().value
)
configureRouting(
httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value,
apService = inject<APService>().value,
userService = inject<UserService>().value,
apUserService = inject<APUserService>().value,
postService = inject<PostApiService>().value,
userApiService = inject<UserApiService>().value,
userQueryService = inject<UserQueryService>().value,
followerQueryService = inject<FollowerQueryService>().value,
userAuthApiService = inject<UserAuthApiService>().value,
webFingerApiService = inject<WebFingerApiService>().value,
transaction = inject<Transaction>().value
)
}
@Deprecated("Ktor is deprecated")
@Suppress("unused")
fun Application.worker() {
val kJob = kjob(ExposedKJob) {
connectionDatabase = inject<Database>().value
}.start()
val apService = inject<APService>().value
kJob.register(ReceiveFollowJob) {
execute {
apService.processActivity(this, it)
}
}
kJob.register(DeliverPostJob) {
execute {
apService.processActivity(this, it)
}
}
kJob.register(DeliverReactionJob) {
execute {
apService.processActivity(this, it)
}
}
kJob.register(DeliverRemoveReactionJob) {
execute {
apService.processActivity(this, it)
}
}
}

View File

@ -35,7 +35,6 @@ import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@EnableWebSecurity(debug = true) @EnableWebSecurity(debug = true)
@Configuration @Configuration
class SecurityConfig { class SecurityConfig {
@ -152,8 +151,6 @@ class SecurityConfig {
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType) { if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType) {
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
context.claims.claim("uid", userDetailsImpl.id.toString()) context.claims.claim("uid", userDetailsImpl.id.toString())
} }
} }
} }

View File

@ -35,5 +35,4 @@ class MastodonAppsApiController(private val appApiService: AppApiService) : AppA
HttpStatus.OK HttpStatus.OK
) )
} }
} }

View File

@ -30,8 +30,6 @@ class UserDetailsImpl(
@Serial @Serial
private const val serialVersionUID: Long = -899168205656607781L private const val serialVersionUID: Long = -899168205656607781L
} }
} }
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@ -46,11 +44,9 @@ class UserDetailsImpl(
@JsonSubTypes @JsonSubTypes
abstract class UserDetailsMixin abstract class UserDetailsMixin
class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() { class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {} val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
val mapper = p.codec as ObjectMapper val mapper = p.codec as ObjectMapper
val jsonNode: JsonNode = mapper.readTree(p) val jsonNode: JsonNode = mapper.readTree(p)
println(jsonNode) println(jsonNode)
@ -70,7 +66,6 @@ class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
true, true,
authorities.toMutableList(), authorities.toMutableList(),
) )
} }
fun JsonNode.readText(field: String, defaultValue: String = ""): String { fun JsonNode.readText(field: String, defaultValue: String = ""): String {
@ -79,5 +74,4 @@ class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
else -> defaultValue else -> defaultValue
} }
} }
} }

View File

@ -11,8 +11,6 @@ import io.ktor.client.plugins.api.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tech.barbero.http.message.signing.HttpMessage import tech.barbero.http.message.signing.HttpMessage
import tech.barbero.http.message.signing.HttpMessageSigner import tech.barbero.http.message.signing.HttpMessageSigner
@ -28,12 +26,6 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.crypto.SecretKey import javax.crypto.SecretKey
suspend fun <T : JsonLd> ApplicationCall.respondAp(message: T, status: HttpStatusCode = HttpStatusCode.OK) {
message.context += "https://www.w3.org/ns/activitystreams"
val activityJson = Config.configData.objectMapper.writeValueAsString(message)
respondText(activityJson, ContentType.Application.Activity, status)
}
suspend fun HttpClient.postAp(urlString: String, username: String, jsonLd: JsonLd): HttpResponse { suspend fun HttpClient.postAp(urlString: String, username: String, jsonLd: JsonLd): HttpResponse {
jsonLd.context += "https://www.w3.org/ns/activitystreams" jsonLd.context += "https://www.w3.org/ns/activitystreams"
return this.post(urlString) { return this.post(urlString) {

View File

@ -1,20 +0,0 @@
package dev.usbharu.hideout.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.compression.*
@Deprecated("Ktor is deprecated")
fun Application.configureCompression() {
install(Compression) {
gzip {
matchContentType(ContentType.Application.JavaScript)
priority = 1.0
}
deflate {
matchContentType(ContentType.Application.JavaScript)
priority = 10.0
minimumSize(1024) // condition
}
}
}

View File

@ -1,24 +0,0 @@
package dev.usbharu.hideout.plugins
import io.ktor.server.application.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.forwardedheaders.*
@Deprecated("Ktor is deprecated")
fun Application.configureHTTP() {
// install(CORS) {
// allowMethod(HttpMethod.Options)
// allowMethod(HttpMethod.Put)
// allowMethod(HttpMethod.Delete)
// allowMethod(HttpMethod.Patch)
// allowHeader(HttpHeaders.Authorization)
// allow
// allowHeader("MyCustomHeader")
// anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
// }
install(DefaultHeaders) {
header("X-Engine", "Ktor") // will send this header with each response
}
install(ForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy
install(XForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy
}

View File

@ -1,14 +0,0 @@
package dev.usbharu.hideout.plugins
import io.ktor.server.application.*
import org.koin.core.module.Module
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger
@Deprecated("Ktor is deprecated")
fun Application.configureKoin(vararg module: Module) {
install(Koin) {
slf4jLogger()
modules(*module)
}
}

View File

@ -1,12 +0,0 @@
package dev.usbharu.hideout.plugins
import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.Level
@Deprecated("Ktor is deprecated")
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
}
}

View File

@ -1,52 +0,0 @@
package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP
import dev.usbharu.hideout.routing.api.internal.v1.auth
import dev.usbharu.hideout.routing.api.internal.v1.posts
import dev.usbharu.hideout.routing.api.internal.v1.users
import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.ap.APService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.service.api.PostApiService
import dev.usbharu.hideout.service.api.UserApiService
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserService
import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
@Suppress("LongParameterList")
fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService,
apService: APService,
userService: UserService,
apUserService: APUserService,
postService: PostApiService,
userApiService: UserApiService,
userQueryService: UserQueryService,
followerQueryService: FollowerQueryService,
userAuthApiService: UserAuthApiService,
webFingerApiService: WebFingerApiService,
transaction: Transaction
) {
install(AutoHeadResponse)
routing {
inbox(httpSignatureVerifyService, apService)
outbox()
usersAP(apUserService, userQueryService, followerQueryService, transaction)
webfinger(webFingerApiService)
route("/api/internal/v1") {
posts(postService)
users(userService, userApiService)
auth(userAuthApiService)
}
}
}

View File

@ -1,51 +0,0 @@
package dev.usbharu.hideout.plugins
import com.auth0.jwk.JwkProvider
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.service.core.MetaService
import dev.usbharu.hideout.util.JsonWebKeyUtil
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
const val TOKEN_AUTH = "jwt-auth"
@Deprecated("Ktor is deprecated")
@Suppress("MagicNumber")
fun Application.configureSecurity(
jwkProvider: JwkProvider,
metaService: MetaService
) {
val issuer = Config.configData.url
install(Authentication) {
jwt(TOKEN_AUTH) {
verifier(jwkProvider, issuer) {
acceptLeeway(3)
}
validate { jwtCredential ->
val uid = jwtCredential.payload.getClaim("uid")
if (uid.isMissing) {
return@validate null
}
if (uid.asLong() == null) {
return@validate null
}
return@validate JWTPrincipal(jwtCredential.payload)
}
}
}
routing {
get("/.well-known/jwks.json") {
//language=JSON
val jwt = metaService.getJwtMeta()
call.respondText(
contentType = ContentType.Application.Json,
text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString())
)
}
}
}

View File

@ -1,21 +0,0 @@
package dev.usbharu.hideout.plugins
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
import com.fasterxml.jackson.databind.DeserializationFeature
import io.ktor.serialization.jackson.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
@Deprecated("Ktor is deprecated")
fun Application.configureSerialization() {
install(ContentNegotiation) {
jackson {
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
configOverride(List::class.java).setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
}
}
}

View File

@ -1,22 +0,0 @@
package dev.usbharu.hideout.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Application.configureStaticRouting() {
routing {
get("/") {
call.respondText(
String.javaClass.classLoader.getResourceAsStream("static/index.html").readAllBytes().decodeToString(),
contentType = ContentType.Text.Html
)
}
static("/") {
resources("static")
}
}
}

View File

@ -1,24 +0,0 @@
package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
@Deprecated("Ktor is deprecated")
fun Application.configureStatusPages() {
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
call.application.log.warn("Bad Request", cause)
}
exception<InvalidUsernameOrPasswordException> { call, _ ->
call.respond(HttpStatusCode.Unauthorized)
}
exception<Throwable> { call, cause ->
call.respondText(text = "500: ${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError)
call.application.log.error("Internal Server Error", cause)
}
}
}

View File

@ -175,7 +175,6 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere
return builder.build() return builder.build()
} }
} }
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql // org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql

View File

@ -1,44 +0,0 @@
package dev.usbharu.hideout.routing
import dev.usbharu.hideout.service.api.UserApiService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Application.register(userApiService: UserApiService) {
routing {
get("/register") {
val principal = call.principal<UserIdPrincipal>()
if (principal != null) {
call.respondRedirect("/users/${principal.name}")
}
call.respondText(ContentType.Text.Html) {
//language=HTML
"""
<html>
<head>
</head>
<body>
<form method='post' action=''>
<input type='text' name='username' value=''>
<input type='password' name='password'>
<input type="submit">
</form>
</body>
</html>
""".trimIndent()
}
}
post("/register") {
val parameters = call.receiveParameters()
val password = parameters["password"] ?: return@post call.respondRedirect("/register")
val username = parameters["username"] ?: return@post call.respondRedirect("/register")
userApiService.createUser(username, password)
call.respondRedirect("/users/$username")
}
}
}

View File

@ -1,77 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.ActivityPubObjectResponse
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.exception.HttpSignatureVerifyException
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Routing.inbox(
httpSignatureVerifyService: HttpSignatureVerifyService,
apService: dev.usbharu.hideout.service.ap.APService
) {
route("/inbox") {
get {
call.respond(HttpStatusCode.MethodNotAllowed)
}
post {
if (httpSignatureVerifyService.verify(call.request.headers).not()) {
throw HttpSignatureVerifyException()
}
val json = call.receiveText()
call.application.log.trace("Received: $json")
val activityTypes = apService.parseActivity(json)
call.application.log.debug("ActivityTypes: ${activityTypes.name}")
val response = apService.processActivity(json, activityTypes)
when (response) {
is ActivityPubObjectResponse -> call.respond(
response.httpStatusCode,
Config.configData.objectMapper.writeValueAsString(
response.message.apply {
context =
listOf("https://www.w3.org/ns/activitystreams")
}
)
)
is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message)
null -> call.respond(HttpStatusCode.NotImplemented)
}
}
}
route("/users/{name}/inbox") {
get {
call.respond(HttpStatusCode.MethodNotAllowed)
}
post {
if (httpSignatureVerifyService.verify(call.request.headers).not()) {
throw HttpSignatureVerifyException()
}
val json = call.receiveText()
call.application.log.trace("Received: $json")
val activityTypes = apService.parseActivity(json)
call.application.log.debug("ActivityTypes: ${activityTypes.name}")
val response = apService.processActivity(json, activityTypes)
when (response) {
is ActivityPubObjectResponse -> call.respond(
response.httpStatusCode,
Config.configData.objectMapper.writeValueAsString(
response.message.apply {
context =
listOf("https://www.w3.org/ns/activitystreams")
}
)
)
is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message)
null -> call.respond(HttpStatusCode.NotImplemented)
}
}
}
}

View File

@ -1,26 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Routing.outbox() {
route("/outbox") {
get {
call.respond(HttpStatusCode.NotImplemented)
}
post {
call.respond(HttpStatusCode.NotImplemented)
}
}
route("/users/{name}/outbox") {
get {
call.respond(HttpStatusCode.NotImplemented)
}
post {
call.respond(HttpStatusCode.NotImplemented)
}
}
}

View File

@ -1,65 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.respondAp
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Routing.usersAP(
apUserService: APUserService,
userQueryService: UserQueryService,
followerQueryService: FollowerQueryService,
transaction: Transaction
) {
route("/users/{name}") {
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
call.application.log.debug("Signature: ${call.request.header("Signature")}")
call.application.log.debug("Authorization: ${call.request.header("Authorization")}")
val name =
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
val person = apUserService.getPersonByName(name)
return@handle call.respondAp(person, HttpStatusCode.OK)
}
get {
// TODO: 暫定処置なので治す
transaction.transaction {
val userEntity = userQueryService.findByNameAndDomain(
call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='name') does not exist."),
Config.configData.domain
)
val personByName = apUserService.getPersonByName(userEntity.name)
call.respondText(
userEntity.toString() + "\n" + followerQueryService.findFollowersById(userEntity.id) +
"\n" + Config.configData.objectMapper.writeValueAsString(
personByName
)
)
}
}
}
}
@Deprecated("Ktor is deprecated")
class ContentTypeRouteSelector(private vararg val contentType: ContentType) : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
context.call.application.log.debug("Accept: ${context.call.request.accept()}")
val requestContentType = context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter
return if (requestContentType.split(",").any { contentType.any { contentType -> contentType.match(it) } }) {
RouteSelectorEvaluation.Constant
} else {
RouteSelectorEvaluation.FailedParameter
}
}
}

View File

@ -1,32 +0,0 @@
package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.api.UserAuthApiService
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Route.auth(userAuthApiService: UserAuthApiService) {
post("/login") {
val loginUser = call.receive<UserLogin>()
return@post call.respond(userAuthApiService.login(loginUser.username, loginUser.password))
}
post("/refresh-token") {
val refreshToken = call.receive<RefreshToken>()
return@post call.respond(userAuthApiService.refreshToken(refreshToken))
}
authenticate(TOKEN_AUTH) {
get("/auth-check") {
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val username = principal.payload.getClaim("uid")
call.respondText("Hello $username")
}
}
}

View File

@ -1,102 +0,0 @@
package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.domain.model.hideout.form.Post
import dev.usbharu.hideout.domain.model.hideout.form.Reaction
import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.api.PostApiService
import dev.usbharu.hideout.util.InstantParseUtil
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
@Suppress("LongMethod")
fun Route.posts(postApiService: PostApiService) {
route("/posts") {
authenticate(TOKEN_AUTH) {
post {
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val userId = principal.payload.getClaim("uid").asLong()
val receive = call.receive<Post>()
val create = postApiService.createPost(receive, userId)
call.response.header("Location", create.url)
call.respond(HttpStatusCode.OK)
}
route("/{id}/reactions") {
get {
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val userId = principal.payload.getClaim("uid").asLong()
val postId = (
call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
)
call.respond(postApiService.getReactionByPostId(postId, userId))
}
post {
val jwtPrincipal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val userId = jwtPrincipal.payload.getClaim("uid").asLong()
val postId = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
val reaction = try {
call.receive<Reaction>()
} catch (_: ContentTransformationException) {
Reaction(null)
}
postApiService.appendReaction(reaction.reaction ?: "", userId, postId)
call.respond(HttpStatusCode.NoContent)
}
delete {
val jwtPrincipal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val userId = jwtPrincipal.payload.getClaim("uid").asLong()
val postId = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
postApiService.removeReaction(userId, postId)
call.respond(HttpStatusCode.NoContent)
}
}
}
authenticate(TOKEN_AUTH, optional = true) {
get {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val since = InstantParseUtil.parse(call.request.queryParameters["since"])
val until = InstantParseUtil.parse(call.request.queryParameters["until"])
val minId = call.request.queryParameters["minId"]?.toLong()
val maxId = call.request.queryParameters["maxId"]?.toLong()
val limit = call.request.queryParameters["limit"]?.toInt()
call.respond(HttpStatusCode.OK, postApiService.getAll(since, until, minId, maxId, limit, userId))
}
get("/{id}") {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val id = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
val post = postApiService.getById(id, userId)
call.respond(post)
}
}
}
route("/users/{name}/posts") {
authenticate(TOKEN_AUTH, optional = true) {
get {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val targetUserName = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
val posts = postApiService.getByUser(targetUserName, userId = userId)
call.respond(posts)
}
get("/{id}") {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val id = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.")
val post = postApiService.getById(id, userId)
call.respond(post)
}
}
}
}

View File

@ -1,107 +0,0 @@
package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.form.UserCreate
import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.api.UserApiService
import dev.usbharu.hideout.service.user.UserService
import dev.usbharu.hideout.util.AcctUtil
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
@Suppress("LongMethod", "CognitiveComplexMethod")
fun Route.users(userService: UserService, userApiService: UserApiService) {
route("/users") {
get {
call.respond(userApiService.findAll())
}
post {
val userCreate = call.receive<UserCreate>()
if (userService.usernameAlreadyUse(userCreate.username)) {
return@post call.respond(HttpStatusCode.BadRequest)
}
val user = userService.createLocalUser(
UserCreateDto(
userCreate.username,
userCreate.username,
"",
userCreate.password
)
)
call.response.header("Location", "${Config.configData.url}/api/internal/v1/users/${user.name}")
call.respond(HttpStatusCode.Created)
}
route("/{name}") {
authenticate(TOKEN_AUTH, optional = true) {
get {
val userParameter = (
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
)
if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findById(userParameter.toLong()))
} else {
val acct = AcctUtil.parse(userParameter)
return@get call.respond(userApiService.findByAcct(acct))
}
}
}
route("/followers") {
get {
val userParameter = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowers(userParameter.toLong()))
}
val acct = AcctUtil.parse(userParameter)
return@get call.respond(userApiService.findFollowersByAcct(acct))
}
authenticate(TOKEN_AUTH) {
post {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
?: throw IllegalStateException("no principal")
val userParameter = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
if (if (userParameter.toLongOrNull() != null) {
userApiService.follow(userParameter.toLong(), userId)
} else {
val parse = AcctUtil.parse(userParameter)
userApiService.follow(parse, userId)
}
) {
call.respond(HttpStatusCode.OK)
} else {
call.respond(HttpStatusCode.Accepted)
}
}
}
}
route("/following") {
get {
val userParameter = (
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
)
if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowings(userParameter.toLong()))
}
val acct = AcctUtil.parse(userParameter)
return@get call.respond(userApiService.findFollowingsByAcct(acct))
}
}
}
}
}

View File

@ -1,18 +0,0 @@
package dev.usbharu.hideout.routing.api.mastodon.v1
// @Suppress("UnusedPrivateMember")
// fun Route.statuses(postService: IPostService) {
// // route("/statuses") {
// // post {
// // val status: StatusForPost = call.receive()
// // val post = dev.usbharu.hideout.domain.model.hideout.form.Post(
// // userId = status.userId,
// // createdAt = System.currentTimeMillis(),
// // text = status.status,
// // visibility = 1
// // )
// // postService.create(post)
// // call.respond(status)
// // }
// // }
// }

View File

@ -1,45 +0,0 @@
package dev.usbharu.hideout.routing.wellknown
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.wellknown.WebFinger
import dev.usbharu.hideout.exception.IllegalParameterException
import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Deprecated("Ktor is deprecated")
fun Routing.webfinger(webFingerApiService: WebFingerApiService) {
route("/.well-known/webfinger") {
get {
val acct = call.request.queryParameters["resource"]?.decodeURLPart()
?: throw ParameterNotExistException("Parameter(name='resource') does not exist.")
if (acct.startsWith("acct:").not()) {
throw IllegalParameterException("Parameter(name='resource') is not start with 'acct:'.")
}
val accountName = acct.substringBeforeLast("@")
.substringAfter("acct:")
.trimStart('@')
val userEntity = webFingerApiService.findByNameAndDomain(accountName, Config.configData.domain)
val webFinger = WebFinger(
subject = acct,
links = listOf(
WebFinger.Link(
rel = "self",
type = ContentType.Application.Activity.toString(),
href = "${Config.configData.url}/users/${userEntity.name}"
)
)
)
return@get call.respond(webFinger)
}
}
}

View File

@ -13,7 +13,6 @@ interface AccountApiService {
suspend fun verifyCredentials(userid: Long): CredentialAccount suspend fun verifyCredentials(userid: Long): CredentialAccount
} }
@Service @Service
class AccountApiServiceImpl(private val accountService: AccountService, private val transaction: Transaction) : class AccountApiServiceImpl(private val accountService: AccountService, private val transaction: Transaction) :
AccountApiService { AccountApiService {
@ -59,5 +58,4 @@ class AccountApiServiceImpl(private val accountService: AccountService, private
role = Role(0, "Admin", "", 32) role = Role(0, "Admin", "", 32)
) )
} }
} }

View File

@ -18,7 +18,6 @@ interface StatusesApiService {
suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status
} }
@Service @Service
class StatsesApiServiceImpl( class StatsesApiServiceImpl(
private val postService: PostService, private val postService: PostService,
@ -28,7 +27,6 @@ class StatsesApiServiceImpl(
) : ) :
StatusesApiService { StatusesApiService {
override suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status { override suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status {
val visibility = when (statusesRequest.visibility) { val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
@ -67,7 +65,6 @@ class StatsesApiServiceImpl(
null null
} }
return Status( return Status(
id = post.id.toString(), id = post.id.toString(),
uri = post.apId, uri = post.apId,

View File

@ -26,11 +26,9 @@ class ExposedOAuth2AuthorizationConsentService(
} }
} }
override fun save(authorizationConsent: AuthorizationConsent?) = runBlocking { override fun save(authorizationConsent: AuthorizationConsent?) = runBlocking {
requireNotNull(authorizationConsent) requireNotNull(authorizationConsent)
transaction.transaction { transaction.transaction {
val singleOrNull = val singleOrNull =
OAuth2AuthorizationConsent.select { OAuth2AuthorizationConsent.select {
OAuth2AuthorizationConsent.registeredClientId OAuth2AuthorizationConsent.registeredClientId
@ -61,7 +59,6 @@ class ExposedOAuth2AuthorizationConsentService(
requireNotNull(registeredClientId) requireNotNull(registeredClientId)
requireNotNull(principalName) requireNotNull(principalName)
transaction.transaction { transaction.transaction {
OAuth2AuthorizationConsent.select { OAuth2AuthorizationConsent.select {
(OAuth2AuthorizationConsent.registeredClientId eq registeredClientId) (OAuth2AuthorizationConsent.registeredClientId eq registeredClientId)
.and(OAuth2AuthorizationConsent.principalName eq principalName) .and(OAuth2AuthorizationConsent.principalName eq principalName)

View File

@ -151,8 +151,6 @@ class ExposedOAuth2AuthorizationService(
override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? = runBlocking { override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? = runBlocking {
requireNotNull(token) requireNotNull(token)
transaction.transaction { transaction.transaction {
when (tokenType?.value) { when (tokenType?.value) {
null -> { null -> {
Authorization.select { Authorization.select {

View File

@ -7,12 +7,10 @@ import java.util.*
@Component @Component
class SecureTokenGeneratorImpl : SecureTokenGenerator { class SecureTokenGeneratorImpl : SecureTokenGenerator {
override fun generate(): String { override fun generate(): String {
val byteArray = ByteArray(16) val byteArray = ByteArray(16)
val secureRandom = SecureRandom() val secureRandom = SecureRandom()
secureRandom.nextBytes(byteArray) secureRandom.nextBytes(byteArray)
return Base64.getUrlEncoder().encodeToString(byteArray) return Base64.getUrlEncoder().encodeToString(byteArray)
} }
} }

View File

@ -1,573 +0,0 @@
package dev.usbharu.hideout.plugins
import com.auth0.jwk.Jwk
import com.auth0.jwk.JwkProvider
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken
import dev.usbharu.hideout.domain.model.hideout.entity.Jwt
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.InvalidRefreshTokenException
import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.api.internal.v1.auth
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.auth.JwtService
import dev.usbharu.hideout.service.core.MetaService
import dev.usbharu.hideout.service.user.UserAuthService
import dev.usbharu.hideout.util.Base64Util
import dev.usbharu.hideout.util.JsonWebKeyUtil
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
import kotlin.test.assertEquals
class SecurityKtTest {
@Test
fun `login ログイン出来るか`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val jwtToken = JwtToken("Token", "RefreshToken")
val userAuthService = mock<UserAuthApiService> {
onBlocking { login(eq("testUser"), eq("password")) } doReturn jwtToken
}
val metaService = mock<MetaService>()
val userQueryService = mock<UserQueryService> {
onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User.of(
id = 1L,
name = "testUser",
domain = "example.com",
screenName = "test",
description = "",
password = "hashedPassword",
inbox = "https://example.com/inbox",
outbox = "https://example.com/outbox",
url = "https://example.com/profile",
publicKey = "",
privateKey = "",
createdAt = Instant.now()
)
}
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService)
}
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("testUser", "password")))
}.apply {
assertEquals(HttpStatusCode.OK, call.response.status)
assertEquals(jwtToken, Config.configData.objectMapper.readValue(call.response.bodyAsText()))
}
}
@Test
fun `login 存在しないユーザーのログインに失敗する`() {
testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
mock<UserAuthService> {
onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false)
}
val metaService = mock<MetaService>()
mock<UserQueryService>()
mock<JwtService>()
val jwkProvider = mock<JwkProvider>()
val userAuthApiService = mock<UserAuthApiService> {
onBlocking { login(anyString(), anyString()) } doThrow InvalidUsernameOrPasswordException()
}
application {
configureStatusPages()
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthApiService)
}
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("InvalidTtestUser", "password")))
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
}
@Test
fun `login 不正なパスワードのログインに失敗する`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val metaService = mock<MetaService>()
val jwkProvider = mock<JwkProvider>()
val userAuthApiService = mock<UserAuthApiService> {
onBlocking { login(anyString(), eq("InvalidPassword")) } doThrow InvalidUsernameOrPasswordException()
}
application {
configureStatusPages()
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthApiService)
}
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("TestUser", "InvalidPassword")))
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーが無いと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(mock())
}
}
client.get("/auth-check").apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーの形式が間違っていると401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Digest dsfjjhogalkjdfmlhaog")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーが空だと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check AuthorizedヘッダーがBeararで空だと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer ")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check 正当なJWTだとアクセスできる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val keyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = keyPair.public as RSAPublicKey
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
val now = Instant.now()
val kid = UUID.randomUUID()
val token = JWT.create()
.withAudience("${Config.configData.url}/users/test")
.withIssuer(Config.configData.url)
.withKeyId(kid.toString())
.withClaim("uid", 123456L)
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<MetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String, Any>>)[0]
)
)
}
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
}.apply {
assertEquals(HttpStatusCode.OK, call.response.status)
assertEquals("Hello 123456", call.response.bodyAsText())
}
}
@Test
fun `auth-check 期限切れのトークンではアクセスできない`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val keyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = keyPair.public as RSAPublicKey
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
val now = Instant.now()
val kid = UUID.randomUUID()
val token = JWT.create()
.withAudience("${Config.configData.url}/users/test")
.withIssuer(Config.configData.url)
.withKeyId(kid.toString())
.withClaim("uid", 123345L)
.withExpiresAt(now.minus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<MetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String, Any>>)[0]
)
)
}
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check issuerが間違っているとアクセスできない`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val keyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = keyPair.public as RSAPublicKey
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
val now = Instant.now()
val kid = UUID.randomUUID()
val token = JWT.create()
.withAudience("${Config.configData.url}/users/test")
.withIssuer("https://example.com")
.withKeyId(kid.toString())
.withClaim("uid", 12345L)
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<MetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String, Any>>)[0]
)
)
}
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check usernameが空だと失敗する`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val keyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = keyPair.public as RSAPublicKey
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
val now = Instant.now()
val kid = UUID.randomUUID()
val token = JWT.create()
.withAudience("${Config.configData.url}/users/test")
.withIssuer(Config.configData.url)
.withKeyId(kid.toString())
.withClaim("uid", null as Long?)
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<MetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String, Any>>)[0]
)
)
}
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check usernameが存在しないと失敗する`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val keyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = keyPair.public as RSAPublicKey
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
val now = Instant.now()
val kid = UUID.randomUUID()
val token = JWT.create()
.withAudience("${Config.configData.url}/users/test")
.withIssuer(Config.configData.url)
.withKeyId(kid.toString())
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<MetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String, Any>>)[0]
)
)
}
application {
configureSerialization()
configureSecurity(jwkProvider, metaService)
routing {
auth(mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `refresh-token リフレッシュトークンが正当だとトークンを発行する`() = testApplication {
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
environment {
config = ApplicationConfig("empty.conf")
}
val jwtService = mock<UserAuthApiService> {
onBlocking { refreshToken(any()) }.doReturn(JwtToken("token", "refreshToken2"))
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(jwtService)
}
}
client.post("/refresh-token") {
header("Content-Type", "application/json")
setBody(Config.configData.objectMapper.writeValueAsString(RefreshToken("refreshToken")))
}.apply {
assertEquals(HttpStatusCode.OK, call.response.status)
}
}
@Test
fun `refresh-token リフレッシュトークンが不正だと失敗する`() = testApplication {
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
environment {
config = ApplicationConfig("empty.conf")
}
val jwtService = mock<UserAuthApiService> {
onBlocking { refreshToken(any()) } doThrow InvalidRefreshTokenException("Invalid Refresh Token")
}
application {
configureStatusPages()
configureSerialization()
configureSecurity(mock(), mock())
routing {
auth(jwtService)
}
}
client.post("/refresh-token") {
header("Content-Type", "application/json")
setBody(Config.configData.objectMapper.writeValueAsString(RefreshToken("InvalidRefreshToken")))
}.apply {
assertEquals(HttpStatusCode.BadRequest, call.response.status)
}
}
}

View File

@ -1,134 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class ContentTypeRouteSelectorTest {
@Test
fun `Content-Typeが一つでマッチする`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
routing {
route("/test") {
createChild(ContentTypeRouteSelector(ContentType.Application.Json)).handle {
call.respondText("OK")
}
get {
call.respondText("NG")
}
}
}
}
client.get("/test") {
accept(ContentType.Text.Html)
}.apply {
assertEquals("NG", bodyAsText())
}
client.get("/test") {
accept(ContentType.Application.Json)
}.apply {
assertEquals("OK", bodyAsText())
}
}
@Test
fun `Content-Typeが一つのとき違うとマッチしない`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
routing {
route("/test") {
createChild(ContentTypeRouteSelector(ContentType.Application.Json)).handle {
call.respondText("OK")
}
get {
call.respondText("NG")
}
}
}
}
client.get("/test") {
accept(ContentType.Text.Html)
}.apply {
assertEquals("NG", bodyAsText())
}
}
@Test
fun `Content-Typeがからのときマッチしない`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
routing {
route("/test") {
createChild(ContentTypeRouteSelector()).handle {
call.respondText("OK")
}
get {
call.respondText("NG")
}
}
}
}
client.get("/test") {
accept(ContentType.Text.Html)
}.apply {
assertEquals("NG", bodyAsText())
}
client.get("/test").apply {
assertEquals("NG", bodyAsText())
}
}
@Test
fun `Content-Typeが複数指定されていてマッチする`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
routing {
route("/test") {
createChild(ContentTypeRouteSelector(ContentType.Application.Json, ContentType.Text.Html)).handle {
call.respondText("OK")
}
get {
call.respondText("NG")
}
}
}
}
client.get("/test") {
accept(ContentType.Text.Html)
}.apply {
assertEquals("OK", bodyAsText())
}
client.get("/test") {
accept(ContentType.Application.Json)
}.apply {
assertEquals("OK", bodyAsText())
}
client.get("/test") {
accept(ContentType.Application.Xml)
}.apply {
assertEquals("NG", bodyAsText())
}
}
}

View File

@ -1,104 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.exception.JsonParseException
import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.plugins.configureStatusPages
import dev.usbharu.hideout.service.ap.APService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.user.UserService
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
class InboxRoutingKtTest {
@Test
fun `sharedInboxにGETしたら405が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
configureSerialization()
routing {
inbox(mock(), mock())
}
}
client.get("/inbox").let {
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
}
}
@Test
fun `sharedInboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {
on { verify(any()) } doReturn true
}
val apService = mock<APService> {
on { parseActivity(any()) } doThrow JsonParseException()
}
mock<UserService>()
mock<APUserService>()
application {
configureStatusPages()
configureSerialization()
routing {
inbox(httpSignatureVerifyService, apService)
}
}
client.post("/inbox").let {
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
}
}
@Test
fun `ユーザのinboxにGETしたら405が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
application {
configureSerialization()
routing {
inbox(mock(), mock())
}
}
client.get("/users/test/inbox").let {
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
}
}
@Test
fun `ユーザーのinboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {
on { verify(any()) } doReturn true
}
val apService = mock<APService> {
on { parseActivity(any()) } doThrow JsonParseException()
}
mock<UserService>()
mock<APUserService>()
application {
configureStatusPages()
configureSerialization()
routing {
inbox(httpSignatureVerifyService, apService)
}
}
client.post("/users/test/inbox").let {
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
}
}
}

View File

@ -1,202 +0,0 @@
package dev.usbharu.hideout.routing.activitypub
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ap.Image
import dev.usbharu.hideout.domain.model.ap.Key
import dev.usbharu.hideout.domain.model.ap.Person
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import utils.TestTransaction
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class UsersAPTest {
@Test()
fun `ユーザのURLにAcceptヘッダーをActivityにしてアクセスしたときPersonが返ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val person = Person(
type = emptyList(),
name = "test",
id = "http://example.com/users/test",
preferredUsername = "test",
summary = "test user",
inbox = "http://example.com/users/test/inbox",
outbox = "http://example.com/users/test/outbox",
url = "http://example.com/users/test",
icon = Image(
type = emptyList(),
name = "http://example.com/users/test/icon.png",
mediaType = "image/png",
url = "http://example.com/users/test/icon.png"
),
publicKey = Key(
type = emptyList(),
name = "Public Key",
id = "http://example.com/users/test#pubkey",
owner = "https://example.com/users/test",
publicKeyPem = "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----"
)
)
person.context = listOf("https://www.w3.org/ns/activitystreams")
val apUserService = mock<APUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person
}
application {
configureSerialization()
routing {
usersAP(apUserService, mock(), mock(), TestTransaction)
}
}
client.get("/users/test") {
accept(ContentType.Application.Activity)
}.let {
val objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper.configOverride(List::class.java).setSetterInfo(
JsonSetter.Value.forValueNulls(
Nulls.AS_EMPTY
)
)
val actual = it.bodyAsText()
val readValue = objectMapper.readValue<Person>(actual)
assertEquals(person, readValue)
}
}
// @Disabled
@Test()
fun `ユーザのURLにAcceptヘッダーをActivityとJson-LDにしてアクセスしたときPersonが返ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val person = Person(
type = emptyList(),
name = "test",
id = "http://example.com/users/test",
preferredUsername = "test",
summary = "test user",
inbox = "http://example.com/users/test/inbox",
outbox = "http://example.com/users/test/outbox",
url = "http://example.com/users/test",
icon = Image(
type = emptyList(),
name = "http://example.com/users/test/icon.png",
mediaType = "image/png",
url = "http://example.com/users/test/icon.png"
),
publicKey = Key(
type = emptyList(),
name = "Public Key",
id = "http://example.com/users/test#pubkey",
owner = "https://example.com/users/test",
publicKeyPem = "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----"
)
)
person.context = listOf("https://www.w3.org/ns/activitystreams")
val apUserService = mock<APUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person
}
application {
configureSerialization()
routing {
usersAP(apUserService, mock(), mock(), TestTransaction)
}
}
client.get("/users/test") {
accept(ContentType.Application.JsonLd)
accept(ContentType.Application.Activity)
}.let {
val objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper.configOverride(List::class.java).setSetterInfo(
JsonSetter.Value.forValueNulls(
Nulls.AS_EMPTY
)
)
val actual = it.bodyAsText()
val readValue = objectMapper.readValue<Person>(actual)
assertEquals(person, readValue)
}
}
// @Disabled
@Test
fun contentType_Test() {
assertTrue(ContentType.Application.Activity.match("application/activity+json"))
val listOf = listOf(ContentType.Application.JsonLd, ContentType.Application.Activity)
assertTrue(
listOf.find { contentType ->
contentType.match("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
}.let { it != null }
)
assertTrue(
ContentType.Application.JsonLd.match(
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
)
}
@Test
fun ユーザーのURLにAcceptヘッダーをhtmlにしてアクセスしたときはただの文字を返す() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userService = mock<UserQueryService> {
onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User.of(
1L,
"test",
"example.com",
"test",
"",
"hashedPassword",
"https://example.com/inbox",
"https://example.com/outbox",
"https://example.com",
"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
"-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
Instant.now()
)
}
application {
routing {
usersAP(mock(), userService, mock(), TestTransaction)
}
}
client.get("/users/test") {
accept(ContentType.Text.Html)
}.let {
assertEquals(HttpStatusCode.OK, it.status)
assertTrue(it.contentType()?.match(ContentType.Text.Plain) == true)
}
}
}

View File

@ -1,734 +0,0 @@
package dev.usbharu.hideout.routing.api.internal.v1
import com.auth0.jwt.interfaces.Claim
import com.auth0.jwt.interfaces.Payload
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.plugins.configureSecurity
import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.service.api.PostApiService
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
import utils.JsonObjectMapper
import java.time.Instant
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class PostsTest {
@Test
fun 認証情報無しでpostsにGETしたらPUBLICな投稿一覧が返ってくる() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
)
val postService = mock<PostApiService> {
onBlocking {
getAll(
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = isNull()
)
} doReturn posts
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/posts").apply {
assertEquals(HttpStatusCode.OK, status)
assertContentEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun 認証情報ありでpostsにGETすると権限のある投稿が返ってくる() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
),
PostResponse(
id = "1234567",
user = user,
text = "Followers only",
visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/3"
)
)
val postService = mock<PostApiService> {
onBlocking {
getAll(
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = isNotNull()
)
} doReturn posts
}
application {
authentication {
bearer(TOKEN_AUTH) {
authenticate {
JWTPrincipal(payload)
}
}
}
configureSerialization()
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/posts") {
header("Authorization", "Bearer asdkaf")
}.apply {
assertEquals(HttpStatusCode.OK, status)
}
}
@Test
fun `posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val post = PostResponse(
id = "12345",
user = user,
text = "aaa",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
)
val postService = mock<PostApiService> {
onBlocking { getById(any(), anyOrNull()) } doReturn post
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/posts/1").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `認証情報ありでposts id にGETしたら権限のある投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val post = PostResponse(
"12345",
UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "aaa",
visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
)
val postService = mock<PostApiService> {
onBlocking { getById(any(), isNotNull()) } doReturn post
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
application {
configureSerialization()
authentication {
bearer(TOKEN_AUTH) {
authenticate {
JWTPrincipal(payload)
}
}
}
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/posts/1") {
header("Authorization", "Bearer asdkaf")
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `posts-post postsにpostしたら投稿できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
val postService = mock<PostApiService> {
onBlocking { createPost(any(), any()) } doAnswer {
val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0)
val userId = it.getArgument<Long>(1)
PostResponse(
id = "123",
user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
overview = null,
text = argument.text,
createdAt = Instant.now().toEpochMilli(),
visibility = Visibility.PUBLIC,
url = "https://example.com"
)
}
}
application {
authentication {
bearer(TOKEN_AUTH) {
authenticate {
println("aaaaaaaaaaaa")
JWTPrincipal(payload)
}
}
}
routing {
route("/api/internal/v1") {
posts(postService)
}
}
configureSerialization()
}
val post = dev.usbharu.hideout.domain.model.hideout.form.Post("test")
client.post("/api/internal/v1/posts") {
header("Authorization", "Bearer asdkaf")
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(post))
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("https://example.com", headers["Location"])
}
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.form.Post> {
verify(postService).createPost(capture(), any())
assertEquals(dev.usbharu.hideout.domain.model.hideout.form.Post("test"), firstValue)
}
}
@Test
fun `users userId postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
)
val postService = mock<PostApiService> {
onBlocking {
getByUser(
nameOrId = any(),
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = anyOrNull()
)
} doReturn posts
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/1/posts").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users username postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
)
val postService = mock<PostApiService> {
onBlocking {
getByUser(
nameOrId = eq("test1"),
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = anyOrNull()
)
} doReturn posts
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/test1/posts").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users username@domain postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
)
val postService = mock<PostApiService> {
onBlocking {
getByUser(
nameOrId = eq("test1@example.com"),
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = anyOrNull()
)
} doReturn posts
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/test1@example.com/posts").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users @username@domain postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf(
PostResponse(
id = "12345",
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
PostResponse(
id = "123456",
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
)
val postService = mock<PostApiService> {
onBlocking {
getByUser(
nameOrId = eq("@test1@example.com"),
since = anyOrNull(),
until = anyOrNull(),
minId = anyOrNull(),
maxId = anyOrNull(),
limit = anyOrNull(),
userId = anyOrNull()
)
} doReturn posts
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/@test1@example.com/posts").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val post = PostResponse(
id = "123456",
user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
val postService = mock<PostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/test/posts/12345").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users id posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val post = PostResponse(
id = "123456",
user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
val postService = mock<PostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/1/posts/12345").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name posts id にGETしたらUserIdが間違っててもPUBLICな投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val post = PostResponse(
id = "123456",
user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
val postService = mock<PostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/423827849732847/posts/12345").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name posts id にGETしたらuserNameが間違っててもPUBLICな投稿を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val post = PostResponse(
id = "123456",
user = UserResponse(
id = "54321",
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
)
val postService = mock<PostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
}
}
}
client.get("/api/internal/v1/users/invalidUserName/posts/12345").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
}

View File

@ -1,692 +0,0 @@
package dev.usbharu.hideout.routing.api.internal.v1
import com.auth0.jwt.interfaces.Claim
import com.auth0.jwt.interfaces.Payload
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.Acct
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.form.UserCreate
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.plugins.configureSecurity
import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.service.api.UserApiService
import dev.usbharu.hideout.service.user.UserService
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
import utils.JsonObjectMapper
import java.time.Instant
import kotlin.test.assertEquals
@Suppress("LargeClass")
class UsersTest {
@Test
fun `users にGETするとユーザー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val users = listOf(
UserResponse(
"12345",
"test1",
"example.com",
"test",
"",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"12343",
"tes2",
"example.com",
"test",
"",
"https://example.com/tes2",
Instant.now().toEpochMilli()
),
)
val userService = mock<UserApiService> {
onBlocking { findAll(anyOrNull(), anyOrNull()) } doReturn users
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userService)
}
}
}
client.get("/api/internal/v1/users").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(users, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users にPOSTすると新規ユーザー作成ができる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userCreateDto = UserCreate("test", "XXXXXXX")
val userService = mock<UserService> {
onBlocking { usernameAlreadyUse(any()) } doReturn false
onBlocking { createLocalUser(any()) } doReturn User.of(
id = 12345,
name = "test",
domain = "example.com",
screenName = "testUser",
description = "test user",
password = "XXXXXXX",
inbox = "https://example.com/inbox",
outbox = "https://example.com/outbox",
url = "https://example.com",
publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
privateKey = "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
createdAt = Instant.now()
)
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(userService, mock())
}
}
}
client.post("/api/internal/v1/users") {
contentType(ContentType.Application.Json)
setBody(JsonObjectMapper.objectMapper.writeValueAsString(userCreateDto))
}.apply {
assertEquals(HttpStatusCode.Created, status)
assertEquals(
"${Config.configData.url}/api/internal/v1/users/${userCreateDto.username}",
headers["Location"]
)
}
}
@Test
fun `users 既にユーザー名が使用されているときはBadRequestが帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userCreateDto = UserCreate("test", "XXXXXXX")
val userService = mock<UserService> {
onBlocking { usernameAlreadyUse(any()) } doReturn true
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(userService, mock())
}
}
}
client.post("/api/internal/v1/users") {
contentType(ContentType.Application.Json)
setBody(JsonObjectMapper.objectMapper.writeValueAsString(userCreateDto))
}.apply {
assertEquals(HttpStatusCode.BadRequest, status)
}
}
@Test
fun `users name にGETしたらユーザーを取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userResponse = UserResponse(
"1234",
"test1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
val userApiService = mock<UserApiService> {
onBlocking { findByAcct(any()) } doReturn userResponse
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users id にGETしたらユーザーを取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userResponse = UserResponse(
"1234",
"test1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
val userApiService = mock<UserApiService> {
onBlocking { findById(any()) } doReturn userResponse
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/1234").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name@domain にGETしたらユーザーを取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userResponse = UserResponse(
"1234",
"test1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
val userApiService = mock<UserApiService> {
onBlocking { findByAcct(any()) } doReturn userResponse
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users @name@domain にGETしたらユーザーを取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userResponse = UserResponse(
"1234",
"test1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
val userApiService = mock<UserApiService> {
onBlocking { findByAcct(any()) } doReturn userResponse
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowersByAcct(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1/followers").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name@domain followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowersByAcct(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/@test1@example.com/followers").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users id followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowers(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/1234/followers").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name followers に認証情報ありでGETしたらフォローできる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
val userApiService = mock<UserApiService> {
onBlocking { findByAcct(any()) } doReturn UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
onBlocking { follow(any<Acct>(), eq(1234)) } doReturn true
}
application {
configureSerialization()
authentication {
bearer(TOKEN_AUTH) {
authenticate {
JWTPrincipal(payload)
}
}
}
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.post("/api/internal/v1/users/test1/followers") {
header(HttpHeaders.Authorization, "Bearer test")
}.apply {
assertEquals(HttpStatusCode.OK, status)
}
}
@Test
fun `users name followers に認証情報ありでGETしたらフォロー処理受付になることもある`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
val userApiService = mock<UserApiService> {
onBlocking { findByAcct(any()) } doReturn UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
onBlocking { follow(any<Acct>(), eq(1234)) } doReturn false
}
application {
configureSerialization()
authentication {
bearer(TOKEN_AUTH) {
authenticate {
JWTPrincipal(payload)
}
}
}
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.post("/api/internal/v1/users/test1/followers") {
header(HttpHeaders.Authorization, "Bearer test")
}.apply {
assertEquals(HttpStatusCode.Accepted, status)
}
}
@Test
fun `users id followers に認証情報ありでGETしたらフォロー処理受付になることもある`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val claim = mock<Claim> {
on { asLong() } doReturn 1234
}
val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim
}
val userApiService = mock<UserApiService> {
onBlocking { findById(any()) } doReturn UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
onBlocking { follow(eq(1235), eq(1234)) } doReturn false
}
application {
configureSerialization()
authentication {
bearer(TOKEN_AUTH) {
authenticate {
JWTPrincipal(payload)
}
}
}
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.post("/api/internal/v1/users/1235/followers") {
header(HttpHeaders.Authorization, "Bearer test")
}.apply {
assertEquals(HttpStatusCode.Accepted, status)
}
}
@Test
fun `users name following にGETしたらフォロイー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowingsByAcct(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1/following").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users name@domain following にGETしたらフォロイー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowingsByAcct(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/test1@domain/following").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
@Test
fun `users id following にGETしたらフォロイー一覧を取得できる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val followers = listOf(
UserResponse(
"1235",
"follower1",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
),
UserResponse(
"1236",
"follower2",
"example.com",
"test",
"test User",
"https://example.com/test",
Instant.now().toEpochMilli()
)
)
val userApiService = mock<UserApiService> {
onBlocking { findFollowings(any()) } doReturn followers
}
application {
configureSerialization()
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
users(mock(), userApiService)
}
}
}
client.get("/api/internal/v1/users/1234/following").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
}
}
}