Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: E2E

# Manual only: run from the Actions tab ("Run workflow") or `gh workflow run e2e.yml`
on:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- run: ./gradlew :app:e2eTest
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-test-reports
path: app/build/reports/tests/e2eTest
if-no-files-found: ignore
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ The `it.vercruysse.lemmyapi.datatypes` package represents the **1.0.0 Lemmy API*
- **Enum entries**: each entry tracks `minimumVersion`/`maximumVersion`. Use `getSupportedEntries<Type>()` to retrieve the valid entries for the connected instance (e.g. sort options).


## End-to-end tests

`gradlew :app:e2eTest` runs the library against a real Lemmy server (currently v1 only) started with Testcontainers, so a running Docker engine is required.


## Compatibility Rules

When adding or updating an older-version implementation:
Expand Down
14 changes: 8 additions & 6 deletions GeneratorScripts/src/main/kotlin/autogenscripts/GenTypes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ val fileFieldEnumOverrides = mapOf(
// Maps the interface name to its @JsonClassDiscriminator field, and whether it needs @Serializable.
data class SealedInterface(val discriminator: String, val serializable: Boolean = false)

// TODO: don't know yet if serializable required but also it breaks the generated code
// Interfaces that are deserialized polymorphically (as response fields) must be @Serializable,
// otherwise the class discriminator is not resolved at runtime.
// TODO refactor
val sealedInterfaces = mapOf(
"PostOrCommentOrPrivateMessage" to SealedInterface("type_"),
"ResolveObjectView" to SealedInterface("type_"),
"ReportCombinedView" to SealedInterface("type_"),
"NotificationData" to SealedInterface("type_", serializable = false),
"PostCommentCombinedView" to SealedInterface("type_"),
"PostOrCommentOrPrivateMessage" to SealedInterface("type_", serializable = true),
"ResolveObjectView" to SealedInterface("type_", serializable = true),
"ReportCombinedView" to SealedInterface("type_", serializable = true),
"NotificationData" to SealedInterface("type_", serializable = true),
"PostCommentCombinedView" to SealedInterface("type_", serializable = true),
)

// Which data classes implement which sealed interfaces and what @SerialName they carry.
Expand Down
28 changes: 28 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ kotlin {
useJUnitPlatform()
}
}

// Separate compilation so the Docker-based E2E tests never run as part of `jvmTest`/`check`
val mainCompilation = compilations.getByName("main")
compilations.create("e2eTest") {
associateWith(mainCompilation)
}
}

android {
Expand Down Expand Up @@ -92,6 +98,13 @@ kotlin {
implementation("io.ktor:ktor-client-mock:$ktorVersion")
}

getByName("jvmE2eTest").dependencies {
implementation(kotlin("test-junit5"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
implementation("org.testcontainers:testcontainers:1.21.4")
implementation("ch.qos.logback:logback-classic:1.6.3")
}

// Below required bc we use HttpClient() see https://ktor.io/docs/client-engines.html#default

jsMain.dependencies {
Expand Down Expand Up @@ -129,6 +142,21 @@ kotlin {
}


val e2eCompilation = kotlin.jvm().compilations.getByName("e2eTest")

tasks.register<Test>("e2eTest") {
description = "Runs JVM E2E tests against a local Lemmy docker setup."
group = "verification"
testClassesDirs = e2eCompilation.output.classesDirs
classpath = files(e2eCompilation.output.allOutputs, e2eCompilation.runtimeDependencyFiles)
useJUnitPlatform()
outputs.upToDateWhen { false }
testLogging {
events("passed", "failed", "skipped")
showStandardStreams = true
}
}

tasks.withType<LintTask> {
exclude("**/datatypes/**")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ suspend inline fun <reified R> HttpClient.postUploadResult(
MultiPartFormDataContent(
formData {
append(
"image", // TODO: this might need to be images[]
"images[]",
image,
Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"image.jpg\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun getModlog(form: it.vercruysse.lemmyapi.datatypes.GetModlog): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.ModlogView>> =
api.getModlog(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun search(form: it.vercruysse.lemmyapi.datatypes.Search): Result<it.vercruysse.lemmyapi.datatypes.SearchResponse> =
Expand All @@ -55,7 +55,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listCommunities(form: it.vercruysse.lemmyapi.datatypes.ListCommunities): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.CommunityView>> =
api.listCommunities(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun followCommunity(form: it.vercruysse.lemmyapi.datatypes.FollowCommunity): Result<it.vercruysse.lemmyapi.datatypes.CommunityResponse> =
Expand All @@ -81,7 +81,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun getFederatedInstances(form: it.vercruysse.lemmyapi.datatypes.GetFederatedInstances): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.FederatedInstanceView>> =
api.getFederatedInstances(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun getPost(form: it.vercruysse.lemmyapi.datatypes.GetPost): Result<it.vercruysse.lemmyapi.datatypes.GetPostResponse> =
Expand All @@ -95,7 +95,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun getPosts(form: it.vercruysse.lemmyapi.datatypes.GetPosts): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostView>> =
api.getPosts(transformer.fromUni(form.toValidatedForm())).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun deletePost(form: it.vercruysse.lemmyapi.datatypes.DeletePost): Result<it.vercruysse.lemmyapi.datatypes.PostResponse> =
Expand Down Expand Up @@ -130,7 +130,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listReports(form: it.vercruysse.lemmyapi.datatypes.ListReports): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.ReportCombinedView>> =
api.listReports(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun getSiteMetadata(form: it.vercruysse.lemmyapi.datatypes.GetSiteMetadata): Result<it.vercruysse.lemmyapi.datatypes.GetSiteMetadataResponse> =
Expand All @@ -147,7 +147,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun getComments(form: it.vercruysse.lemmyapi.datatypes.GetComments): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.CommentView>> =
api.getComments(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun deleteComment(form: it.vercruysse.lemmyapi.datatypes.DeleteComment): Result<it.vercruysse.lemmyapi.datatypes.CommentResponse> =
Expand Down Expand Up @@ -191,7 +191,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listPersons(form: it.vercruysse.lemmyapi.datatypes.ListPersons): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PersonView>> =
api.listPersons(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun register(form: it.vercruysse.lemmyapi.datatypes.Register): Result<it.vercruysse.lemmyapi.datatypes.LoginResponse> =
Expand All @@ -208,7 +208,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listNotifications(form: ListNotifications): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.NotificationView>> =
api.listNotifications(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun banPerson(form: it.vercruysse.lemmyapi.datatypes.BanPerson): Result<it.vercruysse.lemmyapi.datatypes.PersonResponse> =
Expand Down Expand Up @@ -255,7 +255,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listRegistrationApplications(form: it.vercruysse.lemmyapi.datatypes.ListRegistrationApplications): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.RegistrationApplicationView>> =
api.listRegistrationApplications(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun approveRegistrationApplication(form: it.vercruysse.lemmyapi.datatypes.ApproveRegistrationApplication): Result<it.vercruysse.lemmyapi.datatypes.RegistrationApplicationResponse> =
Expand Down Expand Up @@ -314,12 +314,12 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listPostLikes(form: it.vercruysse.lemmyapi.datatypes.ListPostLikes): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.VoteView>> =
api.listPostLikes(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listCommentLikes(form: it.vercruysse.lemmyapi.datatypes.ListCommentLikes): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.VoteView>> =
api.listCommentLikes(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun createRegistrationInvitation(form: it.vercruysse.lemmyapi.datatypes.CreateInvitation): Result<it.vercruysse.lemmyapi.datatypes.CreateInvitationResponse> =
Expand All @@ -330,17 +330,17 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listRegistrationInvitations(form: it.vercruysse.lemmyapi.datatypes.ListInvitations): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.LocalUserInvite>> =
api.listRegistrationInvitations(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listMedia(form: it.vercruysse.lemmyapi.datatypes.ListMedia): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.LocalImageView>> =
api.listMedia(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listMediaAdmin(form: it.vercruysse.lemmyapi.datatypes.ListMedia): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.LocalImageView>> =
api.listMediaAdmin(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun hidePost(form: it.vercruysse.lemmyapi.datatypes.HidePost): Result<it.vercruysse.lemmyapi.datatypes.PostResponse> =
Expand All @@ -353,7 +353,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listPersonContent(form: it.vercruysse.lemmyapi.datatypes.ListPersonContent): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostCommentCombinedView>> =
api.listPersonContent(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun uploadImage(image: ByteArray): Result<it.vercruysse.lemmyapi.datatypes.UploadImageResponse> =
Expand Down Expand Up @@ -406,7 +406,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun adminListUsers(form: it.vercruysse.lemmyapi.datatypes.AdminListUsers): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.LocalUserView>> =
api.adminListUsers(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun getRandomCommunity(form: it.vercruysse.lemmyapi.datatypes.GetRandomCommunity): Result<it.vercruysse.lemmyapi.datatypes.CommunityResponse> =
Expand Down Expand Up @@ -435,7 +435,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listCommunityPendingFollows(form: it.vercruysse.lemmyapi.datatypes.ListCommunityPendingFollows): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PendingFollowerView>> =
api.listCommunityPendingFollows(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun modEditPost(form: it.vercruysse.lemmyapi.datatypes.ModEditPost): Result<it.vercruysse.lemmyapi.datatypes.PostResponse> =
Expand All @@ -449,7 +449,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun getCommentsSlim(form: it.vercruysse.lemmyapi.datatypes.GetComments): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.CommentSlimView>> =
api.getCommentsSlim(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun lockComment(form: it.vercruysse.lemmyapi.datatypes.LockComment): Result<it.vercruysse.lemmyapi.datatypes.CommentResponse> =
Expand All @@ -463,22 +463,22 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listPersonSaved(form: it.vercruysse.lemmyapi.datatypes.ListPersonSaved): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostCommentCombinedView>> =
api.listPersonSaved(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listPersonRead(form: it.vercruysse.lemmyapi.datatypes.ListPersonRead): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostView>> =
api.listPersonRead(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listPersonHidden(form: it.vercruysse.lemmyapi.datatypes.ListPersonHidden): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostView>> =
api.listPersonHidden(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun listPersonLiked(form: it.vercruysse.lemmyapi.datatypes.ListPersonLiked): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.PostCommentCombinedView>> =
api.listPersonLiked(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun resendVerificationEmail(form: it.vercruysse.lemmyapi.datatypes.ResendVerificationEmail): Result<Unit> =
Expand Down Expand Up @@ -507,7 +507,7 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listTaglines(form: it.vercruysse.lemmyapi.datatypes.ListTaglines): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.Tagline>> =
api.listTaglines(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}

override suspend fun createOAuthProvider(form: it.vercruysse.lemmyapi.datatypes.CreateOAuthProvider): Result<it.vercruysse.lemmyapi.datatypes.AdminOAuthProvider> =
Expand Down Expand Up @@ -539,6 +539,6 @@ internal class LemmyApiUniWrapper(client: HttpClient, apiBaseUrl: String, instan

override suspend fun listMultiCommunities(form: it.vercruysse.lemmyapi.datatypes.ListMultiCommunities): Result<PagedResponse<it.vercruysse.lemmyapi.datatypes.MultiCommunityView>> =
api.listMultiCommunities(transformer.fromUni(form)).map { resp ->
PagedResponse(resp.items.map(transformer::toUni))
PagedResponse(resp.items.map(transformer::toUni), resp.next_page, resp.prev_page)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package it.vercruysse.lemmyapi.v1.x0.x0.datatypes

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
internal sealed interface NotificationData
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package it.vercruysse.lemmyapi.v1.x0.x0.datatypes

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
internal sealed interface PostCommentCombinedView
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package it.vercruysse.lemmyapi.v1.x0.x0.datatypes

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
internal sealed interface PostOrCommentOrPrivateMessage
Loading
Loading