Skip to content

Commit 5061bc0

Browse files
committed
Add integration tests, some refactorings
1 parent 6eff306 commit 5061bc0

File tree

13 files changed

+281
-24
lines changed

13 files changed

+281
-24
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ jobs:
2424
uses: mikepenz/action-junit-report@v4
2525
if: always()
2626
with:
27-
report_paths: '**/build/test-results/test/TEST-*.xml'
27+
report_paths: '**/build/test-results/*/TEST-*.xml'

build.gradle.kts

+43-4
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ repositories {
2424
}
2525

2626
sourceSets {
27-
create("dev") {
28-
compileClasspath += sourceSets.main.get().output
29-
runtimeClasspath += sourceSets.main.get().output
27+
val main = main.get()
28+
val dev = create("dev") {
29+
compileClasspath += main.output
30+
runtimeClasspath += main.output
31+
}
32+
test {
33+
compileClasspath += dev.output
34+
runtimeClasspath += dev.output
35+
}
36+
create("testIntegration") {
37+
compileClasspath += main.output + test.get().output + dev.output
38+
runtimeClasspath += main.output + test.get().output + dev.output
3039
}
3140
}
3241

@@ -91,12 +100,17 @@ dependencies {
91100
implementation(libraries.jobrunr)
92101

93102
// testing
103+
testImplementation(tests.junit.api)
104+
testRuntimeOnly(tests.junit.engine)
105+
testRuntimeOnly(tests.junit.launcher)
94106
testImplementation(tests.ktor.server.tests)
95-
testImplementation(tests.kotlin.test.junit)
96107
testImplementation(tests.mockk)
97108
testImplementation(tests.assertj.core)
98109
testImplementation(tests.json)
99110
testImplementation(tests.h2)
111+
testImplementation(tests.testcontainers)
112+
testImplementation(tests.testcontainers.junit)
113+
testImplementation(tests.testcontainers.keycloak)
100114

101115
// dev
102116
devImplementation(dev.keycloak.adminClient)
@@ -115,6 +129,29 @@ application {
115129

116130
tasks.test {
117131
jvmArgs("-Dio.ktor.development=true")
132+
useJUnitPlatform()
133+
}
134+
135+
val integrationTest: SourceSet = sourceSets["testIntegration"]
136+
137+
138+
configurations[integrationTest.implementationConfigurationName].extendsFrom(configurations.testImplementation.get())
139+
configurations[integrationTest.runtimeOnlyConfigurationName].extendsFrom(configurations.testRuntimeOnly.get())
140+
141+
val integrationTestTask = tasks.register<Test>("integrationTest") {
142+
group = "verification"
143+
description = "Runs tests against integrations (database, LMS, etc)"
144+
145+
useJUnitPlatform()
146+
147+
testClassesDirs = integrationTest.output.classesDirs
148+
classpath = integrationTest.runtimeClasspath
149+
150+
shouldRunAfter("test")
151+
}
152+
153+
tasks.check {
154+
dependsOn(integrationTestTask)
118155
}
119156

120157
buildInfo {
@@ -192,6 +229,8 @@ idea {
192229
// long import times but worth it as, without it, functions may not have proper documentation
193230
isDownloadJavadoc = true
194231
isDownloadSources = true
232+
sourceDirs.minusAssign(file("src/testIntegration"))
233+
testSources.from(file("src/testIntegration/kotlin"))
195234
}
196235

197236
project {

settings.gradle.kts

+17-2
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,6 @@ dependencyResolutionManagement {
141141

142142
create("tests") {
143143
ktorServerPlugin("tests", prefix = "ktor")
144-
library("kotlin-test-junit", "org.jetbrains.kotlin", "kotlin-test-junit")
145-
.version(kotlinVersion)
146144
library("mockk", "io.mockk", "mockk")
147145
.version("1.13.10")
148146
library("assertj-core", "org.assertj", "assertj-core")
@@ -151,6 +149,23 @@ dependencyResolutionManagement {
151149
.version("20240303")
152150
library("h2", "com.h2database", "h2")
153151
.version("2.2.224")
152+
153+
val testcontainers = version("testcontainers", "1.19.7")
154+
library("testcontainers", "org.testcontainers", "testcontainers")
155+
.versionRef(testcontainers)
156+
library("testcontainers-junit", "org.testcontainers", "junit-jupiter")
157+
.versionRef(testcontainers)
158+
library("testcontainers-keycloak", "com.github.dasniko", "testcontainers-keycloak")
159+
.version("3.3.1")
160+
161+
val junit = version("junit", "5.10.2")
162+
library("junit-api", "org.junit.jupiter", "junit-jupiter")
163+
.versionRef(junit)
164+
library("junit-engine", "org.junit.jupiter", "junit-jupiter-engine")
165+
.versionRef(junit)
166+
// required to run tests with IntelliJ
167+
library("junit-launcher", "org.junit.platform", "junit-platform-launcher")
168+
.version("1.10.2")
154169
}
155170

156171
create("dev") {

src/main/kotlin/me/snoty/backend/integration/common/Integration.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ abstract class AbstractIntegration<S : Any>(
2525
schedulerFactory: IntegrationSchedulerFactory<S>,
2626
database: Database,
2727
meterRegistry: MeterRegistry,
28-
metricsPool: ScheduledExecutorService
28+
private val metricsPool: ScheduledExecutorService
2929
) : Integration {
3030
constructor(
3131
name: String,
@@ -49,12 +49,11 @@ abstract class AbstractIntegration<S : Any>(
4949
override val scheduler: IntegrationScheduler<Any>
5050
= schedulerFactory.create(entityDiffMetrics) as IntegrationScheduler<Any>
5151

52-
init {
52+
override fun start() {
5353
metricsPool.scheduleAtFixedRate(entityDiffMetrics.Job(), 0, 30, TimeUnit.SECONDS)
54+
scheduleAll()
5455
}
5556

56-
override fun start() = scheduleAll()
57-
5857
private fun scheduleAll() {
5958
val allSettings = IntegrationConfigTable.getAllIntegrationConfigs<S>(name)
6059
allSettings.forEach {

src/main/kotlin/me/snoty/backend/server/plugins/Security.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ fun ApplicationCall.getUserOrNull(): User? {
8484
val claims = principal.payload.claims
8585
return User(
8686
id = claims["sub"]?.`as`(UUID::class.java) ?: NULL_UUID,
87-
name = claims["name"]?.asString() ?: "unknown",
87+
name = claims["preferred_username"]?.asString() ?: "unknown",
8888
email = claims["email"]?.asString() ?: "unknown"
8989
)
9090
}

src/test/kotlin/me/snoty/backend/BuildInfoTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import io.ktor.http.*
66
import me.snoty.backend.test.ktorApplicationTest
77
import org.assertj.core.api.Assertions
88
import org.json.JSONObject
9-
import kotlin.test.Test
10-
import kotlin.test.assertEquals
9+
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Test
1111

1212
class BuildInfoTest {
1313
@Test

src/test/kotlin/me/snoty/backend/ErrorHandlingTest.kt

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import io.ktor.client.statement.*
55
import io.ktor.http.*
66
import io.ktor.server.routing.*
77
import me.snoty.backend.config.Environment
8+
import me.snoty.backend.server.handler.NotFoundException
89
import me.snoty.backend.test.TestConfig
10+
import me.snoty.backend.test.assertErrorResponse
911
import me.snoty.backend.test.ktorApplicationTest
1012
import org.assertj.core.api.Assertions
1113
import org.json.JSONObject
12-
import kotlin.test.Test
13-
import kotlin.test.assertEquals
14+
import org.junit.jupiter.api.Assertions.assertEquals
15+
import org.junit.jupiter.api.Test
1416

1517
class ErrorHandlingTest {
1618
@Test
@@ -19,13 +21,12 @@ class ErrorHandlingTest {
1921
assertEquals(HttpStatusCode.NotFound, status)
2022
Assertions.assertThat(bodyAsText())
2123
.isNotEmpty()
22-
val body = JSONObject(bodyAsText()).toMap()
24+
val body = JSONObject(bodyAsText())
2325

24-
Assertions.assertThat(body)
25-
.containsEntry("code", 404)
26-
.containsEntry("message", "Not Found")
26+
assertErrorResponse(body, NotFoundException())
2727
}
2828
}
29+
2930
@Test
3031
fun testInternalServerErrorCatchAll_DevMode() =
3132
internalServerErrorCatchAllTest(Environment.DEVELOPMENT, "test", "test")
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package me.snoty.backend.test
22

3+
import me.snoty.backend.server.handler.HttpStatusException
4+
import org.json.JSONObject
35
import org.junit.Assert
46
import org.junit.function.ThrowingRunnable
57

68
inline fun <reified T : Throwable> assertThrows(block: ThrowingRunnable) {
79
Assert.assertThrows(T::class.java, block)
810
}
11+
12+
fun assertErrorResponse(body: JSONObject, exception: HttpStatusException) {
13+
Assert.assertEquals(exception.code.value, body.getInt("code"))
14+
Assert.assertEquals(exception.message, body.getString("message"))
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package me.snoty.backend.test
2+
3+
import io.ktor.client.*
4+
import io.ktor.client.call.*
5+
import io.ktor.client.plugins.contentnegotiation.*
6+
import io.ktor.client.request.forms.*
7+
import io.ktor.http.*
8+
import io.ktor.serialization.kotlinx.json.*
9+
import kotlinx.serialization.SerialName
10+
import kotlinx.serialization.Serializable
11+
import kotlinx.serialization.json.Json
12+
import me.snoty.backend.config.OidcConfig
13+
import org.keycloak.admin.client.CreatedResponseUtil
14+
import org.keycloak.admin.client.resource.RealmResource
15+
import org.keycloak.admin.client.resource.UsersResource
16+
import org.keycloak.representations.idm.CredentialRepresentation
17+
import org.keycloak.representations.idm.UserRepresentation
18+
19+
20+
private val httpClient = HttpClient {
21+
install(ContentNegotiation) {
22+
json(Json {
23+
ignoreUnknownKeys = true
24+
})
25+
}
26+
}
27+
suspend fun getAccessToken(oidcConfig: OidcConfig, username: String, password: String): String {
28+
@Serializable
29+
data class TokenResponse(@SerialName("access_token") val accessToken: String)
30+
31+
return httpClient.submitForm(
32+
url = "${oidcConfig.oidcUrl}/token",
33+
formParameters = parameters {
34+
append("username", username)
35+
append("password", password)
36+
append("grant_type", "password")
37+
append("client_id", oidcConfig.clientId)
38+
append("client_secret", oidcConfig.clientSecret)
39+
}
40+
).body<TokenResponse>().accessToken
41+
}
42+
43+
data class UserLogin(val properties: UserRepresentation, val password: String, val accessToken: String)
44+
45+
suspend fun RealmResource.createAndLoginUser(
46+
oidcConfig: OidcConfig,
47+
firstName: String = "First",
48+
lastName: String = "Last",
49+
username: String = "${firstName.lowercase()}.${lastName.lowercase()}",
50+
password: String = "12345",
51+
email: String = "[email protected]"
52+
) = createAndLoginUser(oidcConfig) {
53+
this.username = username
54+
this.firstName = firstName
55+
this.lastName = lastName
56+
this.email = email
57+
CredentialRepresentation().apply {
58+
isTemporary = false
59+
type = CredentialRepresentation.PASSWORD
60+
value = password
61+
}
62+
}
63+
64+
suspend fun RealmResource.createAndLoginUser(
65+
oidcConfig: OidcConfig,
66+
userCustomizer: UserRepresentation.() -> CredentialRepresentation
67+
): UserLogin {
68+
val user = UserRepresentation()
69+
user.isEnabled = true
70+
val password = user.userCustomizer()
71+
val usersRessource: UsersResource = users()
72+
// Create user (requires manage-users role)
73+
val response = usersRessource.create(user)
74+
val userId = CreatedResponseUtil.getCreatedId(response)
75+
76+
// Define password credential
77+
val userResource = usersRessource[userId]
78+
// Set password credential
79+
userResource.resetPassword(password)
80+
81+
val accessToken = getAccessToken(oidcConfig, user.username, password.value)
82+
83+
return UserLogin(user, password.value, accessToken)
84+
}

src/test/kotlin/me/snoty/backend/test/TestObjects.kt

+27
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ val TestConfig = Config(
2121
)
2222
)
2323

24+
class TestConfigBuilder(block: TestConfigBuilder.() -> Unit) {
25+
var port: Short = 8080
26+
var environment: Environment = Environment.TEST
27+
var publicHost: String = "http://localhost:8080"
28+
var database: DatabaseConfig = DatabaseConfig(mockk<HikariDataSource>())
29+
var authentication: OidcConfig = OidcConfig(
30+
serverUrl = "http://localhost:8081",
31+
clientId = "",
32+
clientSecret = ""
33+
)
34+
35+
init {
36+
block()
37+
}
38+
39+
fun build() = Config(
40+
port = port,
41+
environment = environment,
42+
publicHost = publicHost,
43+
database = database,
44+
authentication = authentication
45+
)
46+
}
47+
48+
fun buildTestConfig(block: TestConfigBuilder.() -> Unit)
49+
= TestConfigBuilder(block).build()
50+
2451
val TestBuildInfo = BuildInfo(
2552
gitBranch = "<test>",
2653
gitCommit = "<test>",

src/test/kotlin/me/snoty/backend/integration/untis/UntisDateTest.kt src/testIntegration/kotlin/me/snoty/backend/integration/untis/UntisDateTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import me.snoty.backend.integration.untis.UntisDateTest.DateWrapper.Companion.ma
88
import me.snoty.backend.integration.untis.model.UntisDate
99
import me.snoty.backend.test.assertThrows
1010
import me.snoty.backend.utils.DateParseException
11+
import org.junit.jupiter.api.Assertions.assertEquals
12+
import org.junit.jupiter.api.Test
1113
import java.time.LocalDate
12-
import kotlin.test.Test
13-
import kotlin.test.assertEquals
1414

1515
class UntisDateTest {
1616
@Serializable

src/test/kotlin/me/snoty/backend/integration/untis/UntisDateTimeTest.kt src/testIntegration/kotlin/me/snoty/backend/integration/untis/UntisDateTimeTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import me.snoty.backend.integration.untis.UntisDateTimeTest.DateWrapper.Companio
88
import me.snoty.backend.integration.untis.model.UntisDateTime
99
import me.snoty.backend.test.assertThrows
1010
import me.snoty.backend.utils.DateTimeParseException
11+
import org.junit.jupiter.api.Assertions.assertEquals
12+
import org.junit.jupiter.api.Test
1113
import java.time.LocalDateTime
12-
import kotlin.test.Test
13-
import kotlin.test.assertEquals
1414

1515
class UntisDateTimeTest {
1616
@Serializable

0 commit comments

Comments
 (0)