Merge pull request #175 from usbharu/feature/mastodon-api-int-test

Feature/mastodon api int test
This commit is contained in:
usbharu 2023-11-30 00:55:32 +09:00 committed by GitHub
commit 3fd051d014
20 changed files with 644 additions and 21 deletions

View File

@ -1,6 +1,6 @@
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
import kotlin.math.max
val ktor_version: String by project val ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
@ -13,7 +13,7 @@ plugins {
kotlin("jvm") version "1.8.21" kotlin("jvm") version "1.8.21"
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("org.springframework.boot") version "3.1.3" id("org.springframework.boot") version "3.2.0"
kotlin("plugin.spring") version "1.8.21" kotlin("plugin.spring") version "1.8.21"
id("org.openapi.generator") version "7.0.1" id("org.openapi.generator") version "7.0.1"
id("org.jetbrains.kotlinx.kover") version "0.7.4" id("org.jetbrains.kotlinx.kover") version "0.7.4"
@ -50,10 +50,6 @@ val integrationTest = task<Test>("integrationTest") {
shouldRunAfter("test") shouldRunAfter("test")
useJUnitPlatform() useJUnitPlatform()
testLogging {
events("passed")
}
} }
tasks.check { dependsOn(integrationTest) } tasks.check { dependsOn(integrationTest) }
@ -61,8 +57,8 @@ tasks.check { dependsOn(integrationTest) }
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
val cpus = Runtime.getRuntime().availableProcessors() val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = max(1, cpus - 1) // maxParallelForks = max(1, cpus - 1)
setForkEvery(4) // setForkEvery(4)
doFirst { doFirst {
jvmArgs = arrayOf( jvmArgs = arrayOf(
"--add-opens", "java.base/java.lang=ALL-UNNAMED" "--add-opens", "java.base/java.lang=ALL-UNNAMED"
@ -180,6 +176,7 @@ dependencies {
implementation("org.postgresql:postgresql:42.6.0") implementation("org.postgresql:postgresql:42.6.0")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0") implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
implementation("org.apache.tika:tika-core:2.9.1") implementation("org.apache.tika:tika-core:2.9.1")
implementation("org.apache.tika:tika-parsers:2.9.1")
implementation("net.coobird:thumbnailator:0.4.20") implementation("net.coobird:thumbnailator:0.4.20")
implementation("org.bytedeco:javacv-platform:1.5.9") implementation("org.bytedeco:javacv-platform:1.5.9")
implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-core")
@ -206,6 +203,9 @@ dependencies {
intTestImplementation("org.springframework.boot:spring-boot-starter-test") intTestImplementation("org.springframework.boot:spring-boot-starter-test")
intTestImplementation("org.springframework.security:spring-security-test") intTestImplementation("org.springframework.security:spring-security-test")
intTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
intTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
} }

View File

@ -1,5 +1,5 @@
ktor_version=2.3.0 ktor_version=2.3.0
kotlin_version=1.8.21 kotlin_version=1.9.21
logback_version=1.4.6 logback_version=1.4.6
kotlin.code.style=official kotlin.code.style=official
exposed_version=0.44.0 exposed_version=0.44.0

View File

@ -1,6 +1,8 @@
package activitypub.inbox package activitypub.inbox
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -92,4 +94,13 @@ class InboxTest {
@Bean @Bean
fun testTransaction() = TestTransaction fun testTransaction() = TestTransaction
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -1,6 +1,8 @@
package activitypub.note package activitypub.note
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -178,4 +180,13 @@ class NoteTest {
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } } .andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
.andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } } .andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } }
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -2,6 +2,8 @@ package activitypub.webfinger
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
@ -24,6 +26,7 @@ class WebFingerTest {
@Test @Test
@Sql("/sql/test-user.sql") @Sql("/sql/test-user.sql")
fun `webfinger 存在するユーザーを取得`() { fun `webfinger 存在するユーザーを取得`() {
mockMvc mockMvc
.get("/.well-known/webfinger?resource=acct:test-user@example.com") .get("/.well-known/webfinger?resource=acct:test-user@example.com")
@ -80,4 +83,13 @@ class WebFingerTest {
@Bean @Bean
fun testTransaction(): Transaction = TestTransaction fun testTransaction(): Transaction = TestTransaction
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -0,0 +1,146 @@
package mastodon.account
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.infrastructure.exposedquery.UserQueryServiceImpl
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiTest {
@Autowired
private lateinit var userQueryServiceImpl: UserQueryServiceImpl
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにreadでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにread_accountsでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:accounts")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsVerifyCredentialsGetに匿名でアクセスすると401() {
mockMvc
.get("/api/v1/accounts/verify_credentials")
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostに匿名でPOSTしたらアカウントを作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-1")
param("password", "very-secure-password")
param("email", "test@example.com")
param("agreement", "true")
param("locale", "")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "localhost")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostで必須パラメーター以外を省略しても作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "localhost")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでusernameパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-3")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.andExpect { status { isBadRequest() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでpasswordパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-3")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.andExpect { status { isBadRequest() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,99 @@
package mastodon.apps
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.jetbrains.exposed.sql.select
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class AppTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにformで匿名でappを作成できる() {
mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("client_name", "test-client")
param("redirect_uris", "https://example.com")
param("scopes", "write read")
param("website", "https://example.com")
}
.asyncDispatch()
.andExpect { status { isOk() } }
val app = RegisteredClient
.select { RegisteredClient.clientName eq "test-client" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにjsonで匿名でappを作成できる() {
mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_JSON
content = """{
"client_name": "test-client-2",
"redirect_uris": "https://example.com",
"scopes": "write read",
"website": "https;//example.com"
}"""
}
.asyncDispatch()
.andExpect { status { isOk() } }
val app = RegisteredClient
.select { RegisteredClient.clientName eq "test-client-2" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client-2")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,124 @@
package mastodon.media
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.service.media.MediaDataStore
import dev.usbharu.hideout.core.service.media.MediaSaveRequest
import dev.usbharu.hideout.core.service.media.SuccessSavedMedia
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.mock.web.MockMultipartFile
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.multipart
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MediaTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var mediaDataStore: MediaDataStore
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun メディアをアップロードできる() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "", ""))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_mediaスコープでメディアをアップロードできる() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "", ""))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:media")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "", ""))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,156 @@
package mastodon.status
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class StatusTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun 投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_statusesスコープで投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun 匿名だと401() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun 匿名の場合通常はcsrfが無いので403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
}
.andExpect { status { isForbidden() } }
}
@Test
fun formでも投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("status", "hello")
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@Sql("/sql/test-post.sql")
fun in_reply_to_idを指定したら返信として処理される() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
//language=JSON
content = """{
"status": "hello",
"in_reply_to_id": "1"
}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { jsonPath("\$.in_reply_to_id") { value("1") } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -18,10 +18,11 @@ hideout:
spring: spring:
flyway: flyway:
enabled: false enabled: true
clean-disabled: false
datasource: datasource:
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1" url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
username: "" username: ""
password: password:
data: data:
@ -34,8 +35,8 @@ spring:
console: console:
enabled: true enabled: true
exposed: # exposed:
generate-ddl: true # generate-ddl: true
excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed # excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server: server:
port: 8080 port: 8080

View File

@ -7,4 +7,5 @@
<root level="TRACE"> <root level="TRACE">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.springframework.security" level="TRACE"/>
</configuration> </configuration>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,3 @@
insert into posts (id, user_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id)
VALUES (1, 1, null, 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
'https://users/1/posts/1');

View File

@ -180,7 +180,8 @@ class SecurityConfig {
builder.pattern("/api/v1/instance/**"), builder.pattern("/api/v1/instance/**"),
builder.pattern("/.well-known/**"), builder.pattern("/.well-known/**"),
builder.pattern("/error"), builder.pattern("/error"),
builder.pattern("/nodeinfo/2.0") builder.pattern("/nodeinfo/2.0"),
builder.pattern("/api/v1/accounts")
).permitAll() ).permitAll()
it.requestMatchers( it.requestMatchers(
builder.pattern("/auth/**") builder.pattern("/auth/**")
@ -188,7 +189,11 @@ class SecurityConfig {
it.requestMatchers(builder.pattern("/change-password")).authenticated() it.requestMatchers(builder.pattern("/change-password")).authenticated()
it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials")) it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials"))
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts") .hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
it.anyRequest().permitAll() it.requestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/media"))
.hasAnyAuthority("SCOPE_write", "SCOPE_write:media")
it.requestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/statuses"))
.hasAnyAuthority("SCOPE_write", "SCOPE_write:statuses")
it.anyRequest().authenticated()
} }
http.oauth2ResourceServer { http.oauth2ResourceServer {
it.jwt(Customizer.withDefaults()) it.jwt(Customizer.withDefaults())

View File

@ -13,6 +13,8 @@ import net.coobird.thumbnailator.Thumbnails
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.awt.Color
import java.awt.image.BufferedImage
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
@ -57,7 +59,14 @@ class ImageMediaProcessService(private val imageMediaProcessorConfiguration: Ima
filePath: Path, filePath: Path,
thumbnails: Path? thumbnails: Path?
): ProcessedMediaPath = withContext(Dispatchers.IO + MDCContext()) { ): ProcessedMediaPath = withContext(Dispatchers.IO + MDCContext()) {
val bufferedImage = ImageIO.read(filePath.inputStream()) val read = ImageIO.read(filePath.inputStream())
val bufferedImage = BufferedImage(read.width, read.height, BufferedImage.TYPE_INT_RGB)
val graphics = bufferedImage.createGraphics()
graphics.drawImage(read, 0, 0, Color.BLACK, null)
val tempFileName = UUID.randomUUID().toString() val tempFileName = UUID.randomUUID().toString()
val tempFile = Files.createTempFile(tempFileName, "tmp") val tempFile = Files.createTempFile(tempFileName, "tmp")
@ -67,9 +76,15 @@ class ImageMediaProcessService(private val imageMediaProcessorConfiguration: Ima
tempThumbnailFile.outputStream().use { tempThumbnailFile.outputStream().use {
val write = ImageIO.write( val write = ImageIO.write(
if (thumbnails != null) { if (thumbnails != null) {
Thumbnails.of(thumbnails.toFile()).size(width, height).asBufferedImage() Thumbnails.of(thumbnails.toFile())
.size(width, height)
.imageType(BufferedImage.TYPE_INT_RGB)
.asBufferedImage()
} else { } else {
Thumbnails.of(bufferedImage).size(width, height).asBufferedImage() Thumbnails.of(bufferedImage)
.size(width, height)
.imageType(BufferedImage.TYPE_INT_RGB)
.asBufferedImage()
}, },
convertType, convertType,
it it

View File

@ -65,7 +65,9 @@ class PostServiceImpl(
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility, visibility = post.visibility,
url = "${user.url}/posts/$id", url = "${user.url}/posts/$id",
mediaIds = post.mediaIds mediaIds = post.mediaIds,
replyId = post.repolyId,
repostId = post.repostId,
) )
return internalCreate(createPost, isLocal) return internalCreate(createPost, isLocal)
} }

View File

@ -13,6 +13,7 @@ import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest
import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility
import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility
import dev.usbharu.hideout.mastodon.service.account.AccountService import dev.usbharu.hideout.mastodon.service.account.AccountService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant
@ -38,12 +39,14 @@ class StatsesApiServiceImpl(
statusesRequest: StatusesRequest, statusesRequest: StatusesRequest,
userId: Long userId: Long
): Status = transaction.transaction { ): Status = transaction.transaction {
logger.debug("START create post by mastodon api. {}", statusesRequest)
val post = postService.createLocal( val post = postService.createLocal(
PostCreateDto( PostCreateDto(
text = statusesRequest.status.orEmpty(), text = statusesRequest.status.orEmpty(),
overview = statusesRequest.spoiler_text, overview = statusesRequest.spoiler_text,
visibility = statusesRequest.visibility.toPostVisibility(), visibility = statusesRequest.visibility.toPostVisibility(),
repolyId = statusesRequest.in_reply_to_id?.toLongOrNull(), repolyId = statusesRequest.in_reply_to_id?.toLong(),
userId = userId, userId = userId,
mediaIds = statusesRequest.media_ids.map { it.toLong() } mediaIds = statusesRequest.media_ids.map { it.toLong() }
) )
@ -91,4 +94,8 @@ class StatsesApiServiceImpl(
editedAt = null, editedAt = null,
) )
} }
companion object {
private val logger = LoggerFactory.getLogger(StatusesApiService::class.java)
}
} }

View File

@ -0,0 +1,19 @@
package dev.usbharu.hideout.core.service.media
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.io.path.toPath
class ApatcheTikaFileTypeDeterminationServiceTest {
@Test
fun png() {
val apatcheTikaFileTypeDeterminationService = ApatcheTikaFileTypeDeterminationService()
val mimeType = apatcheTikaFileTypeDeterminationService.fileType(
String.javaClass.classLoader.getResource("400x400.png").toURI().toPath(), "400x400.png"
)
assertThat(mimeType.type).isEqualTo("image")
assertThat(mimeType.subtype).isEqualTo("png")
}
}

View File

@ -0,0 +1,11 @@
package dev.usbharu.hideout.core.service.media
import org.junit.jupiter.api.Test
class MediaServiceImplTest {
@Test
fun png画像をアップロードできる() {
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB