diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index c100e253..aa68bd97 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -1,16 +1,14 @@ -name: PullRequest Merge Check - +name: pull-request-merge-check.yml on: pull_request: - paths-ignore: - - 'owl/**' branches: - "develop" types: - - opened # default - - reopened # default - - synchronize # default - - ready_for_review # 必要 + - opened + - reopened + - synchronize + - ready_for_review + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -22,10 +20,43 @@ permissions: pull-requests: write jobs: - setup: - name: Setup + change: if: github.event.pull_request.draft == false runs-on: ubuntu-latest + outputs: + core: ${{ steps.filter.outputs.core }} + mastodon: ${{ steps.filter.outputs.mastodon }} + activitypub: ${{ steps.filter.outputs.ap }} + owl: $${{ steps.filter.outputs.owl }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Check Changes + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + core: + - 'hideout-core/**' + - 'libs.versions.toml' + ap: + - 'hideout-activitypub/**' + - 'libs.versions.toml' + mastodon: + - 'hideout-mastodon/**' + - 'libs.versions.toml' + owl: + - 'owl/**' + - 'libs.versions.toml' + + hideout-core-setup: + needs: + - change + if: github.event.pull_request.draft == false && needs.change.outputs.core == 'true' + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -48,11 +79,100 @@ jobs: gradle-home-cache-cleanup: true - name: Build - run: ./gradlew classes --no-daemon + run: ./gradlew :hideout-core:classes --no-daemon - unit-test: - name: Unit Test - needs: [ setup ] + hideout-mastodon-setup: + needs: + - change + if: github.event.pull_request.draft == false && needs.change.outputs.mastodon == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Build + run: ./gradlew :hideout-mastodon:classes --no-daemon + + hideout-activitypub-setup: + needs: + - change + if: github.event.pull_request.draft == false && needs.change.outputs.activitypub == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Build + run: ./gradlew :hideout-activitypub:classes --no-daemon + + owl-setup: + needs: + - change + if: github.event.pull_request.draft == false && needs.change.outputs.owl == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Build + run: ./owl/gradlew :owl:classes --no-daemon + + hideout-core-unit-test: + needs: + - hideout-core-setup + - change + if: github.event.pull_request.draft == false && needs.change.outputs.core == 'true' runs-on: ubuntu-latest steps: - name: Checkout @@ -75,39 +195,30 @@ jobs: - name: Unit Test run: ./hideout-core/gradlew :hideout-core:koverXmlReport - - name: Add coverage report to PR - if: always() - id: kover - uses: madrapps/jacoco-report@v1.7.0 - with: - paths: | - ${{ github.workspace }}/hideout-core/build/reports/kover/report.xml - token: ${{ secrets.GITHUB_TOKEN }} - title: Code Coverage - update-comment: true - min-coverage-overall: 50 - min-coverage-changed-files: 80 - - name: JUnit Test Report uses: mikepenz/action-junit-report@v4 with: report_paths: '**/TEST-*.xml' + check_name: 'hideout-core JUnit Test Report' - - name: Verify Coverage - run: ./hideout-core/gradlew :hideout-core:koverVerify + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: 'hideout-core.xml' + path: 'hideout-core/build/reports/kover/hideout-core.xml' - lint: - name: Lint - needs: [ setup ] + + hideout-mastodon-unit-test: + needs: + - hideout-mastodon-setup + - change + if: github.event.pull_request.draft == false && needs.change.outputs.mastodon == 'true' runs-on: ubuntu-latest - permissions: - contents: write steps: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} - token: '${{ secrets.PAT }}' + token: ${{ secrets.PAT }} - name: Set up JDK 21 uses: actions/setup-java@v4 @@ -121,8 +232,164 @@ jobs: cache-read-only: false gradle-home-cache-cleanup: true - - name: Build with Gradle - run: ./gradlew :hideout-core:detektMain :hideout-mastodon:detektMain + - name: Unit Test + run: ./hideout-mastodon/gradlew :hideout-mastodon:koverXmlReport + + - name: JUnit Test Report + uses: mikepenz/action-junit-report@v4 + with: + report_paths: '**/TEST-*.xml' + check_name: 'hideout-mastodon JUnit Test Report' + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: 'hideout-mastodon.xml' + path: 'hideout-mastodon/build/reports/kover/hideout-mastodon.xml' + + hideout-activitypub-unit-test: + needs: + - hideout-activitypub-setup + - change + if: github.event.pull_request.draft == false && needs.change.outputs.activitypub == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Unit Test + run: ./hideout-activitypub/gradlew :hideout-activitypub:koverXmlReport + + - name: JUnit Test Report + uses: mikepenz/action-junit-report@v4 + with: + report_paths: '**/TEST-*.xml' + check_name: 'hideout-activitypub JUnit Test Report' + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: 'hideout-activitypub.xml' + path: 'hideout-activitypub/build/reports/kover/hideout-activitypub.xml' + + owl-unit-test: + needs: + - owl-setup + - change + if: github.event.pull_request.draft == false && needs.change.outputs.owl == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Unit Test + run: ./owl/gradlew :owl:koverXmlReport + + - name: JUnit Test Report + uses: mikepenz/action-junit-report@v4 + with: + report_paths: '**/TEST-*.xml' + check_name: 'owl JUnit Test Report' + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: 'owl.xml' + path: 'owl/build/reports/kover/owl.xml' + + coverage: + if: always() && (needs.change.outputs.core == 'true' || needs.change.outputs.activitypub == 'true' || needs.change.outputs.mastodon == 'true' || needs.change.outputs.owl == 'true') + needs: + - change + - hideout-core-unit-test + - hideout-mastodon-unit-test + - hideout-activitypub-unit-test + - owl-unit-test + runs-on: ubuntu-latest + steps: + - name: Download Coverage Report + uses: actions/download-artifact@v4 + with: + path: 'hideout-core/build/reports/kover' + + - name: Report Coverage + uses: madrapps/jacoco-report@v1.7.0 + with: + paths: | + ${{ github.workspace }}/hideout-core/build/reports/kover/hideout-core.xml/hideout-core.xml, + ${{ github.workspace }}/hideout-core/build/reports/kover/hideout-mastodon.xml/hideout-mastodon.xml, + ${{ github.workspace }}/hideout-core/build/reports/kover/hideout-activitypub.xml/hideout-activitypub.xml + ${{ github.workspace }}/hideout-core/build/reports/kover/owl.xml/owl.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: Code Coverage + update-comment: true + min-coverage-overall: 50 + min-coverage-changed-files: 80 + + lint: + if: always() && (needs.change.outputs.core == 'true' || needs.change.outputs.activitypub == 'true' || needs.change.outputs.mastodon == 'true' || needs.change.outputs.owl == 'true') + needs: + - change + - hideout-core-setup + - hideout-mastodon-setup + - hideout-activitypub-setup + - owl-setup + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.PAT }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Lint + run: ./gradlew :hideout-core:detektMain :hideout-mastodon:detektMain :hideout-activitypub:detektMain + + - name: owl Lint + if: always() + run: ./owl/gradlew :owl:detektMain - name: Auto Commit if: ${{ always() }} diff --git a/.gitignore b/.gitignore index e3b32bdc..99c1c155 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ out/ /hideout-mastodon/.kotlin/sessions/ /http-client.private.env.json /logs/ +/hideout-mastodon/logs/ +/hideout-mastodon/files/ diff --git a/hideout-core/build.gradle.kts b/hideout-core/build.gradle.kts index a9247b41..b488437b 100644 --- a/hideout-core/build.gradle.kts +++ b/hideout-core/build.gradle.kts @@ -211,12 +211,18 @@ kover { reports { verify { rule { - bound{ + bound { minValue = 50 coverageUnits = CoverageUnit.INSTRUCTION } } } + total { + xml { + title = "Hideout Core" + xmlFile = file("$buildDir/reports/kover/hideout-core.xml") + } + } filters { excludes { annotatedBy("org.springframework.context.annotation.Configuration") @@ -229,6 +235,7 @@ kover { packages("org.jetbrains") } } + } } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt index e59663be..53613621 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt @@ -16,7 +16,10 @@ package dev.usbharu.hideout.core.application.exception +import dev.usbharu.hideout.core.domain.model.support.principal.Principal + class PermissionDeniedException : RuntimeException { + constructor(principal: Principal) : super("Permission Denied $principal") constructor() : super() constructor(message: String?) : super(message) constructor(message: String?, cause: Throwable?) : super(message, cause) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt index e97209fc..b6682c64 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt @@ -34,7 +34,8 @@ class UserGetFilterApplicationService(private val filterRepository: FilterReposi ) { override suspend fun internalExecute(command: GetFilter, principal: LocalUser): Filter { val filter = - filterRepository.findByFilterId(FilterId(command.filterId)) ?: throw IllegalArgumentException("Not Found") + filterRepository.findByFilterId(FilterId(command.filterId)) + ?: throw IllegalArgumentException("Filter ${command.filterId} not found.") if (filter.userDetailId != principal.userDetailId) { throw PermissionDeniedException() } diff --git a/hideout-mastodon/build.gradle.kts b/hideout-mastodon/build.gradle.kts index 2265937b..37094786 100644 --- a/hideout-mastodon/build.gradle.kts +++ b/hideout-mastodon/build.gradle.kts @@ -1,3 +1,4 @@ +import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import org.openapitools.generator.gradle.plugin.tasks.GenerateTask plugins { @@ -6,6 +7,7 @@ plugins { alias(libs.plugins.spring.boot) alias(libs.plugins.kotlin.spring) alias(libs.plugins.detekt) + alias(libs.plugins.kover) } @@ -60,9 +62,14 @@ dependencies { implementation(libs.bundles.coroutines) testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testImplementation(libs.bundles.spring.boot.oauth2) testImplementation(libs.kotlin.junit) testImplementation(libs.coroutines.test) testImplementation(libs.h2db) + testImplementation(libs.flyway.core) + testImplementation(libs.http.signature) + testRuntimeOnly(libs.flyway.postgresql) } @@ -126,6 +133,53 @@ configurations.matching { it.name == "detekt" }.all { } } +project.gradle.taskGraph.whenReady { + if (this.hasTask(":koverGenerateArtifact")) { + val task = this.allTasks.find { it.name == "test" } + val verificationTask = task as VerificationTask + verificationTask.ignoreFailures = true + } +} + +kover { + currentProject { + sources { + + + } + } + + reports { + verify { + rule { + bound { + minValue = 50 + coverageUnits = CoverageUnit.INSTRUCTION + } + } + } + total { + xml { + title = "Hideout Mastodon" + xmlFile = file("$buildDir/reports/kover/hideout-mastodon.xml") + } + } + filters { + excludes { + annotatedBy("org.springframework.context.annotation.Configuration") + annotatedBy("org.springframework.boot.context.properties.ConfigurationProperties") + packages( + "dev.usbharu.hideout.controller.mastodon.generated", + "dev.usbharu.hideout.domain.mastodon.model.generated" + ) + packages("org.springframework") + packages("org.jetbrains") + } + } + + } +} + tasks.withType { exclude("**/generated/**") doFirst { diff --git a/hideout-mastodon/gradlew b/hideout-mastodon/gradlew old mode 100644 new mode 100755 diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt index 4961d383..53ed42ff 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt @@ -33,7 +33,7 @@ class DeleteFilterV1ApplicationService(private val filterRepository: FilterRepos ) { override suspend fun internalExecute(command: DeleteFilterV1, principal: LocalUser) { val filter = filterRepository.findByFilterKeywordId(FilterKeywordId(command.filterKeywordId)) - ?: throw IllegalArgumentException("Filter ${command.filterKeywordId} not found") + ?: throw IllegalArgumentException("Filter ${command.filterKeywordId} by KeywordId not found") if (principal.userDetailId != filter.userDetailId) { throw PermissionDeniedException() } diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt index 6f485361..25e7f658 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt @@ -39,7 +39,7 @@ class GetFilterV1ApplicationService(private val filterRepository: FilterReposito ?: throw IllegalArgumentException("Filter ${command.filterKeywordId} not found") if (filter.userDetailId != principal.userDetailId) { - throw PermissionDeniedException() + throw PermissionDeniedException(principal) } val filterKeyword = filter.filterKeywords.find { it.id.id == command.filterKeywordId } diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt index 1734be87..8e92209e 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt @@ -84,6 +84,7 @@ class SpringFilterApi( account -> FilterContext.ACCOUNT } }.toSet() + val principal = principalContextHolder.getPrincipal() val filter = userRegisterFilterApplicationService.execute( RegisterFilter( v1FilterPostRequest.phrase, @@ -91,12 +92,12 @@ class SpringFilterApi( FilterAction.WARN, setOf(RegisterFilterKeyword(v1FilterPostRequest.phrase, filterMode)) ), - principalContextHolder.getPrincipal() + principal ) return ResponseEntity.ok( getFilterV1ApplicationService.execute( GetFilterV1(filter.filterKeywords.first().id), - principalContextHolder.getPrincipal() + principal ) ) } diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt index d5bed541..3834f11e 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt @@ -21,9 +21,12 @@ import dev.usbharu.hideout.core.application.media.UploadMediaApplicationService import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder import dev.usbharu.hideout.mastodon.interfaces.api.generated.MediaApi import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller import org.springframework.web.multipart.MultipartFile +import org.springframework.web.server.ResponseStatusException import java.nio.file.Files @Controller @@ -37,6 +40,11 @@ class SpringMediaApi( description: String?, focus: String?, ): ResponseEntity { + if (file.size == 0L) { + logger.warn("File is empty.") + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty.") + } + val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") Files.newOutputStream(tempFile).use { outputStream -> @@ -73,4 +81,8 @@ class SpringMediaApi( ) ) } + + companion object { + private val logger = LoggerFactory.getLogger(SpringMediaApi::class.java) + } } diff --git a/hideout-mastodon/src/test/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImplTest.kt b/hideout-mastodon/src/test/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImplTest.kt index da7ef30c..abf5bea5 100644 --- a/hideout-mastodon/src/test/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImplTest.kt +++ b/hideout-mastodon/src/test/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImplTest.kt @@ -6,6 +6,8 @@ import dev.usbharu.hideout.core.domain.model.support.acct.Acct import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId import kotlinx.coroutines.test.runTest +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -30,4 +32,13 @@ class StatusQueryServiceImplTest { assertNull(status) } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } } \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiPaginationTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiPaginationTest.kt new file mode 100644 index 00000000..b2057786 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiPaginationTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.account + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import org.assertj.core.api.Assertions.assertThat +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.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +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.get +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/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/accounts/test-accounts-statuses.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class AccountApiPaginationTest { + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @Test + fun `apiV1AccountsIdStatusesGet 投稿を取得できる`() { + val content = mockMvc + .get("/api/v1/accounts/1/statuses"){ + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { header { string("Link","; rel=\"next\", ; rel=\"prev\"") } } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("100") + assertThat(value.last().id).isEqualTo("81") + assertThat(value).size().isEqualTo(20) + } + + @Test + fun `apiV1AccountsIdStatusesGet 結果が0件のときはLinkヘッダーがない`() { + val content = mockMvc + .get("/api/v1/accounts/1/statuses?min_id=100"){ + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { header { doesNotExist("Link") } } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + + assertThat(value).isEmpty() + } + + @Test + fun `apiV1AccountsIdStatusesGet maxIdを指定して取得`() { + val content = mockMvc + .get("/api/v1/accounts/1/statuses?max_id=100"){ + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { header { string("Link","; rel=\"next\", ; rel=\"prev\"") } } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("99") + assertThat(value.last().id).isEqualTo("80") + assertThat(value).size().isEqualTo(20) + } + + @Test + fun `apiV1AccountsIdStatusesGet minIdを指定して取得`() { + val content = mockMvc + .get("/api/v1/accounts/1/statuses?min_id=1"){ + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { header { string("Link","; rel=\"next\", ; rel=\"prev\"") } } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("21") + assertThat(value.last().id).isEqualTo("2") + assertThat(value).size().isEqualTo(20) + } + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiTest.kt new file mode 100644 index 00000000..c1e5c1be --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/account/AccountApiTest.kt @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.account + +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.infrastructure.exposedrepository.ExposedRelationshipRepository +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +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.csrf +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/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class AccountApiTest { + + @Autowired + private lateinit var actorRepository: ActorRepository + + @Autowired + lateinit var relationshipRepository: ExposedRelationshipRepository + + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(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(csrf()) + } + .asyncDispatch() + .andExpect { status { isFound() } } + + actorRepository.findByNameAndDomain("api-test-user-1", "example.com") + } + + @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(csrf()) + } + .asyncDispatch() + .andExpect { status { isFound() } } + + actorRepository.findByNameAndDomain("api-test-user-2", "example.com") + } + + @Test + @WithAnonymousUser + fun apiV1AccountsPostでusernameパラメーターを省略したら400() = runTest { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("password", "api-test-user-3") + with(csrf()) + } + .andDo { print() } + .andExpect { status { isUnprocessableEntity() } } + } + + @Test + @WithAnonymousUser + fun apiV1AccountsPostでpasswordパラメーターを省略したら400() = runTest { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("username", "api-test-user-4") + with(csrf()) + } + .andExpect { status { isUnprocessableEntity() } } + } + + @Test + @Disabled("JSONでも作れるようにするため") + @WithAnonymousUser + fun apiV1AccountsPostでJSONで作ろうとしても400() { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_JSON + content = """{"username":"api-test-user-5","password":"very-very-secure-password"}""" + with(csrf()) + } + .andExpect { status { isUnsupportedMediaType() } } + } + + @Test + @WithAnonymousUser + fun apiV1AccountsPostにCSRFトークンは必要() { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("username", "api-test-user-2") + param("password", "very-secure-password") + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdGet 匿名でアカウント情報を取得できる`() { + mockMvc + .get("/api/v1/accounts/1") + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdFollowPost write_follows権限でPOSTでフォローできる`() { + mockMvc + .post("/api/v1/accounts/2/follow") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:follows"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdFollowPost write権限でPOSTでフォローできる`() { + mockMvc + .post("/api/v1/accounts/2/follow") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdFollowPost read権限でだと403`() { + mockMvc + .post("/api/v1/accounts/2/follow") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AAccountsIdFollowPost 匿名だと401`() { + mockMvc + .post("/api/v1/accounts/2/follow") { + contentType = MediaType.APPLICATION_JSON + with(csrf()) + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AAccountsIdFollowPost 匿名の場合通常csrfトークンは持ってないので403`() { + mockMvc + .post("/api/v1/accounts/2/follow") { + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1AccountsRelationshipsGet 匿名だと401`() { + mockMvc + .get("/api/v1/accounts/relationships") + .andExpect { status { isUnauthorized() } } + } + + @Test + fun `apiV1AccountsRelationshipsGet read_follows権限を持っていたら取得できる`() { + mockMvc + .get("/api/v1/accounts/relationships") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:follows"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsRelationshipsGet read権限を持っていたら取得できる`() { + mockMvc + .get("/api/v1/accounts/relationships") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsRelationshipsGet write権限だと403`() { + mockMvc + .get("/api/v1/accounts/relationships") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @Sql("/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql") + fun `apiV1AccountsIdFollowPost フォローできる`() = runTest { + mockMvc + .post("/api/v1/accounts/3733363/follow") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "37335363") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + + + val alreadyFollow = relationshipRepository.findByActorIdAndTargetId(ActorId(3733363),ActorId(37335363))?.following + + + assertThat(alreadyFollow).isTrue() + } + + @Test + fun `apiV1AccountsIdMutePost write権限でミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdMutePost write_mutes権限でミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdMutePost read権限だと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdMutePost 匿名だと401`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(csrf()) + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdMutePost csrfトークンがないと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost write権限でアンミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost write_mutes権限でアンミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost read権限だと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdUnmutePost 匿名だと401`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(csrf()) + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdUnmutePost csrfトークンがないと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1MutesGet read権限でミュートしているアカウント一覧を取得できる`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1MutesGet read_mutes権限でミュートしているアカウント一覧を取得できる`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1MutesGet write権限だと403`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1MutesGet 匿名だと401`() { + mockMvc + .get("/api/v1/mutes") + .andExpect { status { isUnauthorized() } } + } + + @Test + fun `apiV1AccountsIdStatusesGet read権限で取得できる`() { + mockMvc + .get("/api/v1/accounts/1/statuses") + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdStatusesGet 匿名でもpublic投稿を取得できる`() { + mockMvc + .get("/api/v1/accounts/1/statuses") + .asyncDispatch() + .andExpect { status { isOk() } } + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} diff --git a/hideout-mastodon/src/test/kotlin/mastodon/apps/AppTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/apps/AppTest.kt new file mode 100644 index 00000000..1b80163c --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/apps/AppTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.apps + +import dev.usbharu.hideout.SpringApplication +import org.assertj.core.api.Assertions.assertThat +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.jdbc.core.JdbcOperations +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository +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 +import util.objectMapper +import kotlin.test.assertNotNull + +@SpringBootTest(classes = [SpringApplication::class]) +@AutoConfigureMockMvc +@Transactional +class AppTest { + + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @Autowired + lateinit var jdbcOperations: JdbcOperations + + + val registeredClientRepository: JdbcRegisteredClientRepository by lazy { + JdbcRegisteredClientRepository( + jdbcOperations + ) + } + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + @Test + @WithAnonymousUser + fun apiV1AppsPostにformで匿名でappを作成できる() { + val contentAsString = 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() } } + .andReturn() + .response + .contentAsString + + val clientId = objectMapper().readTree(contentAsString)["client_id"].asText() + + + val registeredClient = registeredClientRepository.findByClientId(clientId) + + assertNotNull(registeredClient) + assertThat(registeredClient.clientName).isEqualTo("test-client") + assertThat(registeredClient.redirectUris.joinToString(",")).isEqualTo("https://example.com") + assertThat(registeredClient.scopes.joinToString(",")).isEqualTo("read,write") + } + + @Test + @WithAnonymousUser + fun apiV1AppsPostにjsonで匿名でappを作成できる() { + val contentAsString = 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() } } + .andReturn() + .response + .contentAsString + + val clientId = objectMapper().readTree(contentAsString)["client_id"].asText() + + + val registeredClient = registeredClientRepository.findByClientId(clientId) + + assertNotNull(registeredClient) + assertThat(registeredClient.clientName).isEqualTo("test-client-2") + assertThat(registeredClient.redirectUris.joinToString(",")).isEqualTo("https://example.com") + assertThat(registeredClient.scopes.joinToString(",")).isEqualTo("read,write") + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} diff --git a/hideout-mastodon/src/test/kotlin/mastodon/filter/FilterTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/filter/FilterTest.kt new file mode 100644 index 00000000..8052c258 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/filter/FilterTest.kt @@ -0,0 +1,711 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.filter + +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterKeywordsPostRequest +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterPostRequest +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterPostRequestKeyword +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.V1FilterPostRequest +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.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.delete +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 +import util.objectMapper + +@SpringBootTest(classes = [SpringApplication::class]) +@AutoConfigureMockMvc +@Transactional +@Sql("/sql/actors.sql", "/sql/userdetail.sql","/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class FilterTest { + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + @Test + fun `apiV2FiltersPost write権限で追加できる`() { + mockMvc + .post("/api/v2/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterPostRequest( + title = "mute test", + context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public), + filterAction = FilterPostRequest.FilterAction.warn, + expiresIn = null, + keywordsAttributes = listOf( + FilterPostRequestKeyword( + keyword = "hoge", + wholeWord = false, + regex = true + ) + ) + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { + content { + jsonPath("$.keywords[0].keyword") { + value("hoge") + } + } + } + } + + @Test + fun `apiV2FiltersPost write_filters権限で追加できる`() { + mockMvc + .post("/api/v2/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterPostRequest( + title = "mute test", + context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public), + filterAction = FilterPostRequest.FilterAction.warn, + expiresIn = null, + keywordsAttributes = listOf( + FilterPostRequestKeyword( + keyword = "fuga", + wholeWord = true, + regex = false + ) + ) + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { + content { + jsonPath("$.keywords[0].keyword") { + value("fuga") + } + } + } + } + + @Test + fun `apiV2FiltersPost read権限で401`() { + mockMvc + .post("/api/v2/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterPostRequest( + title = "mute test", + context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public), + filterAction = FilterPostRequest.FilterAction.warn, + expiresIn = null, + keywordsAttributes = listOf( + FilterPostRequestKeyword( + keyword = "fuga", + wholeWord = true, + regex = false + ) + ) + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersGet read権限で取得できる`() { + mockMvc + .get("/api/v2/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersGet read_filters権限で取得できる`() { + mockMvc + .get("/api/v2/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersGet write権限で401`() { + mockMvc + .get("/api/v2/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersIdGet read権限で取得できる`() { + mockMvc + .get("/api/v2/filters/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + + @Test + fun `apiV2FiltersIdGet read_filters権限で取得できる`() { + mockMvc + .get("/api/v2/filters/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersIdGet write権限で401`() { + mockMvc + .get("/api/v2/filters/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsGet read権限で取得できる`() { + mockMvc + .get("/api/v2/filters/1/keywords") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsGet read_filters権限で取得できる`() { + mockMvc + .get("/api/v2/filters/1/keywords") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsGet writeで403`() { + mockMvc + .get("/api/v2/filters/1/keywords") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsPost writeで追加できる`() { + mockMvc + .post("/api/v2/filters/1/keywords") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterKeywordsPostRequest( + "hage", false, false + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsPost write_filtersで追加できる`() { + mockMvc + .post("/api/v2/filters/1/keywords") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterKeywordsPostRequest( + "hage", false, false + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersFilterIdKeywordsPost readで403`() { + mockMvc + .post("/api/v2/filters/1/keywords") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + FilterKeywordsPostRequest( + "hage", false, false + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersKeywordsIdGet readで取得できる`() { + mockMvc + .get("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersKeywordsIdGet read_filtersで取得できる`() { + mockMvc + .get("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersKeywordsIdGet writeだと403`() { + mockMvc + .get("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersKeyowrdsIdDelete writeで削除できる`() = runTest { + mockMvc + .delete("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersKeyowrdsIdDelete write_filtersで削除できる`() = runTest { + mockMvc + .delete("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV2FiltersKeyowrdsIdDelete readで403`() = runTest { + mockMvc + .delete("/api/v2/filters/keywords/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersFilterIdStatuses readで取得できる`() { + mockMvc + .get("/api/v2/filters/1/statuses") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersFilterIdStatuses read_filtersで取得できる`() { + mockMvc + .get("/api/v2/filters/1/statuses") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersFilterIdStatuses writeで403`() { + mockMvc + .get("/api/v2/filters/1/statuses") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersStatusesIdGet readで取得できる`() { + mockMvc + .get("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersStatusesIdGet read_filtersで取得できる`() { + mockMvc + .get("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersStatusesIdGet writeで403`() { + mockMvc + .get("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV2FiltersStatusesIdDelete writeで削除できる`() { + mockMvc + .delete("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersStatusesIdDelete write_filtersで削除できる`() { + mockMvc + .delete("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `apiV2FiltersStatusesIdDelete readで403`() { + mockMvc + .delete("/api/v2/filters/statuses/1") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1FiltersGet readで取得できる`() { + mockMvc + .get("/api/v1/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersGet read_filtersで取得できる`() { + mockMvc + .get("/api/v1/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersGet writeで403`() { + mockMvc + .get("/api/v1/filters") { + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1FiltersPost writeで新規作成`() { + mockMvc + .post("/api/v1/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + V1FilterPostRequest( + phrase = "hoge", + context = listOf(V1FilterPostRequest.Context.home), + irreversible = false, + wholeWord = false, + expiresIn = null + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersPost write_filtersで新規作成`() { + mockMvc + .post("/api/v1/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + V1FilterPostRequest( + phrase = "hoge", + context = listOf(V1FilterPostRequest.Context.home), + irreversible = false, + wholeWord = false, + expiresIn = null + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersPost readで403`() { + mockMvc + .post("/api/v1/filters") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper().writeValueAsString( + V1FilterPostRequest( + phrase = "hoge", + context = listOf(V1FilterPostRequest.Context.home), + irreversible = false, + wholeWord = false, + expiresIn = null + ) + ) + with( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1FiltersIdGet readで取得できる`() { + mockMvc + .get("/api/v1/filters/1") { + with( + jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersIdGet read_filtersで取得できる`() { + mockMvc + .get("/api/v1/filters/1") { + with( + jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersIdGet writeで403`() { + mockMvc + .get("/api/v1/filters/1") { + with( + jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .andExpect { status { isForbidden() } } + } + + @Test + @Sql("/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + fun `apiV1FiltersIdDelete writeで削除できる`() { + mockMvc + .delete("/api/v1/filters/1") { + with( + jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + @Sql("/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + fun `apiV1FiltersIdDelete write_filtersで削除できる`() { + mockMvc + .delete("/api/v1/filters/1") { + with( + jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1FiltersIdDelete readで403`() { + mockMvc + .delete("/api/v1/filters/1") { + 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() + } + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/mastodon/media/MediaTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/media/MediaTest.kt new file mode 100644 index 00000000..7e4755d9 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/media/MediaTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.media + +import dev.usbharu.hideout.SpringApplication +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.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/actors.sql","/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class MediaTest { + + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + @Test + fun メディアをアップロードできる() = runTest { + + + 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 { + + + 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 { + + + 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() + } + } + +} diff --git a/hideout-mastodon/src/test/kotlin/mastodon/notifications/ExposedNotificationsApiPaginationTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/notifications/ExposedNotificationsApiPaginationTest.kt new file mode 100644 index 00000000..97f6f8f1 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/notifications/ExposedNotificationsApiPaginationTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.notifications + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Notification +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +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.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +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.get +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], properties = ["hideout.use-mongodb=false"]) +@AutoConfigureMockMvc +@Transactional +@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +//@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +//@Sql("/sql/notification/test-mastodon_notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class ExposedNotificationsApiPaginationTest { + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @Test + fun `通知を取得できる`() = runTest { + val content = mockMvc + .get("/api/v1/notifications") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("65") + assertThat(value.last().id).isEqualTo("26") + } + + @Test + fun maxIdを指定して通知を取得できる() = runTest { + val content = mockMvc + .get("/api/v1/notifications?max_id=26") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("25") + assertThat(value.last().id).isEqualTo("1") + + } + + @Test + fun minIdを指定して通知を取得できる() = runTest { + val content = mockMvc + .get("/api/v1/notifications?min_id=25") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value.first().id).isEqualTo("65") + assertThat(value.last().id).isEqualTo("26") + } + + @Test + fun 結果が0件のときはページネーションのヘッダーがない() = runTest { + val content = mockMvc + .get("/api/v1/notifications?max_id=1") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + doesNotExist("Link") + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + assertThat(value).size().isZero() + } + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/mastodon/notifications/MongodbNotificationsApiPaginationTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/notifications/MongodbNotificationsApiPaginationTest.kt new file mode 100644 index 00000000..c0debcd5 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/notifications/MongodbNotificationsApiPaginationTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.notifications + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Notification +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions +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.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +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.get +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], properties = ["hideout.use-mongodb=true"]) +@AutoConfigureMockMvc +@Transactional +@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +//@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class MongodbNotificationsApiPaginationTest { + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @Test + fun `通知を取得できる`() = runTest { + val content = mockMvc + .get("/api/v1/notifications") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andDo { print() } + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + Assertions.assertThat(value.first().id).isEqualTo("65") + Assertions.assertThat(value.last().id).isEqualTo("26") + } + + @Test + fun maxIdを指定して通知を取得できる() = runTest { + val content = mockMvc + .get("/api/v1/notifications?max_id=26") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + Assertions.assertThat(value.first().id).isEqualTo("25") + Assertions.assertThat(value.last().id).isEqualTo("1") + + } + + @Test + fun minIdを指定して通知を取得できる() = runTest { + val content = mockMvc + .get("/api/v1/notifications?min_id=25") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + string( + "Link", + "; rel=\"next\", ; rel=\"prev\"" + ) + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + Assertions.assertThat(value.first().id).isEqualTo("65") + Assertions.assertThat(value.last().id).isEqualTo("26") + } + + @Test + fun 結果が0件のときはページネーションのヘッダーがない() = runTest { + val content = mockMvc + .get("/api/v1/notifications?max_id=1") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { + header { + doesNotExist("Link") + } + } + .andReturn() + .response + .contentAsString + + val value = jacksonObjectMapper().readValue(content, object : TypeReference>() {}) + + Assertions.assertThat(value).size().isZero() + } + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/mastodon/status/StatusTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/status/StatusTest.kt new file mode 100644 index 00000000..35a4a8a6 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/status/StatusTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.status + +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji +import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions +import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction +import org.assertj.core.api.Assertions.assertThat +import org.flywaydb.core.Flyway +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.selectAll +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.csrf +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.post +import org.springframework.test.web.servlet.put +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/actors.sql", "/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/posts.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/test-custom-emoji.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(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + @Test + fun 投稿できる() { + mockMvc + .post("/api/v1/statuses") { + contentType = MediaType.APPLICATION_JSON + content = """{"status":"hello"}""" + with( + 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( + 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( + 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( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + 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( + jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")) + ) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { jsonPath("\$.in_reply_to_id") { value("1") } } + } + + @Test + fun ユニコード絵文字をリアクションできる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/😭") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = + Reactions.selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction() + assertThat(reaction.unicodeEmoji).isEqualTo(UnicodeEmoji("😭")) + assertThat(reaction.postId).isEqualTo(1) + assertThat(reaction.actorId).isEqualTo(1) + } + + @Test + fun 存在しない絵文字はフォールバックされる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/hoge") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = + Reactions.selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction() + assertThat(reaction.unicodeEmoji).isEqualTo(UnicodeEmoji("❤")) + assertThat(reaction.postId).isEqualTo(1) + assertThat(reaction.actorId).isEqualTo(1) + } + + @Test + fun カスタム絵文字をリアクションできる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/kotlin") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = + Reactions.leftJoin(CustomEmojis).selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) } + .single() + .toReaction() + + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} diff --git a/hideout-mastodon/src/test/kotlin/mastodon/timelines/TimelineApiTest.kt b/hideout-mastodon/src/test/kotlin/mastodon/timelines/TimelineApiTest.kt new file mode 100644 index 00000000..3f6bf40a --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/mastodon/timelines/TimelineApiTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mastodon.timelines + +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.context.SpringBootTest +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.setup.SecurityMockMvcConfigurers +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.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]) +@Transactional +@Sql("/sql/actors.sql", "/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +class TimelineApiTest { + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun beforeEach() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build() + } + + @Test + fun `apiV1TimelinesHomeGetにreadでアクセスできる`() { + mockMvc + .get("/api/v1/timelines/home") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1TimelinesHomeGetにread statusesでアクセスできる`() { + mockMvc + .get("/api/v1/timelines/home") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:statuses")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + @WithAnonymousUser + fun apiV1TimelineHomeGetに匿名でアクセスすると401() { + mockMvc + .get("/api/v1/timelines/home") + .andExpect { status { isUnauthorized() } } + } + + @Test + fun apiV1TimelinesPublicGetにreadでアクセスできる() { + mockMvc + .get("/api/v1/timelines/public") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1TimelinesPublicGetにread statusesでアクセスできる`() { + mockMvc + .get("/api/v1/timelines/public") { + with( + SecurityMockMvcRequestPostProcessors.jwt() + .jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:statuses")) + ) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + @WithAnonymousUser + fun apiV1TimeinesPublicGetに匿名でアクセスできる() { + mockMvc + .get("/api/v1/timelines/public") + .asyncDispatch() + .andExpect { status { isOk() } } + } + + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } +} diff --git a/hideout-mastodon/src/test/kotlin/util/Json.kt b/hideout-mastodon/src/test/kotlin/util/Json.kt new file mode 100644 index 00000000..fe49c2ca --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/Json.kt @@ -0,0 +1,8 @@ +package util + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper + +fun objectMapper(): ObjectMapper { + return jacksonObjectMapper() +} \ No newline at end of file diff --git a/hideout-mastodon/src/test/kotlin/util/SpringApplicationTestBase.kt b/hideout-mastodon/src/test/kotlin/util/SpringApplicationTestBase.kt new file mode 100644 index 00000000..158f8c7a --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/SpringApplicationTestBase.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +abstract class SpringApplicationTestBase diff --git a/hideout-mastodon/src/test/kotlin/util/TestTransaction.kt b/hideout-mastodon/src/test/kotlin/util/TestTransaction.kt new file mode 100644 index 00000000..8830ba4e --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/TestTransaction.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +import dev.usbharu.hideout.core.application.shared.Transaction + + +object TestTransaction : Transaction { + override suspend fun transaction(block: suspend () -> T): T = block() + + override suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T = block() +} diff --git a/hideout-mastodon/src/test/kotlin/util/WithHttpSignature.kt b/hideout-mastodon/src/test/kotlin/util/WithHttpSignature.kt new file mode 100644 index 00000000..d7e58abc --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/WithHttpSignature.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +//@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +//@Retention(AnnotationRetention.RUNTIME) +//@Inherited +//@MustBeDocumented +//@WithSecurityContext(factory = WithHttpSignatureSecurityContextFactory::class) +//annotation class WithHttpSignature( +// @get:AliasFor( +// annotation = WithSecurityContext::class +// ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD, +// val keyId: String = "https://example.com/users/test-user#pubkey", +// val url: String = "https://example.com/inbox", +// val method: String = "GET" +//) diff --git a/hideout-mastodon/src/test/kotlin/util/WithHttpSignatureSecurityContextFactory.kt b/hideout-mastodon/src/test/kotlin/util/WithHttpSignatureSecurityContextFactory.kt new file mode 100644 index 00000000..626cf79e --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/WithHttpSignatureSecurityContextFactory.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +//class WithHttpSignatureSecurityContextFactory( +// private val actorRepository: ActorRepository, +// private val transaction: Transaction +//) : WithSecurityContextFactory { +// +// private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy() +// +// override fun createSecurityContext(annotation: WithHttpSignature): SecurityContext = runBlocking { +// val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( +// annotation.keyId, HttpRequest( +// URL("https://example.com/inbox"), +// HttpHeaders(mapOf()), HttpMethod.GET +// ) +// ) +// val httpSignatureUser = transaction.transaction { +// val findByKeyId = +// actorRepository.findByKeyId(annotation.keyId) ?: throw IllegalArgumentException(annotation.keyId) +// HttpSignatureUser( +// findByKeyId.name, +// findByKeyId.domain, +// findByKeyId.id, +// true, +// true, +// mutableListOf() +// ) +// } +// preAuthenticatedAuthenticationToken.details = httpSignatureUser +// preAuthenticatedAuthenticationToken.isAuthenticated = true +// val emptyContext = securityContextStrategy.createEmptyContext() +// emptyContext.authentication = preAuthenticatedAuthenticationToken +// return@runBlocking emptyContext +// } +// +//} diff --git a/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignature.kt b/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignature.kt new file mode 100644 index 00000000..fcaba839 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignature.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +// +//@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +//@Retention(AnnotationRetention.RUNTIME) +//@Inherited +//@MustBeDocumented +//@WithSecurityContext(factory = WithMockHttpSignatureSecurityContextFactory::class) +//annotation class WithMockHttpSignature( +// @get:AliasFor( +// annotation = WithSecurityContext::class +// ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD, +// val username: String = "test-user", +// val domain: String = "example.com", +// val keyId: String = "https://example.com/users/test-user#pubkey", +// val id: Long = 1234L, +// val url: String = "https://example.com/inbox", +// val method: String = "GET" +//) diff --git a/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt b/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt new file mode 100644 index 00000000..bf5218f6 --- /dev/null +++ b/hideout-mastodon/src/test/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util +// +//import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser +//import dev.usbharu.httpsignature.common.HttpHeaders +//import dev.usbharu.httpsignature.common.HttpMethod +//import dev.usbharu.httpsignature.common.HttpRequest +//import org.springframework.security.core.context.SecurityContext +//import org.springframework.security.core.context.SecurityContextHolder +//import org.springframework.security.test.context.support.WithSecurityContextFactory +//import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +//import java.net.URL +// +//class WithMockHttpSignatureSecurityContextFactory : +// WithSecurityContextFactory { +// +// private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy() +// +// override fun createSecurityContext(annotation: WithMockHttpSignature): SecurityContext { +// val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( +// annotation.keyId, HttpRequest( +// URL(annotation.url), +// HttpHeaders(mapOf()), HttpMethod.valueOf(annotation.method.uppercase()) +// ) +// ) +// val httpSignatureUser = HttpSignatureUser( +// annotation.username, +// annotation.domain, +// annotation.id, +// true, +// true, +// mutableListOf() +// ) +// preAuthenticatedAuthenticationToken.details = httpSignatureUser +// preAuthenticatedAuthenticationToken.isAuthenticated = true +// val emptyContext = securityContextStrategy.createEmptyContext() +// emptyContext.authentication = preAuthenticatedAuthenticationToken +// return emptyContext +// } +//} diff --git a/hideout-mastodon/src/test/resources/application.yml b/hideout-mastodon/src/test/resources/application.yml index 05671a3f..0f4fd0e1 100644 --- a/hideout-mastodon/src/test/resources/application.yml +++ b/hideout-mastodon/src/test/resources/application.yml @@ -2,6 +2,8 @@ spring: datasource: url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;" driver-class-name: org.h2.Driver + flyway: + clean-disabled: false hideout: url: "https://test-hideout-dev.usbharu.dev" security: @@ -9,4 +11,4 @@ hideout: generate: true key-id: a private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==" - public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" \ No newline at end of file + public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" diff --git a/hideout-mastodon/src/test/resources/media/400x400.png b/hideout-mastodon/src/test/resources/media/400x400.png new file mode 100644 index 00000000..0d2e71be Binary files /dev/null and b/hideout-mastodon/src/test/resources/media/400x400.png differ diff --git a/hideout-mastodon/src/test/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql b/hideout-mastodon/src/test/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql new file mode 100644 index 00000000..a2b01c22 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql @@ -0,0 +1,17 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (3733363, 'follow-test-user-1', 'example.com', 'follow-test-user-1-name', '', + 'https://example.com/users/follow-test-user-1/inbox', + 'https://example.com/users/follow-test-user-1/outbox', 'https://example.com/users/follow-test-user-1', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/follow-test-user-1#pubkey', 'https://example.com/users/follow-test-user-1/following', + 'https://example.com/users/follow-test-user-1/followers', 0, false, 0, 0, 0, null), + (37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '', + 'https://example.com/users/follow-test-user-2/inbox', + 'https://example.com/users/follow-test-user-2/outbox', 'https://example.com/users/follow-test-user-2', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/follow-test-user-2#pubkey', 'https://example.com/users/follow-test-user-2/following', + 'https://example.com/users/follow-test-user-2/followers', 0, false, 0, 0, 0, null); diff --git a/hideout-mastodon/src/test/resources/sql/accounts/test-accounts-statuses.sql b/hideout-mastodon/src/test/resources/sql/accounts/test-accounts-statuses.sql new file mode 100644 index 00000000..058a3e81 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/accounts/test-accounts-statuses.sql @@ -0,0 +1,201 @@ +insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, deleted, hide, move_to) +VALUES (1, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/1', + null, null, false, 'https://example.com/users/1/posts/1', false,false,null), + (2, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/2', + null, 1, false, 'https://example.com/users/1/posts/2', false,false,null), + (3, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/3', + null, null, false, 'https://example.com/users/1/posts/3', false,false,null), + (4, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/4', + null, 3, false, 'https://example.com/users/1/posts/4', false,false,null), + (5, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/5', + null, null, false, 'https://example.com/users/1/posts/5', false,false,null), + (6, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/6', + null, null, false, 'https://example.com/users/1/posts/6', false,false,null), + (7, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/7', + null, null, false, 'https://example.com/users/1/posts/7', false,false,null), + (8, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/8', + null, 7, false, 'https://example.com/users/1/posts/8', false,false,null), + (9, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/9', + null, null, false, 'https://example.com/users/1/posts/9', false,false,null), + (10, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/10', + null, 9, false, 'https://example.com/users/1/posts/10', false,false,null), + (11, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/11', + null, null, false, 'https://example.com/users/1/posts/11', false,false,null), + (12, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/12', + null, null, false, 'https://example.com/users/1/posts/12', false,false,null), + (13, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/13', + null, null, false, 'https://example.com/users/1/posts/13', false,false,null), + (14, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/14', + null, 13, false, 'https://example.com/users/1/posts/14', false,false,null), + (15, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/15', + null, null, false, 'https://example.com/users/1/posts/15', false,false,null), + (16, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/16', + null, 15, false, 'https://example.com/users/1/posts/16', false,false,null), + (17, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/17', + null, null, false, 'https://example.com/users/1/posts/17', false,false,null), + (18, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/18', + null, null, false, 'https://example.com/users/1/posts/18', false,false,null), + (19, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/19', + null, null, false, 'https://example.com/users/1/posts/19', false,false,null), + (20, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/20', + null, 19, false, 'https://example.com/users/1/posts/20', false,false,null), + (21, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/21', + null, null, false, 'https://example.com/users/1/posts/21', false,false,null), + (22, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/22', + null, 21, false, 'https://example.com/users/1/posts/22', false,false,null), + (23, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/23', + null, null, false, 'https://example.com/users/1/posts/23', false,false,null), + (24, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/24', + null, null, false, 'https://example.com/users/1/posts/24', false,false,null), + (25, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/25', + null, null, false, 'https://example.com/users/1/posts/25', false,false,null), + (26, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/26', + null, 25, false, 'https://example.com/users/1/posts/26', false,false,null), + (27, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/27', + null, null, false, 'https://example.com/users/1/posts/27', false,false,null), + (28, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/28', + null, 27, false, 'https://example.com/users/1/posts/28', false,false,null), + (29, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/29', + null, null, false, 'https://example.com/users/1/posts/29', false,false,null), + (30, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/30', + null, null, false, 'https://example.com/users/1/posts/30', false,false,null), + (31, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/31', + null, null, false, 'https://example.com/users/1/posts/31', false,false,null), + (32, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/32', + null, 31, false, 'https://example.com/users/1/posts/32', false,false,null), + (33, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/33', + null, null, false, 'https://example.com/users/1/posts/33', false,false,null), + (34, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/34', + null, 33, false, 'https://example.com/users/1/posts/34', false,false,null), + (35, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/35', + null, null, false, 'https://example.com/users/1/posts/35', false,false,null), + (36, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/36', + null, null, false, 'https://example.com/users/1/posts/36', false,false,null), + (37, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/37', + null, null, false, 'https://example.com/users/1/posts/37', false,false,null), + (38, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/38', + null, 37, false, 'https://example.com/users/1/posts/38', false,false,null), + (39, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/39', + null, null, false, 'https://example.com/users/1/posts/39', false,false,null), + (40, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/40', + null, 39, false, 'https://example.com/users/1/posts/40', false,false,null), + (41, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/41', + null, null, false, 'https://example.com/users/1/posts/41', false,false,null), + (42, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/42', + null, null, false, 'https://example.com/users/1/posts/42', false,false,null), + (43, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/43', + null, null, false, 'https://example.com/users/1/posts/43', false,false,null), + (44, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/44', + null, 43, false, 'https://example.com/users/1/posts/44', false,false,null), + (45, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/45', + null, null, false, 'https://example.com/users/1/posts/45', false,false,null), + (46, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/46', + null, 45, false, 'https://example.com/users/1/posts/46', false,false,null), + (47, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/47', + null, null, false, 'https://example.com/users/1/posts/47', false,false,null), + (48, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/48', + null, null, false, 'https://example.com/users/1/posts/48', false,false,null), + (49, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/49', + null, null, false, 'https://example.com/users/1/posts/49', false,false,null), + (50, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/50', + null, 49, false, 'https://example.com/users/1/posts/50', false,false,null), + (51, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/51', + null, null, false, 'https://example.com/users/1/posts/51', false,false,null), + (52, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/52', + null, 51, false, 'https://example.com/users/1/posts/52', false,false,null), + (53, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/53', + null, null, false, 'https://example.com/users/1/posts/53', false,false,null), + (54, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/54', + null, null, false, 'https://example.com/users/1/posts/54', false,false,null), + (55, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/55', + null, null, false, 'https://example.com/users/1/posts/55', false,false,null), + (56, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/56', + null, 55, false, 'https://example.com/users/1/posts/56', false,false,null), + (57, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/57', + null, null, false, 'https://example.com/users/1/posts/57', false,false,null), + (58, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/58', + null, 57, false, 'https://example.com/users/1/posts/58', false,false,null), + (59, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/59', + null, null, false, 'https://example.com/users/1/posts/59', false,false,null), + (60, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/60', + null, null, false, 'https://example.com/users/1/posts/60', false,false,null), + (61, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/61', + null, null, false, 'https://example.com/users/1/posts/61', false,false,null), + (62, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/62', + null, 61, false, 'https://example.com/users/1/posts/62', false,false,null), + (63, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/63', + null, null, false, 'https://example.com/users/1/posts/63', false,false,null), + (64, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/64', + null, 63, false, 'https://example.com/users/1/posts/64', false,false,null), + (65, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/65', + null, null, false, 'https://example.com/users/1/posts/65', false,false,null), + (66, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/66', + null, null, false, 'https://example.com/users/1/posts/66', false,false,null), + (67, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/67', + null, null, false, 'https://example.com/users/1/posts/67', false,false,null), + (68, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/68', + null, 67, false, 'https://example.com/users/1/posts/68', false,false,null), + (69, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/69', + null, null, false, 'https://example.com/users/1/posts/69', false,false,null), + (70, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/70', + null, 69, false, 'https://example.com/users/1/posts/70', false,false,null), + (71, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/71', + null, null, false, 'https://example.com/users/1/posts/71', false,false,null), + (72, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/72', + null, null, false, 'https://example.com/users/1/posts/72', false,false,null), + (73, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/73', + null, null, false, 'https://example.com/users/1/posts/73', false,false,null), + (74, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/74', + null, 73, false, 'https://example.com/users/1/posts/74', false,false,null), + (75, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/75', + null, null, false, 'https://example.com/users/1/posts/75', false,false,null), + (76, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/76', + null, 75, false, 'https://example.com/users/1/posts/76', false,false,null), + (77, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/77', + null, null, false, 'https://example.com/users/1/posts/77', false,false,null), + (78, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/78', + null, null, false, 'https://example.com/users/1/posts/78', false,false,null), + (79, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/79', + null, null, false, 'https://example.com/users/1/posts/79', false,false,null), + (80, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/80', + null, 79, false, 'https://example.com/users/1/posts/80', false,false,null), + (81, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/81', + null, null, false, 'https://example.com/users/1/posts/81', false,false,null), + (82, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/82', + null, 81, false, 'https://example.com/users/1/posts/82', false,false,null), + (83, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/83', + null, null, false, 'https://example.com/users/1/posts/83', false,false,null), + (84, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/84', + null, null, false, 'https://example.com/users/1/posts/84', false,false,null), + (85, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/85', + null, null, false, 'https://example.com/users/1/posts/85', false,false,null), + (86, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/86', + null, 85, false, 'https://example.com/users/1/posts/86', false,false,null), + (87, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/87', + null, null, false, 'https://example.com/users/1/posts/87', false,false,null), + (88, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/88', + null, 87, false, 'https://example.com/users/1/posts/88', false,false,null), + (89, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/89', + null, null, false, 'https://example.com/users/1/posts/89', false,false,null), + (90, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/90', + null, null, false, 'https://example.com/users/1/posts/90', false,false,null), + (91, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/91', + null, null, false, 'https://example.com/users/1/posts/91', false,false,null), + (92, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/92', + null, 91, false, 'https://example.com/users/1/posts/92', false,false,null), + (93, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/93', + null, null, false, 'https://example.com/users/1/posts/93', false,false,null), + (94, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/94', + null, 93, false, 'https://example.com/users/1/posts/94', false,false,null), + (95, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/95', + null, null, false, 'https://example.com/users/1/posts/95', false,false,null), + (96, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/96', + null, null, false, 'https://example.com/users/1/posts/96', false,false,null), + (97, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/97', + null, null, false, 'https://example.com/users/1/posts/97', false,false,null), + (98, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/98', + null, 97, false, 'https://example.com/users/1/posts/98', false,false,null), + (99, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/99', + null, null, false, 'https://example.com/users/1/posts/99', false,false,null), + (100, 1,1,null, '

this is test

', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/100', + null, 99, false, 'https://example.com/users/1/posts/100', false,false,null); \ No newline at end of file diff --git a/hideout-mastodon/src/test/resources/sql/filter/test-filter.sql b/hideout-mastodon/src/test/resources/sql/filter/test-filter.sql new file mode 100644 index 00000000..8eab045a --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/filter/test-filter.sql @@ -0,0 +1,4 @@ +insert into filters (id, user_id, name, context, action) +VALUES (1, 1, 'test filter', 'HOME', 'WARN'); +insert into filter_keywords(id, filter_id, keyword, mode) +VALUES (1, 1, 'hoge', 'NONE') \ No newline at end of file diff --git a/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql new file mode 100644 index 00000000..dc2ab9f1 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql @@ -0,0 +1,28 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-user8.', + 'https://example.com/users/test-user8/inbox', + 'https://example.com/users/test-user8/outbox', 'https://example.com/users/test-user8', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following', + 'https://example.com/users/test-user8/followers', 0, false, 0, 0, 0, null), + (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.', + 'https://follower.example.com/users/test-user9/inbox', + 'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user9#pubkey', + 'https://follower.example.com/users/test-user9/following', + 'https://follower.example.com/users/test-user9/followers', 0, false, 0, 0, 0, null); + +insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (9, 8, true, false, false, false, false); + +insert into POSTS (ID, ACTOR_ID, OVERVIEW, CONTENT, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE, + AP_ID) +VALUES (1239, 8, null, '

test post

', 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239', + null, null, false, + 'https://example.com/users/test-user8/posts/1239'); diff --git a/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql new file mode 100644 index 00000000..a44861f4 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql @@ -0,0 +1,29 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test user4.', + 'https://example.com/users/test-user4/inbox', + 'https://example.com/users/test-user4/outbox', 'https://example.com/users/test-user4', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following', + 'https://example.com/users/test-user4/followers', 0, false, 0, 0, 0, null), + (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.', + 'https://follower.example.com/users/test-user5/inbox', + 'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user5#pubkey', + 'https://follower.example.com/users/test-user5/following', + 'https://follower.example.com/users/test-user5/followers', 0, false, 0, 0, 0, null); + +insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (5, 4, true, false, false, false, false); + +insert into POSTS (ID, "actor_id", OVERVIEW, CONTENT, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id", + SENSITIVE, + AP_ID) +VALUES (1237, 4, null, '

test post

', 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237', + null, null, false, + 'https://example.com/users/test-user4/posts/1237'); diff --git a/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql new file mode 100644 index 00000000..77cb3395 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql @@ -0,0 +1,29 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-user6.', + 'https://example.com/users/test-user6/inbox', + 'https://example.com/users/test-user6/outbox', 'https://example.com/users/test-user6', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following', + 'https://example.com/users/test-user6/followers', 0, false, 0, 0, 0, null), + (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.', + 'https://follower.example.com/users/test-user7/inbox', + 'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user7#pubkey', + 'https://follower.example.com/users/test-user7/following', + 'https://follower.example.com/users/test-user7/followers', 0, false, 0, 0, 0, null); + +insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (7, 6, true, false, false, false, false); + +insert into POSTS (ID, "actor_ID", OVERVIEW, CONTENT, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID", + SENSITIVE, + AP_ID) +VALUES (1238, 6, null, '

test post

', 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238', + null, null, false, + 'https://example.com/users/test-user6/posts/1238'); diff --git a/hideout-mastodon/src/test/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/hideout-mastodon/src/test/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql new file mode 100644 index 00000000..6cc9db8c --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -0,0 +1,25 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is test-user11.', + 'https://example.com/users/test-user11/inbox', + 'https://example.com/users/test-user11/outbox', 'https://example.com/users/test-user11', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following', + 'https://example.com/users/test-user11/followers', 0, false, 0, 0, 0, null); + +insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, + ap_id, + deleted) +VALUES (1242, 11, null, '

test post

', 'test post', 12345680, 0, + 'https://example.com/users/test-user11/posts/1242', null, null, false, + 'https://example.com/users/test-user11/posts/1242', false); + +insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION) +VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png', null), + (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png', null); + +insert into POSTS_MEDIA(POST_ID, MEDIA_ID) +VALUES (1242, 1), + (1242, 2); diff --git a/hideout-mastodon/src/test/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql b/hideout-mastodon/src/test/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql new file mode 100644 index 00000000..41ac73a4 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql @@ -0,0 +1,20 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is test-user10.', + 'https://example.com/users/test-user10/inbox', + 'https://example.com/users/test-user10/outbox', 'https://example.com/users/test-user10', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following', + 'https://example.com/users/test-user10/followers', 0, false, 0, 0, 0, null); + +insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, + ap_id, + deleted) +VALUES (1240, 10, null, '

test post

', 'test post', 12345680, 0, + 'https://example.com/users/test-user10/posts/1240', null, null, false, + 'https://example.com/users/test-user10/posts/1240', false), + (1241, 10, null, '

test post

', 'test post', 12345680, 0, + 'https://example.com/users/test-user10/posts/1241', null, 1240, false, + 'https://example.com/users/test-user10/posts/1241', false); diff --git a/hideout-mastodon/src/test/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql b/hideout-mastodon/src/test/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql new file mode 100644 index 00000000..250cfb5a --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql @@ -0,0 +1,17 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test user3.', + 'https://example.com/users/test-user3/inbox', + 'https://example.com/users/test-user3/outbox', 'https://example.com/users/test-user3', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following', + 'https://example.com/users/test-user3/followers', 0, false, 0, 0, 0, null); + +insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, + ap_id, + deleted) +VALUES (1236, 3, null, '

test post

', 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236', + null, null, false, + 'https://example.com/users/test-user3/posts/1236', false) diff --git a/hideout-mastodon/src/test/resources/sql/note/匿名でpublic投稿を取得できる.sql b/hideout-mastodon/src/test/resources/sql/note/匿名でpublic投稿を取得できる.sql new file mode 100644 index 00000000..777f9244 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/匿名でpublic投稿を取得できる.sql @@ -0,0 +1,17 @@ +insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at, + key_id, following, followers, instance, locked, following_count, followers_count, posts_count, + last_post_at) +VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', + 'https://example.com/users/test-user/inbox', + 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', + 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null); + +insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, + ap_id, + deleted) +VALUES (1234, 1, null, '

test post

', 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234', + null, null, false, + 'https://example.com/users/test-user/posts/1234', false) diff --git a/hideout-mastodon/src/test/resources/sql/note/匿名でunlisted投稿を取得できる.sql b/hideout-mastodon/src/test/resources/sql/note/匿名でunlisted投稿を取得できる.sql new file mode 100644 index 00000000..b132734d --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/note/匿名でunlisted投稿を取得できる.sql @@ -0,0 +1,17 @@ +insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at, + key_id, following, followers, instance, locked, following_count, followers_count, posts_count, + last_post_at) +VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test user2.', + 'https://example.com/users/test-user2/inbox', + 'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following', + 'https://example.com/users/test-user2/followers', 0, false, 0, 0, 0, null); + +insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, + ap_id, + deleted) +VALUES (1235, 2, null, '

test post

', 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235', + null, null, false, + 'https://example.com/users/test-user2/posts/1235', false) diff --git a/hideout-mastodon/src/test/resources/sql/notification/test-mastodon_notifications.sql b/hideout-mastodon/src/test/resources/sql/notification/test-mastodon_notifications.sql new file mode 100644 index 00000000..c97a25a7 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/notification/test-mastodon_notifications.sql @@ -0,0 +1,66 @@ +insert into mastodon_notifications (id, user_id, type, created_at, account_id, status_id, report_id, relationship_serverance_event_id) +values (1, 1, 'follow', current_timestamp, 2, null, null, null), + (2, 1, 'follow', current_timestamp, 2, null, null, null), + (3, 1, 'follow', current_timestamp, 2, null, null, null), + (4, 1, 'follow', current_timestamp, 2, null, null, null), + (5, 1, 'follow', current_timestamp, 2, null, null, null), + (6, 1, 'follow', current_timestamp, 2, null, null, null), + (7, 1, 'follow', current_timestamp, 2, null, null, null), + (8, 1, 'follow', current_timestamp, 2, null, null, null), + (9, 1, 'follow', current_timestamp, 2, null, null, null), + (10, 1, 'follow', current_timestamp, 2, null, null, null), + (11, 1, 'follow', current_timestamp, 2, null, null, null), + (12, 1, 'follow', current_timestamp, 2, null, null, null), + (13, 1, 'follow', current_timestamp, 2, null, null, null), + (14, 1, 'follow', current_timestamp, 2, null, null, null), + (15, 1, 'follow', current_timestamp, 2, null, null, null), + (16, 1, 'follow', current_timestamp, 2, null, null, null), + (17, 1, 'follow', current_timestamp, 2, null, null, null), + (18, 1, 'follow', current_timestamp, 2, null, null, null), + (19, 1, 'follow', current_timestamp, 2, null, null, null), + (20, 1, 'follow', current_timestamp, 2, null, null, null), + (21, 1, 'follow', current_timestamp, 2, null, null, null), + (22, 1, 'follow', current_timestamp, 2, null, null, null), + (23, 1, 'follow', current_timestamp, 2, null, null, null), + (24, 1, 'follow', current_timestamp, 2, null, null, null), + (25, 1, 'follow', current_timestamp, 2, null, null, null), + (26, 1, 'follow', current_timestamp, 2, null, null, null), + (27, 1, 'follow', current_timestamp, 2, null, null, null), + (28, 1, 'follow', current_timestamp, 2, null, null, null), + (29, 1, 'follow', current_timestamp, 2, null, null, null), + (30, 1, 'follow', current_timestamp, 2, null, null, null), + (31, 1, 'follow', current_timestamp, 2, null, null, null), + (32, 1, 'follow', current_timestamp, 2, null, null, null), + (33, 1, 'follow', current_timestamp, 2, null, null, null), + (34, 1, 'follow', current_timestamp, 2, null, null, null), + (35, 1, 'follow', current_timestamp, 2, null, null, null), + (36, 1, 'follow', current_timestamp, 2, null, null, null), + (37, 1, 'follow', current_timestamp, 2, null, null, null), + (38, 1, 'follow', current_timestamp, 2, null, null, null), + (39, 1, 'follow', current_timestamp, 2, null, null, null), + (40, 1, 'follow', current_timestamp, 2, null, null, null), + (41, 1, 'follow', current_timestamp, 2, null, null, null), + (42, 1, 'follow', current_timestamp, 2, null, null, null), + (43, 1, 'follow', current_timestamp, 2, null, null, null), + (44, 1, 'follow', current_timestamp, 2, null, null, null), + (45, 1, 'follow', current_timestamp, 2, null, null, null), + (46, 1, 'follow', current_timestamp, 2, null, null, null), + (47, 1, 'follow', current_timestamp, 2, null, null, null), + (48, 1, 'follow', current_timestamp, 2, null, null, null), + (49, 1, 'follow', current_timestamp, 2, null, null, null), + (50, 1, 'follow', current_timestamp, 2, null, null, null), + (51, 1, 'follow', current_timestamp, 2, null, null, null), + (52, 1, 'follow', current_timestamp, 2, null, null, null), + (53, 1, 'follow', current_timestamp, 2, null, null, null), + (54, 1, 'follow', current_timestamp, 2, null, null, null), + (55, 1, 'follow', current_timestamp, 2, null, null, null), + (56, 1, 'follow', current_timestamp, 2, null, null, null), + (57, 1, 'follow', current_timestamp, 2, null, null, null), + (58, 1, 'follow', current_timestamp, 2, null, null, null), + (59, 1, 'follow', current_timestamp, 2, null, null, null), + (60, 1, 'follow', current_timestamp, 2, null, null, null), + (61, 1, 'follow', current_timestamp, 2, null, null, null), + (62, 1, 'follow', current_timestamp, 2, null, null, null), + (63, 1, 'follow', current_timestamp, 2, null, null, null), + (64, 1, 'follow', current_timestamp, 2, null, null, null), + (65, 1, 'follow', current_timestamp, 2, null, null, null); \ No newline at end of file diff --git a/hideout-mastodon/src/test/resources/sql/notification/test-notifications.sql b/hideout-mastodon/src/test/resources/sql/notification/test-notifications.sql new file mode 100644 index 00000000..38982603 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/notification/test-notifications.sql @@ -0,0 +1,66 @@ +insert into notifications(id, type, user_id, source_actor_id, post_id, text, reaction_id, created_at) +VALUES (1, 'follow', 1, 2, null, null, null, current_timestamp), + (2, 'follow', 1, 2, null, null, null, current_timestamp), + (3, 'follow', 1, 2, null, null, null, current_timestamp), + (4, 'follow', 1, 2, null, null, null, current_timestamp), + (5, 'follow', 1, 2, null, null, null, current_timestamp), + (6, 'follow', 1, 2, null, null, null, current_timestamp), + (7, 'follow', 1, 2, null, null, null, current_timestamp), + (8, 'follow', 1, 2, null, null, null, current_timestamp), + (9, 'follow', 1, 2, null, null, null, current_timestamp), + (10, 'follow', 1, 2, null, null, null, current_timestamp), + (11, 'follow', 1, 2, null, null, null, current_timestamp), + (12, 'follow', 1, 2, null, null, null, current_timestamp), + (13, 'follow', 1, 2, null, null, null, current_timestamp), + (14, 'follow', 1, 2, null, null, null, current_timestamp), + (15, 'follow', 1, 2, null, null, null, current_timestamp), + (16, 'follow', 1, 2, null, null, null, current_timestamp), + (17, 'follow', 1, 2, null, null, null, current_timestamp), + (18, 'follow', 1, 2, null, null, null, current_timestamp), + (19, 'follow', 1, 2, null, null, null, current_timestamp), + (20, 'follow', 1, 2, null, null, null, current_timestamp), + (21, 'follow', 1, 2, null, null, null, current_timestamp), + (22, 'follow', 1, 2, null, null, null, current_timestamp), + (23, 'follow', 1, 2, null, null, null, current_timestamp), + (24, 'follow', 1, 2, null, null, null, current_timestamp), + (25, 'follow', 1, 2, null, null, null, current_timestamp), + (26, 'follow', 1, 2, null, null, null, current_timestamp), + (27, 'follow', 1, 2, null, null, null, current_timestamp), + (28, 'follow', 1, 2, null, null, null, current_timestamp), + (29, 'follow', 1, 2, null, null, null, current_timestamp), + (30, 'follow', 1, 2, null, null, null, current_timestamp), + (31, 'follow', 1, 2, null, null, null, current_timestamp), + (32, 'follow', 1, 2, null, null, null, current_timestamp), + (33, 'follow', 1, 2, null, null, null, current_timestamp), + (34, 'follow', 1, 2, null, null, null, current_timestamp), + (35, 'follow', 1, 2, null, null, null, current_timestamp), + (36, 'follow', 1, 2, null, null, null, current_timestamp), + (37, 'follow', 1, 2, null, null, null, current_timestamp), + (38, 'follow', 1, 2, null, null, null, current_timestamp), + (39, 'follow', 1, 2, null, null, null, current_timestamp), + (40, 'follow', 1, 2, null, null, null, current_timestamp), + (41, 'follow', 1, 2, null, null, null, current_timestamp), + (42, 'follow', 1, 2, null, null, null, current_timestamp), + (43, 'follow', 1, 2, null, null, null, current_timestamp), + (44, 'follow', 1, 2, null, null, null, current_timestamp), + (45, 'follow', 1, 2, null, null, null, current_timestamp), + (46, 'follow', 1, 2, null, null, null, current_timestamp), + (47, 'follow', 1, 2, null, null, null, current_timestamp), + (48, 'follow', 1, 2, null, null, null, current_timestamp), + (49, 'follow', 1, 2, null, null, null, current_timestamp), + (50, 'follow', 1, 2, null, null, null, current_timestamp), + (51, 'follow', 1, 2, null, null, null, current_timestamp), + (52, 'follow', 1, 2, null, null, null, current_timestamp), + (53, 'follow', 1, 2, null, null, null, current_timestamp), + (54, 'follow', 1, 2, null, null, null, current_timestamp), + (55, 'follow', 1, 2, null, null, null, current_timestamp), + (56, 'follow', 1, 2, null, null, null, current_timestamp), + (57, 'follow', 1, 2, null, null, null, current_timestamp), + (58, 'follow', 1, 2, null, null, null, current_timestamp), + (59, 'follow', 1, 2, null, null, null, current_timestamp), + (60, 'follow', 1, 2, null, null, null, current_timestamp), + (61, 'follow', 1, 2, null, null, null, current_timestamp), + (62, 'follow', 1, 2, null, null, null, current_timestamp), + (63, 'follow', 1, 2, null, null, null, current_timestamp), + (64, 'follow', 1, 2, null, null, null, current_timestamp), + (65, 'follow', 1, 2, null, null, null, current_timestamp); \ No newline at end of file diff --git a/hideout-mastodon/src/test/resources/sql/posts.sql b/hideout-mastodon/src/test/resources/sql/posts.sql index f8dec6b9..cf8c71bf 100644 --- a/hideout-mastodon/src/test/resources/sql/posts.sql +++ b/hideout-mastodon/src/test/resources/sql/posts.sql @@ -1,15 +1,15 @@ insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, deleted, hide, move_to) values (1, 1, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false, - 'https://example.com', false, false, null), + 'https://example.com/1', false, false, null), (2, 2, 2, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false, - 'https://example.com', false, false, null), + 'https://example.com/2', false, false, null), (3, 3, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false, - 'https://example.com', false, false, null), + 'https://example.com/3', false, false, null), (4, 4, 1, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false, - 'https://example.com', false, false, null), + 'https://example.com/4', false, false, null), (5, 4, 1, null, 'content', 'text', current_timestamp, 'DIRECT', 'https://example.com', null, null, false, - 'https://example.com', false, false, null); + 'https://example.com/5', false, false, null); insert into posts_visible_actors(post_id, actor_id) VALUES (5, 2); \ No newline at end of file diff --git a/hideout-mastodon/src/test/resources/sql/test-custom-emoji.sql b/hideout-mastodon/src/test/resources/sql/test-custom-emoji.sql new file mode 100644 index 00000000..83c2747a --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/test-custom-emoji.sql @@ -0,0 +1,3 @@ +insert into emojis(id, name, domain, instance_id, url, category, created_at) +VALUES (1, 'kotlin', 'example.com', null, 'https://example.com/emojis/kotlin', null, + TIMESTAMP '2024-01-08 07:51:30.036Z'); diff --git a/hideout-mastodon/src/test/resources/sql/test-post.sql b/hideout-mastodon/src/test/resources/sql/test-post.sql new file mode 100644 index 00000000..da7c5343 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/test-post.sql @@ -0,0 +1,4 @@ +insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, + sensitive, ap_id, deleted, hide, move_to) +VALUES (1, 1, 0, null,'

test post

', 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false, + 'https://users/1/posts/1'); diff --git a/hideout-mastodon/src/test/resources/sql/test-user.sql b/hideout-mastodon/src/test/resources/sql/test-user.sql new file mode 100644 index 00000000..728af6f5 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/test-user.sql @@ -0,0 +1,11 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', + 'https://example.com/users/test-user/inbox', + 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', + 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null); + diff --git a/hideout-mastodon/src/test/resources/sql/test-user2.sql b/hideout-mastodon/src/test/resources/sql/test-user2.sql new file mode 100644 index 00000000..0f736704 --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/test-user2.sql @@ -0,0 +1,10 @@ +insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, key_id, following, followers, instance, locked, following_count, followers_count, + posts_count, last_post_at) +VALUES (2, 'test-user2', 'example.com', 'Im test user.', 'THis account is test user.', + 'https://example.com/users/test-user2/inbox', + 'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following', + 'https://example.com/users/test-user2s/followers', 0, false, 0, 0, 0, null); diff --git a/hideout-mastodon/src/test/resources/sql/userdetail.sql b/hideout-mastodon/src/test/resources/sql/userdetail.sql new file mode 100644 index 00000000..c0b3ee2d --- /dev/null +++ b/hideout-mastodon/src/test/resources/sql/userdetail.sql @@ -0,0 +1,2 @@ +insert into user_details(id, actor_id, password, auto_accept_followee_follow_request, last_migration, home_timeline_id) +VALUES (1, 1, 'veeeeeeeeeeryStrongPassword', false, null, null); \ No newline at end of file