style: fix missing assertion imports in ServerTest
Some checks failed
CI Pipeline / lint-and-test (pull_request) Failing after 43s

This commit is contained in:
Oliver Wallisch 2026-09-11 22:38:56 +02:00
parent 809d559562
commit 0d8fdec26a
10 changed files with 450 additions and 373 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
[*.{kt,kts}]
# Erlaubt Wildcard-Imports für das gesamte Projekt:
ij_kotlin_allow_trailing_comma = true
disabled_rules = no-wildcard-imports

View file

@ -1,12 +1,13 @@
import com.oliver.kidio.backend.plugins.configureRouting
import com.oliver.kidio.backend.data.database.DatabaseFactory import com.oliver.kidio.backend.data.database.DatabaseFactory
import com.oliver.kidio.backend.data.database.RedisFactory import com.oliver.kidio.backend.data.database.RedisFactory
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
import com.oliver.kidio.backend.plugins.configureRouting
import com.oliver.kidio.backend.plugins.configureSecurity import com.oliver.kidio.backend.plugins.configureSecurity
import com.oliver.kidio.backend.security.JwtService import com.oliver.kidio.backend.security.JwtService
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.* import io.ktor.server.application.Application
import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
fun Application.module() { fun Application.module() {
// Datenbank initialisieren // Datenbank initialisieren
@ -25,6 +26,6 @@ fun Application.module() {
configureRouting( configureRouting(
repository = repository, repository = repository,
jwtService = jwtService jwtService = jwtService,
) )
} }

View file

@ -22,6 +22,7 @@ object ScreentimeTable : Table("user_screentime") {
override val primaryKey = PrimaryKey(userId) override val primaryKey = PrimaryKey(userId)
} }
object UserTable : Table("users") { object UserTable : Table("users") {
val id = varchar("id", 36) val id = varchar("id", 36)
val email = varchar("email", 255).uniqueIndex() val email = varchar("email", 255).uniqueIndex()
@ -30,7 +31,6 @@ object UserTable : Table("users") {
val childAge = integer("child_age") val childAge = integer("child_age")
val parentPin = varchar("parent_pin", 4) val parentPin = varchar("parent_pin", 4)
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
@ -46,11 +46,12 @@ object DatabaseFactory {
val jdbcURL = "jdbc:postgresql://$host:$port/$dbName" val jdbcURL = "jdbc:postgresql://$host:$port/$dbName"
val database = Database.connect( val database =
Database.connect(
url = jdbcURL, url = jdbcURL,
driver = driverClassName, driver = driverClassName,
user = dbUsername, user = dbUsername,
password = password password = password,
) )
transaction(database) { transaction(database) {
@ -93,6 +94,5 @@ object DatabaseFactory {
} }
} }
suspend fun <T> dbQuery(block: suspend () -> T): T = suspend fun <T> dbQuery(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() }
newSuspendedTransaction(Dispatchers.IO) { block() }
} }

View file

@ -1,5 +1,6 @@
package com.oliver.kidio.backend.data.database package com.oliver.kidio.backend.data.database
import io.ktor.server.config.* import io.ktor.server.config.ApplicationConfig
import io.ktor.server.routing.get
import redis.clients.jedis.JedisPool import redis.clients.jedis.JedisPool
import redis.clients.jedis.JedisPoolConfig import redis.clients.jedis.JedisPoolConfig
@ -7,15 +8,20 @@ object RedisFactory {
private var jedisPool: JedisPool? = null private var jedisPool: JedisPool? = null
fun init(config: ApplicationConfig) { fun init(config: ApplicationConfig) {
val host = System.getenv("REDIS_HOST") val host =
System.getenv("REDIS_HOST")
?: config.propertyOrNull("storage.redisHost")?.getString() ?: config.propertyOrNull("storage.redisHost")?.getString()
?: "localhost" ?: "localhost"
val port = (System.getenv("REDIS_PORT") val port =
(
System.getenv("REDIS_PORT")
?: config.propertyOrNull("storage.redisPort")?.getString() ?: config.propertyOrNull("storage.redisPort")?.getString()
?: "6379").toInt() ?: "6379"
).toInt()
val poolConfig = JedisPoolConfig().apply { val poolConfig =
JedisPoolConfig().apply {
maxTotal = 32 maxTotal = 32
maxIdle = 8 maxIdle = 8
minIdle = 2 minIdle = 2
@ -23,6 +29,7 @@ object RedisFactory {
jedisPool = JedisPool(poolConfig, host, port) jedisPool = JedisPool(poolConfig, host, port)
} }
fun getResource() = jedisPool?.resource fun getResource() =
jedisPool?.resource
?: throw IllegalStateException("Redis Verbindungspool ist nicht initzialisiert") ?: throw IllegalStateException("Redis Verbindungspool ist nicht initzialisiert")
} }

View file

@ -8,14 +8,14 @@ data class Task(
val id: String, val id: String,
val question: String, val question: String,
val options: List<String>, val options: List<String>,
val rewardMinutes: Int // Wie viel Belohnung es gibt val rewardMinutes: Int,
) )
// Anfrage des Kindes, wenn es eine Aufgabe löst // Anfrage des Kindes, wenn es eine Aufgabe löst
@Serializable @Serializable
data class TaskVerifyRequest( data class TaskVerifyRequest(
val taskId: String, val taskId: String,
val selectedAnswer: String val selectedAnswer: String,
) )
// Antwort vom Server nach der Aufgaben-Prüfung // Antwort vom Server nach der Aufgaben-Prüfung
@ -23,20 +23,20 @@ data class TaskVerifyRequest(
data class TaskVerifyResponse( data class TaskVerifyResponse(
val isCorrect: Boolean, val isCorrect: Boolean,
val earnedMinutes: Int, val earnedMinutes: Int,
val message: String val message: String,
) )
// Datenabgleich für die Bildschirmzeit (Sync) // Datenabgleich für die Bildschirmzeit (Sync)
@Serializable @Serializable
data class SyncRequest( data class SyncRequest(
val userId: String, val userId: String,
val usedSecondsSinceLastSync: Int val usedSecondsSinceLastSync: Int,
) )
@Serializable @Serializable
data class SyncResponse( data class SyncResponse(
val remainingSeconds: Int, val remainingSeconds: Int,
val isBlocked: Boolean val isBlocked: Boolean,
) )
// --- NEU: AUTHENTIFIZIERUNG --- // --- NEU: AUTHENTIFIZIERUNG ---
@ -47,21 +47,21 @@ data class RegisterRequest(
val passwordPlain: String, val passwordPlain: String,
val familyName: String, val familyName: String,
val childAge: Int, val childAge: Int,
val parentPin: String val parentPin: String,
) )
@Serializable @Serializable
data class LoginRequest( data class LoginRequest(
val email: String, val email: String,
val passwordPlain: String val passwordPlain: String,
) )
@Serializable @Serializable
data class AuthResponse( data class AuthResponse(
val token: String, // Der digitale "Ausweis" (JWT) val token: String,
val userId: String, // Wichtig für die App, um Aufgaben zuzuordnen val userId: String,
val familyName: String, // Für "Hallo Familie Müller!" val familyName: String,
val childAge: Int val childAge: Int,
) )
// Interne Backend-Klasse (Domain Model). // Interne Backend-Klasse (Domain Model).
@ -71,5 +71,5 @@ data class User(
val passwordHash: String, val passwordHash: String,
val familyName: String, val familyName: String,
val childAge: Int, val childAge: Int,
val parentPin: String val parentPin: String,
) )

View file

@ -12,27 +12,41 @@ import org.mindrot.jbcrypt.BCrypt
interface KidioRepository { interface KidioRepository {
suspend fun getAllTasks(): List<Task> suspend fun getAllTasks(): List<Task>
suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>?
suspend fun getScreentime(userId: String): Int suspend fun getScreentime(userId: String): Int
suspend fun updateScreentime(userId: String, seconds: Int)
suspend fun updateScreentime(
userId: String,
seconds: Int,
)
suspend fun findByEmail(email: String): User? suspend fun findByEmail(email: String): User?
suspend fun verifyPassword(password: String, passwordHash: String): Boolean
suspend fun verifyPassword(
password: String,
passwordHash: String,
): Boolean
suspend fun registerUser(user: User): Boolean suspend fun registerUser(user: User): Boolean
} }
class ExposedKidioRepository : KidioRepository { class ExposedKidioRepository : KidioRepository {
override suspend fun getAllTasks(): List<Task> = DatabaseFactory.dbQuery { override suspend fun getAllTasks(): List<Task> =
DatabaseFactory.dbQuery {
TasksTable.selectAll().map { TasksTable.selectAll().map {
Task( Task(
id = it[TasksTable.id], id = it[TasksTable.id],
question = it[TasksTable.question], question = it[TasksTable.question],
options = it[TasksTable.options].split(",").map { opt -> opt.trim() }, options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
rewardMinutes = it[TasksTable.rewardMinutes] rewardMinutes = it[TasksTable.rewardMinutes],
) )
} }
} }
override suspend fun registerUser(user: User): Boolean = DatabaseFactory.dbQuery { override suspend fun registerUser(user: User): Boolean =
DatabaseFactory.dbQuery {
try { try {
UserTable.insert { UserTable.insert {
it[id] = user.id it[id] = user.id
@ -53,7 +67,8 @@ class ExposedKidioRepository : KidioRepository {
} }
} }
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = DatabaseFactory.dbQuery { override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? =
DatabaseFactory.dbQuery {
TasksTable.selectAll() TasksTable.selectAll()
.where { TasksTable.id eq taskId } .where { TasksTable.id eq taskId }
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] } .map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
@ -75,7 +90,8 @@ class ExposedKidioRepository : KidioRepository {
println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}") println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}")
} }
val dbSeconds = DatabaseFactory.dbQuery { val dbSeconds =
DatabaseFactory.dbQuery {
ScreentimeTable.selectAll() ScreentimeTable.selectAll()
.where { ScreentimeTable.userId eq userId } .where { ScreentimeTable.userId eq userId }
.map { it[ScreentimeTable.remainingSeconds] } .map { it[ScreentimeTable.remainingSeconds] }
@ -93,9 +109,13 @@ class ExposedKidioRepository : KidioRepository {
return dbSeconds return dbSeconds
} }
override suspend fun updateScreentime(userId: String, seconds: Int) { override suspend fun updateScreentime(
userId: String,
seconds: Int,
) {
DatabaseFactory.dbQuery { DatabaseFactory.dbQuery {
val rowsUpdated = ScreentimeTable.update({ ScreentimeTable.userId eq userId }) { val rowsUpdated =
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
it[remainingSeconds] = seconds it[remainingSeconds] = seconds
} }
if (rowsUpdated == 0) { if (rowsUpdated == 0) {
@ -116,7 +136,8 @@ class ExposedKidioRepository : KidioRepository {
} }
} }
override suspend fun findByEmail(email: String): User? = DatabaseFactory.dbQuery { override suspend fun findByEmail(email: String): User? =
DatabaseFactory.dbQuery {
UserTable.selectAll() UserTable.selectAll()
.where { UserTable.email eq email } .where { UserTable.email eq email }
.map { .map {
@ -126,28 +147,34 @@ class ExposedKidioRepository : KidioRepository {
passwordHash = it[UserTable.passwordHash], passwordHash = it[UserTable.passwordHash],
familyName = it[UserTable.familyName], familyName = it[UserTable.familyName],
childAge = it[UserTable.childAge], childAge = it[UserTable.childAge],
parentPin = it[UserTable.parentPin] parentPin = it[UserTable.parentPin],
) )
} }
.singleOrNull() .singleOrNull()
} }
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { override suspend fun verifyPassword(
password: String,
passwordHash: String,
): Boolean {
return BCrypt.checkpw(password, passwordHash) return BCrypt.checkpw(password, passwordHash)
} }
} }
class InMemoryKidioRepository : KidioRepository { class InMemoryKidioRepository : KidioRepository {
private val tasks = mutableListOf( private val tasks =
mutableListOf(
Task("t1", "Was ist 12 + 15?", listOf("25", "27", "30", "22"), 10), Task("t1", "Was ist 12 + 15?", listOf("25", "27", "30", "22"), 10),
Task("t2", "Was ist 8 x 7?", listOf("54", "56", "64", "48"), 15) Task("t2", "Was ist 8 x 7?", listOf("54", "56", "64", "48"), 15),
) )
private val answers = mutableMapOf( private val answers =
mutableMapOf(
"t1" to ("27" to 10), "t1" to ("27" to 10),
"t2" to ("56" to 15) "t2" to ("56" to 15),
) )
private val screentimes = mutableMapOf( private val screentimes =
"default_user" to 1800 mutableMapOf(
"default_user" to 1800,
) )
private val users = mutableListOf<User>() private val users = mutableListOf<User>()
@ -157,7 +184,10 @@ class InMemoryKidioRepository : KidioRepository {
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800 override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
override suspend fun updateScreentime(userId: String, seconds: Int) { override suspend fun updateScreentime(
userId: String,
seconds: Int,
) {
screentimes[userId] = seconds screentimes[userId] = seconds
} }
@ -165,7 +195,10 @@ class InMemoryKidioRepository : KidioRepository {
return users.find { it.email == email } return users.find { it.email == email }
} }
override suspend fun verifyPassword(password: String, passwordHash: String): Boolean { override suspend fun verifyPassword(
password: String,
passwordHash: String,
): Boolean {
return BCrypt.checkpw(password, passwordHash) return BCrypt.checkpw(password, passwordHash)
} }

View file

@ -13,7 +13,10 @@ import io.ktor.server.routing.*
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import java.util.UUID import java.util.UUID
fun Application.configureRouting(repository: KidioRepository, jwtService : JwtService) { fun Application.configureRouting(
repository: KidioRepository,
jwtService: JwtService,
) {
routing { routing {
post("/api/v1/auth/register") { post("/api/v1/auth/register") {
val registerRequest = call.receive<RegisterRequest>() val registerRequest = call.receive<RegisterRequest>()
@ -36,7 +39,16 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt()) val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt())
// datenbank insert hinzufügen // datenbank insert hinzufügen
repository.registerUser(User(userId, registerRequest.email, passwordHash, registerRequest.familyName, registerRequest.childAge, registerRequest.parentPin)) repository.registerUser(
User(
userId,
registerRequest.email,
passwordHash,
registerRequest.familyName,
registerRequest.childAge,
registerRequest.parentPin,
),
)
val token = jwtService.generateToken(userId) val token = jwtService.generateToken(userId)
@ -45,8 +57,8 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
mapOf( mapOf(
"token" to token, "token" to token,
"userId" to userId, "userId" to userId,
"message" to "Registrierung erfolgreich" "message" to "Registrierung erfolgreich",
) ),
) )
} }
// Health-Check // Health-Check
@ -66,21 +78,19 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
return@post return@post
} }
val token = jwtService.generateToken(getUser.id) val token = jwtService.generateToken(getUser.id)
call.respond(HttpStatusCode.OK, AuthResponse( call.respond(
HttpStatusCode.OK,
AuthResponse(
token, token,
userId = getUser.id, userId = getUser.id,
familyName = getUser.familyName, familyName = getUser.familyName,
childAge = getUser.childAge childAge = getUser.childAge,
)) ),
)
return@post return@post
} }
authenticate("auth-jwt") { authenticate("auth-jwt") {
// 1. Alle verfügbaren Aufgaben abrufen // 1. Alle verfügbaren Aufgaben abrufen
get("/api/v1/tasks") { get("/api/v1/tasks") {
val tasks = repository.getAllTasks() val tasks = repository.getAllTasks()
@ -102,8 +112,8 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
TaskVerifyResponse( TaskVerifyResponse(
isCorrect = false, isCorrect = false,
earnedMinutes = 0, earnedMinutes = 0,
message = "Aufgabe nicht gefunden!" message = "Aufgabe nicht gefunden!",
) ),
) )
return@post return@post
} }
@ -120,22 +130,24 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
TaskVerifyResponse( TaskVerifyResponse(
isCorrect = true, isCorrect = true,
earnedMinutes = reward, earnedMinutes = reward,
message = "Super gemacht! Du hast $reward Minuten gewonnen." message = "Super gemacht! Du hast $reward Minuten gewonnen.",
) ),
) )
} else { } else {
call.respond( call.respond(
TaskVerifyResponse( TaskVerifyResponse(
isCorrect = false, isCorrect = false,
earnedMinutes = 0, earnedMinutes = 0,
message = "Schade, das war leider falsch. Versuche es nochmal!" message = "Schade, das war leider falsch. Versuche es nochmal!",
) ),
) )
} }
} }
get("/api/v1/screentime") { get("/api/v1/screentime") {
val principal = call.principal<UserPrincipal>() val principal = call.principal<UserPrincipal>()
val userid = principal?.userId?: run { val userid =
principal?.userId
?: run {
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token")) call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
return@get return@get
} }
@ -144,11 +156,9 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
HttpStatusCode.OK, HttpStatusCode.OK,
SyncResponse( SyncResponse(
remainingSeconds = remainingSeconds, remainingSeconds = remainingSeconds,
isBlocked = remainingSeconds <= 0 isBlocked = remainingSeconds <= 0,
),
) )
)
} }
// 3. Bildschirmzeit synchronisieren // 3. Bildschirmzeit synchronisieren
@ -168,11 +178,10 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
call.respond( call.respond(
SyncResponse( SyncResponse(
remainingSeconds = newSeconds, remainingSeconds = newSeconds,
isBlocked = newSeconds <= 0 isBlocked = newSeconds <= 0,
) ),
) )
} }
} }
} }
} }

View file

@ -9,10 +9,10 @@ class JwtService(
private val secret: String = System.getenv("JWT_SECRET") ?: "super-geheimes-secret-fuer-dev", private val secret: String = System.getenv("JWT_SECRET") ?: "super-geheimes-secret-fuer-dev",
private val issuer: String = "https://kidio.oliver.com/", private val issuer: String = "https://kidio.oliver.com/",
private val audience: String = "kidio-users", private val audience: String = "kidio-users",
private val expirationInMs: Long = java.util.concurrent.TimeUnit.DAYS.toMillis(30) // 30 Tage private val expirationInMs: Long = java.util.concurrent.TimeUnit.DAYS.toMillis(30),
) { ) {
val verifier: JWTVerifier =
val verifier: JWTVerifier = JWT JWT
.require(Algorithm.HMAC256(secret)) .require(Algorithm.HMAC256(secret))
.withAudience(audience) .withAudience(audience)
.withIssuer(issuer) .withIssuer(issuer)

View file

@ -4,20 +4,29 @@ import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
import com.oliver.kidio.backend.plugins.configureRouting import com.oliver.kidio.backend.plugins.configureRouting
import com.oliver.kidio.backend.plugins.configureSecurity import com.oliver.kidio.backend.plugins.configureSecurity
import com.oliver.kidio.backend.security.JwtService import com.oliver.kidio.backend.security.JwtService
import io.ktor.client.request.* import io.ktor.client.request.get
import io.ktor.client.statement.* import io.ktor.client.request.header
import io.ktor.http.* import io.ktor.client.request.post
import io.ktor.serialization.kotlinx.json.* import io.ktor.client.request.setBody
import io.ktor.server.application.* import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.testing.testApplication
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
import io.ktor.server.testing.*
import kotlinx.serialization.json.*
import kotlin.test.*
class ServerTest { class ServerTest {
@Test @Test
fun `test health endpoint`() = testApplication { fun `test health endpoint`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -35,7 +44,8 @@ class ServerTest {
} }
@Test @Test
fun `test get screentime returns current status`() = testApplication { fun `test get screentime returns current status`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -52,7 +62,8 @@ class ServerTest {
assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status) assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status)
// Mit Token -> 200 OK // Mit Token -> 200 OK
val response = client.get("/api/v1/screentime") { val response =
client.get("/api/v1/screentime") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
} }
assertEquals(HttpStatusCode.OK, response.status) assertEquals(HttpStatusCode.OK, response.status)
@ -62,7 +73,8 @@ class ServerTest {
} }
@Test @Test
fun `test protected endpoints reject unauthorized requests`() = testApplication { fun `test protected endpoints reject unauthorized requests`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -80,7 +92,8 @@ class ServerTest {
} }
@Test @Test
fun `test registration and login flow`() = testApplication { fun `test registration and login flow`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -93,7 +106,8 @@ class ServerTest {
} }
// 1. Registrierung // 1. Registrierung
val registerResponse = client.post("/api/v1/auth/register") { val registerResponse =
client.post("/api/v1/auth/register") {
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody( setBody(
"""{ """{
@ -102,20 +116,21 @@ class ServerTest {
"familyName": "Familie Schmidt", "familyName": "Familie Schmidt",
"childAge": 8, "childAge": 8,
"parentPin": "4321" "parentPin": "4321"
}""" }""",
) )
} }
assertEquals(HttpStatusCode.Created, registerResponse.status) assertEquals(HttpStatusCode.Created, registerResponse.status)
assertTrue(registerResponse.bodyAsText().contains("token")) assertTrue(registerResponse.bodyAsText().contains("token"))
// 2. Login mit denselben Daten // 2. Login mit denselben Daten
val loginResponse = client.post("/api/v1/auth/login") { val loginResponse =
client.post("/api/v1/auth/login") {
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody( setBody(
"""{ """{
"email": "test@family.de", "email": "test@family.de",
"passwordPlain": "strengGeheim1" "passwordPlain": "strengGeheim1"
}""" }""",
) )
} }
assertEquals(HttpStatusCode.OK, loginResponse.status) assertEquals(HttpStatusCode.OK, loginResponse.status)
@ -125,7 +140,8 @@ class ServerTest {
} }
@Test @Test
fun `test tasks endpoint and verify with jwt`() = testApplication { fun `test tasks endpoint and verify with jwt`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -140,7 +156,8 @@ class ServerTest {
val token = jwtService.generateToken("default_user") val token = jwtService.generateToken("default_user")
// 1. Get Tasks mit Token // 1. Get Tasks mit Token
val tasksResponse = client.get("/api/v1/tasks") { val tasksResponse =
client.get("/api/v1/tasks") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
} }
assertEquals(HttpStatusCode.OK, tasksResponse.status) assertEquals(HttpStatusCode.OK, tasksResponse.status)
@ -150,7 +167,8 @@ class ServerTest {
assertEquals("Was ist 12 + 15?", tasksJson[0].question) assertEquals("Was ist 12 + 15?", tasksJson[0].question)
// 2. Verify Task correct answer // 2. Verify Task correct answer
val verifyResponse = client.post("/api/v1/tasks/verify") { val verifyResponse =
client.post("/api/v1/tasks/verify") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("""{"taskId":"t1","selectedAnswer":"27"}""") setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
@ -161,7 +179,8 @@ class ServerTest {
assertEquals(10, verifyJson.earnedMinutes) assertEquals(10, verifyJson.earnedMinutes)
// 3. Verify Task incorrect answer // 3. Verify Task incorrect answer
val verifyWrongResponse = client.post("/api/v1/tasks/verify") { val verifyWrongResponse =
client.post("/api/v1/tasks/verify") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("""{"taskId":"t1","selectedAnswer":"99"}""") setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
@ -173,7 +192,8 @@ class ServerTest {
} }
@Test @Test
fun `test screentime sync deducts time repeatedly`() = testApplication { fun `test screentime sync deducts time repeatedly`() =
testApplication {
val jwtService = JwtService() val jwtService = JwtService()
val repository = InMemoryKidioRepository() val repository = InMemoryKidioRepository()
@ -188,7 +208,8 @@ class ServerTest {
val token = jwtService.generateToken("default_user") val token = jwtService.generateToken("default_user")
// 1. Sync: 200 Sekunden abziehen (1800 -> 1600) // 1. Sync: 200 Sekunden abziehen (1800 -> 1600)
val syncResponse1 = client.post("/api/v1/screentime/sync") { val syncResponse1 =
client.post("/api/v1/screentime/sync") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""") setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
@ -199,7 +220,8 @@ class ServerTest {
assertFalse(syncJson1.isBlocked) assertFalse(syncJson1.isBlocked)
// 2. Erneuter Sync: Weitere 300 Sekunden abziehen (1600 -> 1300) - darf nicht auf 1800 zurückspringen! // 2. Erneuter Sync: Weitere 300 Sekunden abziehen (1600 -> 1300) - darf nicht auf 1800 zurückspringen!
val syncResponse2 = client.post("/api/v1/screentime/sync") { val syncResponse2 =
client.post("/api/v1/screentime/sync") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""") setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
@ -210,7 +232,8 @@ class ServerTest {
assertFalse(syncJson2.isBlocked) assertFalse(syncJson2.isBlocked)
// 3. Sync bis zur Sperre (weitere 1500 Sekunden abziehen -> 0) // 3. Sync bis zur Sperre (weitere 1500 Sekunden abziehen -> 0)
val syncResponse3 = client.post("/api/v1/screentime/sync") { val syncResponse3 =
client.post("/api/v1/screentime/sync") {
header(HttpHeaders.Authorization, "Bearer $token") header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""") setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")