feat: new endpoint for seeing remainnig screentime, test integration #8
4
.editorconfig
Normal file
4
.editorconfig
Normal 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
|
||||||
|
|
@ -45,4 +45,4 @@ dependencies {
|
||||||
|
|
||||||
// redis
|
// redis
|
||||||
implementation("redis.clients:jedis:5.1.2")
|
implementation("redis.clients:jedis:5.1.2")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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,12 +46,13 @@ object DatabaseFactory {
|
||||||
|
|
||||||
val jdbcURL = "jdbc:postgresql://$host:$port/$dbName"
|
val jdbcURL = "jdbc:postgresql://$host:$port/$dbName"
|
||||||
|
|
||||||
val database = Database.connect(
|
val database =
|
||||||
url = jdbcURL,
|
Database.connect(
|
||||||
driver = driverClassName,
|
url = jdbcURL,
|
||||||
user = dbUsername,
|
driver = driverClassName,
|
||||||
password = password
|
user = dbUsername,
|
||||||
)
|
password = password,
|
||||||
|
)
|
||||||
|
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
SchemaUtils.create(TasksTable, ScreentimeTable, UserTable)
|
SchemaUtils.create(TasksTable, ScreentimeTable, UserTable)
|
||||||
|
|
@ -79,7 +80,7 @@ object DatabaseFactory {
|
||||||
it[remainingSeconds] = 1800
|
it[remainingSeconds] = 1800
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UserTable.selectAll().empty()) {
|
if (UserTable.selectAll().empty()) {
|
||||||
UserTable.insert {
|
UserTable.insert {
|
||||||
it[id] = "default_user"
|
it[id] = "default_user"
|
||||||
|
|
@ -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() }
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,22 +8,28 @@ 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 =
|
||||||
?: config.propertyOrNull("storage.redisHost")?.getString()
|
System.getenv("REDIS_HOST")
|
||||||
?: "localhost"
|
?: config.propertyOrNull("storage.redisHost")?.getString()
|
||||||
|
?: "localhost"
|
||||||
|
|
||||||
val port = (System.getenv("REDIS_PORT")
|
val port =
|
||||||
?: config.propertyOrNull("storage.redisPort")?.getString()
|
(
|
||||||
?: "6379").toInt()
|
System.getenv("REDIS_PORT")
|
||||||
|
?: config.propertyOrNull("storage.redisPort")?.getString()
|
||||||
|
?: "6379"
|
||||||
|
).toInt()
|
||||||
|
|
||||||
val poolConfig = JedisPoolConfig().apply {
|
val poolConfig =
|
||||||
maxTotal = 32
|
JedisPoolConfig().apply {
|
||||||
maxIdle = 8
|
maxTotal = 32
|
||||||
minIdle = 2
|
maxIdle = 8
|
||||||
}
|
minIdle = 2
|
||||||
|
}
|
||||||
jedisPool = JedisPool(poolConfig, host, port)
|
jedisPool = JedisPool(poolConfig, host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getResource() = jedisPool?.resource
|
fun getResource() =
|
||||||
?: throw IllegalStateException("Redis Verbindungspool ist nicht initzialisiert")
|
jedisPool?.resource
|
||||||
|
?: throw IllegalStateException("Redis Verbindungspool ist nicht initzialisiert")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -12,53 +12,68 @@ 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> =
|
||||||
TasksTable.selectAll().map {
|
DatabaseFactory.dbQuery {
|
||||||
Task(
|
TasksTable.selectAll().map {
|
||||||
id = it[TasksTable.id],
|
Task(
|
||||||
question = it[TasksTable.question],
|
id = it[TasksTable.id],
|
||||||
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
question = it[TasksTable.question],
|
||||||
rewardMinutes = it[TasksTable.rewardMinutes]
|
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
||||||
)
|
rewardMinutes = it[TasksTable.rewardMinutes],
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun registerUser(user: User): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
try {
|
|
||||||
UserTable.insert {
|
|
||||||
it[id] = user.id
|
|
||||||
it[email] = user.email
|
|
||||||
it[passwordHash] = user.passwordHash
|
|
||||||
it[familyName] = user.familyName
|
|
||||||
it[childAge] = user.childAge
|
|
||||||
it[parentPin] = user.parentPin
|
|
||||||
}
|
}
|
||||||
ScreentimeTable.insert {
|
|
||||||
it[userId] = user.id
|
|
||||||
it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("FEHLER bei Registrierung: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? = DatabaseFactory.dbQuery {
|
override suspend fun registerUser(user: User): Boolean =
|
||||||
TasksTable.selectAll()
|
DatabaseFactory.dbQuery {
|
||||||
.where { TasksTable.id eq taskId }
|
try {
|
||||||
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
UserTable.insert {
|
||||||
.singleOrNull()
|
it[id] = user.id
|
||||||
}
|
it[email] = user.email
|
||||||
|
it[passwordHash] = user.passwordHash
|
||||||
|
it[familyName] = user.familyName
|
||||||
|
it[childAge] = user.childAge
|
||||||
|
it[parentPin] = user.parentPin
|
||||||
|
}
|
||||||
|
ScreentimeTable.insert {
|
||||||
|
it[userId] = user.id
|
||||||
|
it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("FEHLER bei Registrierung: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
TasksTable.selectAll()
|
||||||
|
.where { TasksTable.id eq taskId }
|
||||||
|
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getScreentime(userId: String): Int {
|
override suspend fun getScreentime(userId: String): Int {
|
||||||
val redisKey = "screentime:$userId"
|
val redisKey = "screentime:$userId"
|
||||||
|
|
@ -75,12 +90,13 @@ 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 =
|
||||||
ScreentimeTable.selectAll()
|
DatabaseFactory.dbQuery {
|
||||||
.where { ScreentimeTable.userId eq userId }
|
ScreentimeTable.selectAll()
|
||||||
.map { it[ScreentimeTable.remainingSeconds] }
|
.where { ScreentimeTable.userId eq userId }
|
||||||
.singleOrNull() ?: 1800
|
.map { it[ScreentimeTable.remainingSeconds] }
|
||||||
}
|
.singleOrNull() ?: 1800
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RedisFactory.getResource().use { jedis ->
|
RedisFactory.getResource().use { jedis ->
|
||||||
|
|
@ -93,11 +109,15 @@ 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 =
|
||||||
it[remainingSeconds] = seconds
|
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
||||||
}
|
it[remainingSeconds] = seconds
|
||||||
|
}
|
||||||
if (rowsUpdated == 0) {
|
if (rowsUpdated == 0) {
|
||||||
ScreentimeTable.insert {
|
ScreentimeTable.insert {
|
||||||
it[ScreentimeTable.userId] = userId
|
it[ScreentimeTable.userId] = userId
|
||||||
|
|
@ -116,39 +136,46 @@ class ExposedKidioRepository : KidioRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByEmail(email: String): User? = DatabaseFactory.dbQuery {
|
override suspend fun findByEmail(email: String): User? =
|
||||||
UserTable.selectAll()
|
DatabaseFactory.dbQuery {
|
||||||
.where { UserTable.email eq email }
|
UserTable.selectAll()
|
||||||
.map {
|
.where { UserTable.email eq email }
|
||||||
User(
|
.map {
|
||||||
id = it[UserTable.id],
|
User(
|
||||||
email = it[UserTable.email],
|
id = it[UserTable.id],
|
||||||
passwordHash = it[UserTable.passwordHash],
|
email = it[UserTable.email],
|
||||||
familyName = it[UserTable.familyName],
|
passwordHash = it[UserTable.passwordHash],
|
||||||
childAge = it[UserTable.childAge],
|
familyName = it[UserTable.familyName],
|
||||||
parentPin = it[UserTable.parentPin]
|
childAge = it[UserTable.childAge],
|
||||||
)
|
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 =
|
||||||
Task("t1", "Was ist 12 + 15?", listOf("25", "27", "30", "22"), 10),
|
mutableListOf(
|
||||||
Task("t2", "Was ist 8 x 7?", listOf("54", "56", "64", "48"), 15)
|
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),
|
||||||
private val answers = mutableMapOf(
|
)
|
||||||
"t1" to ("27" to 10),
|
private val answers =
|
||||||
"t2" to ("56" to 15)
|
mutableMapOf(
|
||||||
)
|
"t1" to ("27" to 10),
|
||||||
private val screentimes = mutableMapOf(
|
"t2" to ("56" to 15),
|
||||||
"default_user" to 1800
|
)
|
||||||
)
|
private val screentimes =
|
||||||
|
mutableMapOf(
|
||||||
|
"default_user" to 1800,
|
||||||
|
)
|
||||||
private val users = mutableListOf<User>()
|
private val users = mutableListOf<User>()
|
||||||
|
|
||||||
override suspend fun getAllTasks(): List<Task> = tasks
|
override suspend fun getAllTasks(): List<Task> = tasks
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,4 +207,4 @@ class InMemoryKidioRepository : KidioRepository {
|
||||||
screentimes[user.id] = 1800
|
screentimes[user.id] = 1800
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>()
|
||||||
|
|
@ -24,7 +27,7 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
||||||
if (registerRequest.parentPin.length != 4) {
|
if (registerRequest.parentPin.length != 4) {
|
||||||
call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein")
|
call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein")
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
val existingUser = repository.findByEmail(registerRequest.email)
|
val existingUser = repository.findByEmail(registerRequest.email)
|
||||||
if (existingUser != null) {
|
if (existingUser != null) {
|
||||||
call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits")
|
call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits")
|
||||||
|
|
@ -35,8 +38,17 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
||||||
val userId = UUID.randomUUID().toString()
|
val userId = UUID.randomUUID().toString()
|
||||||
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
|
||||||
|
|
@ -54,33 +66,31 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
||||||
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
post ("/api/v1/auth/login" ) {
|
post("/api/v1/auth/login") {
|
||||||
val loginRequest = call.receive<LoginRequest>()
|
val loginRequest = call.receive<LoginRequest>()
|
||||||
if(loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()){
|
if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
|
||||||
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
val getUser = repository.findByEmail(loginRequest.email)
|
val getUser = repository.findByEmail(loginRequest.email)
|
||||||
if (getUser == null ||!repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)){
|
if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) {
|
||||||
call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten")
|
call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten")
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
val token = jwtService.generateToken(getUser.id)
|
val token = jwtService.generateToken(getUser.id)
|
||||||
call.respond(HttpStatusCode.OK, AuthResponse(
|
call.respond(
|
||||||
token,
|
HttpStatusCode.OK,
|
||||||
userId = getUser.id,
|
AuthResponse(
|
||||||
familyName = getUser.familyName,
|
token,
|
||||||
childAge = getUser.childAge
|
userId = getUser.id,
|
||||||
))
|
familyName = getUser.familyName,
|
||||||
|
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()
|
||||||
|
|
@ -88,67 +98,67 @@ fun Application.configureRouting(repository: KidioRepository, jwtService : JwtSe
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Antwort auf eine Aufgabe überprüfen
|
// 2. Antwort auf eine Aufgabe überprüfen
|
||||||
post("/api/v1/tasks/verify") {
|
post("/api/v1/tasks/verify") {
|
||||||
val request = call.receive<TaskVerifyRequest>()
|
val request = call.receive<TaskVerifyRequest>()
|
||||||
val principal = call.principal<UserPrincipal>()
|
val principal = call.principal<UserPrincipal>()
|
||||||
val userId = principal?.userId
|
val userId = principal?.userId
|
||||||
if (userId == null){
|
if (userId == null) {
|
||||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token"))
|
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiges oder abgelaufenes Token"))
|
||||||
return@post
|
return@post
|
||||||
}
|
|
||||||
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
|
||||||
if (taskInfo == null) {
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = false,
|
|
||||||
earnedMinutes = 0,
|
|
||||||
message = "Aufgabe nicht gefunden!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val (correctAnswer, reward) = taskInfo
|
|
||||||
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
|
||||||
|
|
||||||
if (isCorrect) {
|
|
||||||
val currentScreentime = repository.getScreentime(userId)
|
|
||||||
val newSeconds = currentScreentime + (reward * 60)
|
|
||||||
repository.updateScreentime(userId, newSeconds)
|
|
||||||
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = true,
|
|
||||||
earnedMinutes = reward,
|
|
||||||
message = "Super gemacht! Du hast $reward Minuten gewonnen."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = false,
|
|
||||||
earnedMinutes = 0,
|
|
||||||
message = "Schade, das war leider falsch. Versuche es nochmal!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
||||||
|
if (taskInfo == null) {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
message = "Aufgabe nicht gefunden!",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val (correctAnswer, reward) = taskInfo
|
||||||
|
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
val currentScreentime = repository.getScreentime(userId)
|
||||||
|
val newSeconds = currentScreentime + (reward * 60)
|
||||||
|
repository.updateScreentime(userId, newSeconds)
|
||||||
|
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = true,
|
||||||
|
earnedMinutes = reward,
|
||||||
|
message = "Super gemacht! Du hast $reward Minuten gewonnen.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
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 =
|
||||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
|
principal?.userId
|
||||||
return@get
|
?: run {
|
||||||
}
|
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
|
||||||
|
return@get
|
||||||
|
}
|
||||||
val remainingSeconds = repository.getScreentime(userid)
|
val remainingSeconds = repository.getScreentime(userid)
|
||||||
call.respond(
|
call.respond(
|
||||||
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,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ 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)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generiert ein JWT-Token für einen bestimmten Benutzer.
|
* Generiert ein JWT-Token für einen bestimmten Benutzer.
|
||||||
|
|
|
||||||
|
|
@ -4,220 +4,243 @@ 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`() =
|
||||||
val jwtService = JwtService()
|
testApplication {
|
||||||
val repository = InMemoryKidioRepository()
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
}
|
}
|
||||||
configureSecurity(jwtService)
|
|
||||||
configureRouting(repository, jwtService)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = client.get("/api/v1/health")
|
val response = client.get("/api/v1/health")
|
||||||
assertEquals(HttpStatusCode.OK, response.status)
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
assertTrue(response.bodyAsText().contains("Kidio Backend läuft!"))
|
assertTrue(response.bodyAsText().contains("Kidio Backend läuft!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test get screentime returns current status`() = testApplication {
|
fun `test get screentime returns current status`() =
|
||||||
val jwtService = JwtService()
|
testApplication {
|
||||||
val repository = InMemoryKidioRepository()
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) { json() }
|
install(ServerContentNegotiation) { json() }
|
||||||
configureSecurity(jwtService)
|
configureSecurity(jwtService)
|
||||||
configureRouting(repository, jwtService)
|
configureRouting(repository, jwtService)
|
||||||
}
|
|
||||||
|
|
||||||
val token = jwtService.generateToken("default_user")
|
|
||||||
|
|
||||||
// Ohne Token -> 401 Unauthorized
|
|
||||||
val unauthResponse = client.get("/api/v1/screentime")
|
|
||||||
assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status)
|
|
||||||
|
|
||||||
// Mit Token -> 200 OK
|
|
||||||
val response = client.get("/api/v1/screentime") {
|
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
assertEquals(HttpStatusCode.OK, response.status)
|
|
||||||
val json = Json.decodeFromString<SyncResponse>(response.bodyAsText())
|
|
||||||
assertEquals(1800, json.remainingSeconds)
|
|
||||||
assertFalse(json.isBlocked)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test protected endpoints reject unauthorized requests`() = testApplication {
|
|
||||||
val jwtService = JwtService()
|
|
||||||
val repository = InMemoryKidioRepository()
|
|
||||||
|
|
||||||
application {
|
|
||||||
install(ServerContentNegotiation) {
|
|
||||||
json()
|
|
||||||
}
|
}
|
||||||
configureSecurity(jwtService)
|
|
||||||
configureRouting(repository, jwtService)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ohne Token muss 401 Unauthorized kommen
|
val token = jwtService.generateToken("default_user")
|
||||||
val tasksResponse = client.get("/api/v1/tasks")
|
|
||||||
assertEquals(HttpStatusCode.Unauthorized, tasksResponse.status)
|
// Ohne Token -> 401 Unauthorized
|
||||||
}
|
val unauthResponse = client.get("/api/v1/screentime")
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status)
|
||||||
|
|
||||||
|
// Mit Token -> 200 OK
|
||||||
|
val response =
|
||||||
|
client.get("/api/v1/screentime") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val json = Json.decodeFromString<SyncResponse>(response.bodyAsText())
|
||||||
|
assertEquals(1800, json.remainingSeconds)
|
||||||
|
assertFalse(json.isBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test registration and login flow`() = testApplication {
|
fun `test protected endpoints reject unauthorized requests`() =
|
||||||
val jwtService = JwtService()
|
testApplication {
|
||||||
val repository = InMemoryKidioRepository()
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
}
|
}
|
||||||
configureSecurity(jwtService)
|
|
||||||
configureRouting(repository, jwtService)
|
// Ohne Token muss 401 Unauthorized kommen
|
||||||
|
val tasksResponse = client.get("/api/v1/tasks")
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, tasksResponse.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Registrierung
|
@Test
|
||||||
val registerResponse = client.post("/api/v1/auth/register") {
|
fun `test registration and login flow`() =
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
testApplication {
|
||||||
setBody(
|
val jwtService = JwtService()
|
||||||
"""{
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Registrierung
|
||||||
|
val registerResponse =
|
||||||
|
client.post("/api/v1/auth/register") {
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody(
|
||||||
|
"""{
|
||||||
"email": "test@family.de",
|
"email": "test@family.de",
|
||||||
"passwordPlain": "strengGeheim1",
|
"passwordPlain": "strengGeheim1",
|
||||||
"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 =
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
client.post("/api/v1/auth/login") {
|
||||||
setBody(
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
"""{
|
setBody(
|
||||||
|
"""{
|
||||||
"email": "test@family.de",
|
"email": "test@family.de",
|
||||||
"passwordPlain": "strengGeheim1"
|
"passwordPlain": "strengGeheim1"
|
||||||
}"""
|
}""",
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, loginResponse.status)
|
||||||
|
val loginJson = Json.decodeFromString<AuthResponse>(loginResponse.bodyAsText())
|
||||||
|
assertNotNull(loginJson.token)
|
||||||
|
assertEquals("Familie Schmidt", loginJson.familyName)
|
||||||
}
|
}
|
||||||
assertEquals(HttpStatusCode.OK, loginResponse.status)
|
|
||||||
val loginJson = Json.decodeFromString<AuthResponse>(loginResponse.bodyAsText())
|
|
||||||
assertNotNull(loginJson.token)
|
|
||||||
assertEquals("Familie Schmidt", loginJson.familyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test tasks endpoint and verify with jwt`() = testApplication {
|
fun `test tasks endpoint and verify with jwt`() =
|
||||||
val jwtService = JwtService()
|
testApplication {
|
||||||
val repository = InMemoryKidioRepository()
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
}
|
}
|
||||||
configureSecurity(jwtService)
|
|
||||||
configureRouting(repository, jwtService)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.get("/api/v1/tasks") {
|
||||||
}
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
assertEquals(HttpStatusCode.OK, tasksResponse.status)
|
}
|
||||||
val tasksJson = Json.decodeFromString<List<Task>>(tasksResponse.bodyAsText())
|
assertEquals(HttpStatusCode.OK, tasksResponse.status)
|
||||||
assertEquals(2, tasksJson.size)
|
val tasksJson = Json.decodeFromString<List<Task>>(tasksResponse.bodyAsText())
|
||||||
assertEquals("t1", tasksJson[0].id)
|
assertEquals(2, tasksJson.size)
|
||||||
assertEquals("Was ist 12 + 15?", tasksJson[0].question)
|
assertEquals("t1", tasksJson[0].id)
|
||||||
|
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 =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.post("/api/v1/tasks/verify") {
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
}
|
setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
|
||||||
assertEquals(HttpStatusCode.OK, verifyResponse.status)
|
}
|
||||||
val verifyJson = Json.decodeFromString<TaskVerifyResponse>(verifyResponse.bodyAsText())
|
assertEquals(HttpStatusCode.OK, verifyResponse.status)
|
||||||
assertTrue(verifyJson.isCorrect)
|
val verifyJson = Json.decodeFromString<TaskVerifyResponse>(verifyResponse.bodyAsText())
|
||||||
assertEquals(10, verifyJson.earnedMinutes)
|
assertTrue(verifyJson.isCorrect)
|
||||||
|
assertEquals(10, verifyJson.earnedMinutes)
|
||||||
|
|
||||||
// 3. Verify Task incorrect answer
|
// 3. Verify Task incorrect answer
|
||||||
val verifyWrongResponse = client.post("/api/v1/tasks/verify") {
|
val verifyWrongResponse =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.post("/api/v1/tasks/verify") {
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, verifyWrongResponse.status)
|
||||||
|
val verifyWrongJson = Json.decodeFromString<TaskVerifyResponse>(verifyWrongResponse.bodyAsText())
|
||||||
|
assertFalse(verifyWrongJson.isCorrect)
|
||||||
|
assertEquals(0, verifyWrongJson.earnedMinutes)
|
||||||
}
|
}
|
||||||
assertEquals(HttpStatusCode.OK, verifyWrongResponse.status)
|
|
||||||
val verifyWrongJson = Json.decodeFromString<TaskVerifyResponse>(verifyWrongResponse.bodyAsText())
|
|
||||||
assertFalse(verifyWrongJson.isCorrect)
|
|
||||||
assertEquals(0, verifyWrongJson.earnedMinutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test screentime sync deducts time repeatedly`() = testApplication {
|
fun `test screentime sync deducts time repeatedly`() =
|
||||||
val jwtService = JwtService()
|
testApplication {
|
||||||
val repository = InMemoryKidioRepository()
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
}
|
}
|
||||||
configureSecurity(jwtService)
|
|
||||||
configureRouting(repository, jwtService)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.post("/api/v1/screentime/sync") {
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
}
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
|
||||||
assertEquals(HttpStatusCode.OK, syncResponse1.status)
|
}
|
||||||
val syncJson1 = Json.decodeFromString<SyncResponse>(syncResponse1.bodyAsText())
|
assertEquals(HttpStatusCode.OK, syncResponse1.status)
|
||||||
assertEquals(1600, syncJson1.remainingSeconds)
|
val syncJson1 = Json.decodeFromString<SyncResponse>(syncResponse1.bodyAsText())
|
||||||
assertFalse(syncJson1.isBlocked)
|
assertEquals(1600, syncJson1.remainingSeconds)
|
||||||
|
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 =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.post("/api/v1/screentime/sync") {
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
}
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
|
||||||
assertEquals(HttpStatusCode.OK, syncResponse2.status)
|
}
|
||||||
val syncJson2 = Json.decodeFromString<SyncResponse>(syncResponse2.bodyAsText())
|
assertEquals(HttpStatusCode.OK, syncResponse2.status)
|
||||||
assertEquals(1300, syncJson2.remainingSeconds)
|
val syncJson2 = Json.decodeFromString<SyncResponse>(syncResponse2.bodyAsText())
|
||||||
assertFalse(syncJson2.isBlocked)
|
assertEquals(1300, syncJson2.remainingSeconds)
|
||||||
|
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 =
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
client.post("/api/v1/screentime/sync") {
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, syncResponse3.status)
|
||||||
|
val syncJson3 = Json.decodeFromString<SyncResponse>(syncResponse3.bodyAsText())
|
||||||
|
assertEquals(0, syncJson3.remainingSeconds)
|
||||||
|
assertTrue(syncJson3.isBlocked)
|
||||||
}
|
}
|
||||||
assertEquals(HttpStatusCode.OK, syncResponse3.status)
|
|
||||||
val syncJson3 = Json.decodeFromString<SyncResponse>(syncResponse3.bodyAsText())
|
|
||||||
assertEquals(0, syncJson3.remainingSeconds)
|
|
||||||
assertTrue(syncJson3.isBlocked)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue