diff --git a/.gitignore b/.gitignore index 93acc85..fea1822 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ # Ignore Gradle build output directory build +.idea/ .env *.tar *.tar.gz -releases/ \ No newline at end of file +releases/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 4979975..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 146ab09..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 206430d..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index e805548..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 2e3f9ea..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0bd20f6..964b583 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,3 +69,7 @@ tasks.named("test") { // Use JUnit Platform for unit tests. useJUnitPlatform() } + +tasks.withType().configureEach { + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") +} diff --git a/app/src/main/kotlin/de/rpr/githubreleases/App.kt b/app/src/main/kotlin/de/rpr/githubreleases/App.kt index 95f9424..7e8a4d4 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/App.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/App.kt @@ -6,7 +6,8 @@ import java.util.concurrent.TimeUnit fun main() { val config = Config() - val publishers = Publishers(config) + val mastodonClientFactory = MastodonClientFactory() + val publishers = Publishers(config, mastodonClientFactory) val releaseRepository = ReleaseRepository() val app = App(config, releaseRepository, OkHttpClient(), publishers) app.schedule() diff --git a/app/src/main/kotlin/de/rpr/githubreleases/Config.kt b/app/src/main/kotlin/de/rpr/githubreleases/Config.kt index cab906f..383e682 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/Config.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/Config.kt @@ -49,11 +49,13 @@ class Config { } mastodonCredentials = githubRepos.associate { + val instanceUrlEnvName = "${it.name.uppercase()}_MASTODON_INSTANCE_URL" + val accessTokenEnvName = "${it.name.uppercase()}_MASTODON_ACCESS_TOKEN" it.name to PublishingCredentials( - instanceUrlEnvName = "${it.name.uppercase()}_INSTANCE_URL", - instanceUrl = System.getenv("${it.name.uppercase()}_INSTANCE_URL"), - accessTokenEnvName = "${it.name.uppercase()}_ACCESS_TOKEN", - accessToken = System.getenv("${it.name.uppercase()}_ACCESS_TOKEN") + instanceUrlEnvName = instanceUrlEnvName, + instanceUrl = System.getenv(instanceUrlEnvName), + accessTokenEnvName = accessTokenEnvName, + accessToken = System.getenv(accessTokenEnvName) ) }.onEach { (_, credentials) -> if (!credentials.valid) { diff --git a/app/src/main/kotlin/de/rpr/githubreleases/MastodonClientFactory.kt b/app/src/main/kotlin/de/rpr/githubreleases/MastodonClientFactory.kt new file mode 100644 index 0000000..a8561ec --- /dev/null +++ b/app/src/main/kotlin/de/rpr/githubreleases/MastodonClientFactory.kt @@ -0,0 +1,8 @@ +package de.rpr.githubreleases + +import social.bigbone.MastodonClient + +class MastodonClientFactory { + fun getClient(url: String, accessToken: String) = MastodonClient.Builder(url) + .accessToken(accessToken).build() +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/rpr/githubreleases/Publisher.kt b/app/src/main/kotlin/de/rpr/githubreleases/Publisher.kt index ac73a8c..b22b1da 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/Publisher.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/Publisher.kt @@ -37,22 +37,29 @@ class Publisher( } } -class Publishers(private val config: Config) : Iterable { +class Publishers( + private val config: Config, + private val mastodonClientFactory: MastodonClientFactory, +) : + Iterable { private val instances: List init { val mastodonClients = config.githubRepos.associate { val publishingCredentials = config.mastodonCredentials[it.name]!! - it.name to MastodonClient.Builder(publishingCredentials.instanceUrl!!) - .accessToken(publishingCredentials.accessToken!!).build() + it.name to mastodonClientFactory.getClient( + publishingCredentials.instanceUrl!!, + publishingCredentials.accessToken!! + ) } instances = config.githubRepos.map { Publisher(it.name, mastodonClients[it.name]!!) } } - fun forName(name: String) = instances.first() { it.name == name } + fun forName(name: String) = instances.first { it.name == name } override fun iterator(): Iterator { return instances.iterator() } -} \ No newline at end of file +} + diff --git a/app/src/main/kotlin/de/rpr/githubreleases/Releases.kt b/app/src/main/kotlin/de/rpr/githubreleases/Releases.kt index 05c17b6..d7ab887 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/Releases.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/Releases.kt @@ -13,6 +13,7 @@ data class Release( "\uD83C\uDF89 Navidrome $title has been published!\n\nRelease notes are available here: $link\n\n#Navidrome" } +fun Release.asCollection() = listOf(this).asCollection() fun List.asCollection() = Releases(this) class Releases(releases: List) : List { diff --git a/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt b/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt new file mode 100644 index 0000000..dde1ec7 --- /dev/null +++ b/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt @@ -0,0 +1,49 @@ +package de.rpr.githubreleases + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsOnly +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.extensions.system.withEnvironment +import org.junit.jupiter.api.assertThrows + +class ConfigTest : DescribeSpec({ + + describe("instantiation") { + + withEnvironment( + mapOf( + "GITHUB_REPOS" to "https://github.com/navidrome/navidrome/", + "NAVIDROME_MASTODON_INSTANCE_URL" to "example.com", + "NAVIDROME_MASTODON_ACCESS_TOKEN" to "token" + ) + ) { + + it("should populate instances from config") { + val config = Config() + assertThat(config.githubRepos).containsExactly( + GithubRepo("navidrome", "https://github.com/navidrome/navidrome/") + ) + assertThat(config.mastodonCredentials).containsOnly( + "navidrome" to Config.PublishingCredentials( + instanceUrl = "example.com", + accessToken = "token", + instanceUrlEnvName = "NAVIDROME_MASTODON_INSTANCE_URL", + accessTokenEnvName = "NAVIDROME_MASTODON_ACCESS_TOKEN" + ) + ) + } + } + + withEnvironment( + mapOf("GITHUB_REPOS" to "https://github.com/navidrome/navidrome/") + ) { + + it("should throw error if env variables for repo are not present") { + assertThrows { + Config() + } + } + } + } +}) \ No newline at end of file diff --git a/app/src/test/kotlin/de/rpr/githubreleases/PublishersTest.kt b/app/src/test/kotlin/de/rpr/githubreleases/PublishersTest.kt new file mode 100644 index 0000000..9f3419b --- /dev/null +++ b/app/src/test/kotlin/de/rpr/githubreleases/PublishersTest.kt @@ -0,0 +1,132 @@ +package de.rpr.githubreleases + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import assertk.assertions.prop +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.extensions.system.withEnvironment +import io.mockk.* +import social.bigbone.MastodonClient +import social.bigbone.MastodonRequest +import social.bigbone.api.entity.Status +import social.bigbone.api.entity.data.Visibility +import social.bigbone.api.method.StatusMethods + +class PublishersTest : DescribeSpec({ + + val mastodonClientFactory = mockk() + val mastodonClient = mockk() + + val statuses = mockk(relaxed = true) + val mockRequest = mockk>(relaxed = true) + + beforeEach { + clearAllMocks() + every { mastodonClient.statuses } returns statuses + every { mastodonClientFactory.getClient(any(), any()) } returns mastodonClient + } + + describe("Publishers collection") { + + withEnvironment( + mapOf( + "GITHUB_REPOS" to "https://github.com/navidrome/navidrome/", + "NAVIDROME_MASTODON_INSTANCE_URL" to "example.com", + "NAVIDROME_MASTODON_ACCESS_TOKEN" to "token" + ) + ) { + + it("should populate instances from config") { + val publishers = Publishers(Config(), mastodonClientFactory) + assertThat(publishers.iterator().hasNext()).isTrue() + assertThat(publishers.forName("navidrome")) + .prop("name") { it.name } + .isEqualTo("navidrome") + } + } + } + + describe("Publisher") { + + val publisher = Publisher("example", mastodonClient) + + xit("should send with correct visibility") { + + every { + statuses.postStatus( + spoilerText = any(), + status = any(), + language = any(), + visibility = any() + ) + } returns mockRequest + + publisher.sendReleases(Releases(testRelease)) + + verify { + statuses.postStatus( + status = any(), + mediaIds = any(), + visibility = Visibility.PUBLIC, + inReplyToId = any(), + sensitive = any(), + spoilerText = any(), + language = "en", + addIdempotencyKey = any() + ) + } + } + + it("should send correct status text") { + every { + statuses.postStatus( + spoilerText = any(), + status = any(), + language = any(), + visibility = any() + ) + } returns mockRequest + + publisher.sendReleases(testRelease.copy(id = "v0.0.0").asCollection()) + + val statusSlot = slot() + + verify { + statuses.postStatus( + status = capture(statusSlot), + mediaIds = any(), + visibility = any(), + inReplyToId = any(), + sensitive = any(), + spoilerText = any(), + language = any(), + addIdempotencyKey = any() + ) + } + + assertThat(statusSlot.captured).isEqualTo("\uD83C\uDF89 Navidrome v0.0.0 has been published!\n\nRelease notes are available here: https://example.com\n\n#Navidrome") + } + } + + describe("Dry-run mode enabled") { + + val publisher = Publisher(name = "example", client = mastodonClient, dryRun = true) + + it("should not actually send events") { + + every { + statuses.postStatus( + spoilerText = any(), + status = any(), + language = any(), + visibility = any() + ) + } returns mockRequest + + publisher.sendReleases(testRelease.asCollection()) + + verify(exactly = 0) { mockRequest.execute() } + } + } +}) \ No newline at end of file