From 3663ca1ec189a44a40a3214fd5d801f24ae34f48 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 24 Oct 2023 23:55:45 +0530 Subject: [PATCH] feat(api): add integration tests for Shiori API --- api/build.gradle.kts | 2 + .../dev/msfjarvis/claw/api/ShioriApiTest.kt | 173 ++++++++++++++++++ gradle/libs.versions.toml | 1 + 3 files changed, 176 insertions(+) create mode 100644 api/src/test/kotlin/dev/msfjarvis/claw/api/ShioriApiTest.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 2a5e3df1..adc87082 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -27,7 +27,9 @@ dependencies { implementation(libs.jsoup) testImplementation(testFixtures(libs.eithernet)) + testImplementation(libs.testcontainers) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.serialization.json) + testImplementation(libs.retrofit.kotlinxSerializationConverter) addTestDependencies(project) } diff --git a/api/src/test/kotlin/dev/msfjarvis/claw/api/ShioriApiTest.kt b/api/src/test/kotlin/dev/msfjarvis/claw/api/ShioriApiTest.kt new file mode 100644 index 00000000..8d25a0a0 --- /dev/null +++ b/api/src/test/kotlin/dev/msfjarvis/claw/api/ShioriApiTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.api + +import com.google.common.truth.Truth.assertThat +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dev.msfjarvis.claw.model.shiori.AuthRequest +import dev.msfjarvis.claw.model.shiori.AuthResponse +import dev.msfjarvis.claw.model.shiori.Bookmark +import dev.msfjarvis.claw.model.shiori.BookmarkRequest +import dev.msfjarvis.claw.model.shiori.EditedBookmark +import dev.msfjarvis.claw.model.shiori.Tag +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.testcontainers.containers.GenericContainer +import retrofit2.Retrofit +import retrofit2.create + +class ShioriApiTest { + + private lateinit var credentials: AuthResponse + + @BeforeEach + fun setUp() { + runBlocking { credentials = api.login(AuthRequest(USER, PASSWORD)) } + } + + @AfterEach + fun tearDown() { + runBlocking { + val ids = api.getBookmarks(credentials.session).bookmarks.map(Bookmark::id) + if (ids.isNotEmpty()) { + api.deleteBookmark(credentials.session, ids) + } + api.logout(credentials.session) + } + } + + @Test + fun getBookmarks() = runTest { + val response = api.getBookmarks(credentials.session) + assertThat(response.page).isEqualTo(1) + assertThat(response.bookmarks).isEmpty() + } + + @Test + fun addBookmark() = runTest { + val response = + api.addBookmark( + credentials.session, + BookmarkRequest( + "https://example.com", + false, + 0, + emptyList(), + "Example Domain", + """ + This domain is for use in illustrative examples in documents. + You may use this domain in literature without prior coordination or asking for permission. + """ + .trimIndent(), + ) + ) + assertThat(response.url).isEqualTo("https://example.com") + assertThat(response.title).isEqualTo("Example Domain") + assertThat(response.excerpt) + .isEqualTo( + """ + This domain is for use in illustrative examples in documents. + You may use this domain in literature without prior coordination or asking for permission. + """ + .trimIndent() + ) + assertThat(response.id).isAtLeast(0) + } + + @Test + @Disabled("Server returns HTTP 500, needs debugging") + fun editBookmark() = runTest { + val response = + api.addBookmark( + credentials.session, + BookmarkRequest( + "https://example.com", + false, + 0, + emptyList(), + "Example Domain", + """ + This domain is for use in illustrative examples in documents. + You may use this domain in literature without prior coordination or asking for permission. + """ + .trimIndent(), + ) + ) + assertThat(response.tags).isEmpty() + val newBookmark = + EditedBookmark( + id = response.id, + tags = listOf(Tag("examples")), + ) + val edited = api.editBookmark(credentials.session, newBookmark) + assertThat(edited.tags).isNotEmpty() + assertThat(edited.tags).containsExactly(Tag("examples")) + } + + @Test + fun deleteBookmark() = runTest { + val response = + api.addBookmark( + credentials.session, + BookmarkRequest( + "https://example.com", + false, + 0, + emptyList(), + "Example Domain", + """ + This domain is for use in illustrative examples in documents. + You may use this domain in literature without prior coordination or asking for permission. + """ + .trimIndent(), + ) + ) + val count = api.deleteBookmark(credentials.session, listOf(response.id)) + assertThat(count).isEqualTo(1) + } + + companion object { + // Default settings for the container + private const val USER = "shiori" + private const val PASSWORD = "gopher" + + private val container = + GenericContainer("ghcr.io/go-shiori/shiori:v1.5.5").withExposedPorts(8080) + + private val json = Json { ignoreUnknownKeys = true } + + // The mapped port can only be obtained after the container has started, so this has to be lazy. + private val api by + lazy(LazyThreadSafetyMode.NONE) { + Retrofit.Builder() + .baseUrl("http://${container.host}:${container.firstMappedPort}") + .addConverterFactory(json.asConverterFactory(MediaType.get("application/json"))) + .build() + .create() + } + + @JvmStatic + @BeforeAll + fun setupContainer() { + container.start() + } + + @JvmStatic + @AfterAll + fun destroyContainer() { + container.stop() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a85c047..4976f85e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -97,6 +97,7 @@ sqldelight-jvmDriver = { module = "app.cash.sqldelight:sqlite-driver", version.r sqldelight-primitiveAdapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "sqldelight" } sqlite-android = "com.github.requery:sqlite-android:3.43.0" swipe = "me.saket.swipe:swipe:1.3.0-SNAPSHOT" +testcontainers = "org.testcontainers:testcontainers:1.19.1" truth = "com.google.truth:truth:1.1.5" unfurl = "me.saket.unfurl:unfurl:1.7.0"