- fixed sonar issues
All checks were successful
CI Pipeline / lint-and-test (pull_request) Successful in 4m3s

- extracted data classes (ZumOerItem, ParsedH5PTask)
- moved hardcoded fallback OER tasks into a dedicated JSON resource file
- removed hardcoded tasks from InMemoryKidioRepository
This commit is contained in:
aldemirm 2026-09-16 10:17:32 +02:00
parent b3a01a4f16
commit 118677d960
9 changed files with 429 additions and 349 deletions

View file

@ -6,37 +6,8 @@ import com.oliver.kidio.backend.domain.models.User
import org.mindrot.jbcrypt.BCrypt
class InMemoryKidioRepository : KidioRepository {
private val tasks =
mutableListOf(
ImportedTask(
id = "t1",
externalSource = "manual",
subject = "Mathematik",
minAge = 6,
maxAge = 10,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 12 + 15?",
options = listOf("25", "27", "30", "22"),
correctAnswer = "27",
rewardMinutes = 10,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
ImportedTask(
id = "t2",
externalSource = "manual",
subject = "Mathematik",
minAge = 7,
maxAge = 12,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 8 x 7?",
options = listOf("54", "56", "64", "48"),
correctAnswer = "56",
rewardMinutes = 15,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
private val tasks = mutableListOf<ImportedTask>()
private val screentimes =
mutableMapOf(
"default_user" to 1800,

View file

@ -21,12 +21,9 @@ object H5PIngestionCli {
limit: Int = 50,
): Int =
runBlocking {
// Bereits vorhandene Task-IDs aus der Datenbank auslesen
val existingIds = repository.getExistingTaskIds("zum.de")
println("Bereits in DB vorhandene ZUM-Aufgaben: ${existingIds.size}")
var importedCount = 0
// existingIds an fetchLiveOerItems übergeben, um Duplikate vor dem Download zu filtern
val items =
client.fetchLiveOerItems(
subject = subject,
@ -34,12 +31,39 @@ object H5PIngestionCli {
existingIds = existingIds,
)
var importedCount = 0
for (item in items) {
var parsedList = emptyList<ParsedH5PTask>()
importedCount += processItem(item, client, repository)
}
// 1. Try parsing JSON content directly (fast, no ZIP decompression needed)
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
importedCount
}
private suspend fun processItem(
item: ZumOerItem,
client: ZumApiClient,
repository: KidioRepository,
): Int {
var importedCount = 0
val parsedList = extractTasks(item, client)
for (parsed in parsedList) {
val task = mapToImportedTask(item, parsed)
if (repository.upsertTask(task)) {
importedCount++
logSuccess(task)
}
}
return importedCount
}
private suspend fun extractTasks(
item: ZumOerItem,
client: ZumApiClient,
): List<ParsedH5PTask> {
if (item.rawJsonContent != null) {
parsedList =
val tasks =
H5PParser.parseAllTasks(
contentJson = item.rawJsonContent,
library = item.library ?: "H5P.MultiChoice",
@ -48,23 +72,30 @@ object H5PIngestionCli {
license = item.license,
sourceUrl = item.downloadUrl,
)
if (tasks.isNotEmpty()) return tasks
}
// 2. Fallback to downloading .h5p ZIP archive if remote archive is specified
if (parsedList.isEmpty() && item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
val h5pBytes = client.downloadH5P(item.downloadUrl)
if (h5pBytes != null) {
if (item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
val h5pBytes = client.downloadH5P(item.downloadUrl) ?: return emptyList()
val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl)
if (single != null) {
parsedList = listOf(single)
}
return listOf(single)
}
}
for (parsed in parsedList) {
return emptyList()
}
private fun mapToImportedTask(
item: ZumOerItem,
parsed: ParsedH5PTask,
): ImportedTask {
val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade)
val task =
ImportedTask(
val isTrueFalse = parsed.options.size == 2 && parsed.options.contains("Wahr")
return ImportedTask(
id = parsed.id,
externalSource = "zum.de",
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
@ -72,26 +103,19 @@ object H5PIngestionCli {
subject = ZumApiClient.mapSubject(item.subject),
minAge = minAge,
maxAge = maxAge,
taskType = if (parsed.options.size == 2 && parsed.options.contains("Wahr")) "TRUE_FALSE" else "MULTIPLE_CHOICE",
taskType = if (isTrueFalse) "TRUE_FALSE" else "MULTIPLE_CHOICE",
question = parsed.question,
options = parsed.options,
correctAnswer = parsed.correctAnswer,
rewardMinutes = 5,
)
}
val success = repository.upsertTask(task)
if (success) {
importedCount++
private fun logSuccess(task: ImportedTask) {
val shortQ = task.question.take(30)
val ageRange = "${task.minAge}-${task.maxAge}"
println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"")
}
}
}
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
importedCount
}
@JvmStatic
fun main(args: Array<String>) {

View file

@ -4,17 +4,6 @@ import kotlinx.serialization.json.*
import java.io.InputStream
import java.util.zip.ZipInputStream
data class ParsedH5PTask(
val id: String,
val title: String,
val library: String,
val question: String,
val options: List<String>,
val correctAnswer: String,
val license: String = "CC BY-SA 4.0",
val sourceUrl: String? = null,
)
object H5PParser {
private val json =
Json {
@ -79,9 +68,9 @@ object H5PParser {
return null
}
var library = "H5P.MultiChoice"
var title = "H5P Task"
var license = "CC BY-SA 4.0"
var library = DEFAULT_LIBRARY
var title = DEFAULT_TITLE
var license = DEFAULT_LICENSE
if (h5pJsonString != null) {
try {
@ -89,7 +78,8 @@ object H5PParser {
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
} catch (_: Exception) {
} catch (e: Exception) {
println("WARNUNG: h5p.json konnte nicht gelesen werden. Nutze Fallback-Werte. Fehler: ${e.message}")
}
}
@ -108,10 +98,10 @@ object H5PParser {
*/
fun parseContentJson(
contentJson: String,
library: String = "H5P.MultiChoice",
library: String = DEFAULT_LIBRARY,
taskId: String = "task-1",
title: String = "H5P Task",
license: String = "CC BY-SA 4.0",
title: String = DEFAULT_TITLE,
license: String = DEFAULT_LICENSE,
sourceUrl: String? = null,
): ParsedH5PTask? {
val element =
@ -133,10 +123,10 @@ object H5PParser {
*/
fun parseAllTasks(
contentJson: String,
library: String = "H5P.MultiChoice",
library: String = DEFAULT_LIBRARY,
taskId: String = "task-1",
title: String = "H5P Task",
license: String = "CC BY-SA 4.0",
title: String = DEFAULT_TITLE,
license: String = DEFAULT_LICENSE,
sourceUrl: String? = null,
): List<ParsedH5PTask> {
val element =
@ -148,40 +138,46 @@ object H5PParser {
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
val results = mutableListOf<ParsedH5PTask>()
for ((idx, qElement) in questionsArray.withIndex()) {
val qObj = qElement.jsonObject
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: "H5P.MultiChoice"
val subParams = qObj["params"]?.jsonObject ?: continue
val subId = "$taskId-q${idx + 1}"
val isTrueFalse =
subLib.contains("TrueFalse", ignoreCase = true) ||
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
val isMultiChoice =
subLib.contains("MultiChoice", ignoreCase = true) || subParams.containsKey("answers")
val parsedSub =
if (isTrueFalse) {
parseTrueFalse(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
} else if (isMultiChoice) {
parseMultiChoice(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
} else {
null
return questionsArray.mapIndexedNotNull { idx, qElement ->
parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl)
}
if (parsedSub != null) {
results.add(parsedSub)
}
}
return results
}
val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl)
return if (single != null) listOf(single) else emptyList()
}
private fun parseQuestionSetItem(
qElement: JsonElement,
idx: Int,
taskId: String,
title: String,
license: String,
sourceUrl: String?,
): ParsedH5PTask? {
val qObj = qElement.jsonObject
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: DEFAULT_LIBRARY
val subParams = qObj["params"]?.jsonObject ?: return null
val subId = "$taskId-q${idx + 1}"
val subTitle = "$title #${idx + 1}"
val isTrueFalse =
subLib.contains("TrueFalse", ignoreCase = true) ||
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
val isMultiChoice =
subLib.contains("MultiChoice", ignoreCase = true) ||
subParams.containsKey("answers")
return when {
isTrueFalse -> parseTrueFalse(subParams, subId, subTitle, subLib, license, sourceUrl)
isMultiChoice -> parseMultiChoice(subParams, subId, subTitle, subLib, license, sourceUrl)
else -> null
}
}
private fun parseMultiChoice(
element: JsonObject,
taskId: String,

View file

@ -0,0 +1,15 @@
package com.oliver.kidio.backend.ingestion
const val DEFAULT_LIBRARY: String = "H5P.MultiChoice"
const val DEFAULT_TITLE: String = "H5P Task"
data class ParsedH5PTask(
val id: String,
val title: String,
val library: String,
val question: String,
val options: List<String>,
val correctAnswer: String,
val license: String = DEFAULT_LICENSE,
val sourceUrl: String? = null,
)

View file

@ -8,17 +8,6 @@ import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
data class ZumOerItem(
val id: String,
val title: String,
val subject: String,
val grade: Int? = null,
val downloadUrl: String,
val license: String = "CC BY-SA 4.0",
val library: String? = null,
val rawJsonContent: String? = null,
)
class ZumApiClient(
private val httpClient: HttpClient =
HttpClient.newBuilder()
@ -113,52 +102,7 @@ class ZumApiClient(
for (page in 0 until maxPages) {
if (results.size >= limit) break
val searchUrl =
if (subject.isNullOrBlank()) {
"https://apps.zum.de/apps?page=$page"
} else {
val encoded = URLEncoder.encode(subject, "UTF-8")
"https://apps.zum.de/apps?keys=$encoded&page=$page"
}
val html = fetchString(searchUrl)
if (html == null) {
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
continue
}
// Filtert bereits verarbeitete oder in DB vorhandene Apps heraus
val appIds =
Regex("""href=["']/apps/(\d+)["']""")
.findAll(html)
.map { it.groupValues[1] }
.distinct()
.filter { appId ->
val fullId = "zum-$appId"
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
}
.toList()
if (appIds.isEmpty()) {
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
continue
}
for (appId in appIds) {
if (results.size >= limit) break
seenAppIds.add(appId)
val item = fetchAppDetails(appId, subject)
if (item != null) {
results.add(item)
val lib = item.library ?: "H5P"
val gr = item.grade ?: "k.A."
println(
"[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]",
)
}
}
processPage(page, subject, limit, existingIds, seenAppIds, results)
}
if (results.isEmpty()) {
@ -170,6 +114,78 @@ class ZumApiClient(
return results
}
private fun processPage(
page: Int,
subject: String?,
limit: Int,
existingIds: Set<String>,
seenAppIds: MutableSet<String>,
results: MutableList<ZumOerItem>,
) {
val searchUrl = buildSearchUrl(subject, page)
val html = fetchString(searchUrl)
if (html == null) {
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
return
}
val appIds = extractNewAppIds(html, existingIds, seenAppIds)
if (appIds.isEmpty()) {
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
return
}
for (appId in appIds) {
if (results.size >= limit) break
seenAppIds.add(appId)
val item = fetchAppDetails(appId, subject)
if (item != null) {
results.add(item)
logNewItem(appId, item)
}
}
}
private fun buildSearchUrl(
subject: String?,
page: Int,
): String {
return if (subject.isNullOrBlank()) {
"https://apps.zum.de/apps?page=$page"
} else {
val encoded = URLEncoder.encode(subject, "UTF-8")
"https://apps.zum.de/apps?keys=$encoded&page=$page"
}
}
private fun extractNewAppIds(
html: String,
existingIds: Set<String>,
seenAppIds: Set<String>,
): List<String> {
return Regex("""href=["']/apps/(\d+)["']""")
.findAll(html)
.map { it.groupValues[1] }
.distinct()
.filter { appId ->
val fullId = "zum-$appId"
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
}
.toList()
}
private fun logNewItem(
appId: String,
item: ZumOerItem,
) {
val lib = item.library ?: "H5P"
val gr = item.grade ?: "k.A."
println("[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]")
}
private fun fetchAppDetails(
appId: String,
fallbackSubject: String?,
@ -216,7 +232,7 @@ class ZumApiClient(
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
val license =
when {
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> "CC BY-SA 4.0"
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> DEFAULT_LICENSE
rawLicense.contains("CC", ignoreCase = true) -> rawLicense
else -> "CC BY-SA 4.0 ($rawLicense)"
}
@ -338,168 +354,26 @@ class ZumApiClient(
}
/**
* Returns curated ZUM/OER sample metadata items, filterable by subject.
* Returns curated ZUM/OER sample metadata items from resources, filterable by subject.
*/
fun getCuratedOerItems(
subject: String? = null,
limit: Int = 20,
): List<ZumOerItem> {
val allItems =
listOf(
ZumOerItem(
id = "zum-math-001",
title = "Grundrechenarten: Multiplikation 3. Klasse",
subject = "Mathematik",
grade = 3,
downloadUrl = "https://apps.zum.de/api/h5p/math-001.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Was ist das Ergebnis von <strong>6 &times; 8</strong>?</p>",
"answers": [
{"text": "42", "correct": false},
{"text": "48", "correct": true},
{"text": "54", "correct": false},
{"text": "56", "correct": false}
]
val allItems: List<ZumOerItem> =
try {
val inputStream = this::class.java.getResourceAsStream("/fallback_oer_items.json")
if (inputStream != null) {
val jsonText = inputStream.bufferedReader().use { it.readText() }
json.decodeFromString(jsonText)
} else {
println("WARNUNG: fallback_oer_items.json wurde nicht gefunden!")
emptyList()
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-math-002",
title = "Zahlenraum bis 100: Addition",
subject = "Mathematik",
grade = 2,
downloadUrl = "https://apps.zum.de/api/h5p/math-002.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Berechne: 34 + 28</p>",
"answers": [
{"text": "52", "correct": false},
{"text": "62", "correct": true},
{"text": "64", "correct": false}
]
} catch (e: Exception) {
println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}")
emptyList()
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-math-003",
title = "Geometrie: Formen erkennen",
subject = "Mathematik",
grade = 4,
downloadUrl = "https://apps.zum.de/api/h5p/math-003.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>",
"correct": true
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-de-001",
title = "Wortarten: Nomen erkennen",
subject = "Deutsch",
grade = 2,
downloadUrl = "https://apps.zum.de/api/h5p/de-001.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Welches der folgenden W&ouml;rter ist ein <strong>Nomen (Namenwort)</strong>?</p>",
"answers": [
{"text": "schnell", "correct": false},
{"text": "laufen", "correct": false},
{"text": "Schule", "correct": true},
{"text": "sch&ouml;n", "correct": false}
]
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-de-002",
title = "Rechtschreibung: Doppel-s oder ss",
subject = "Deutsch",
grade = 4,
downloadUrl = "https://apps.zum.de/api/h5p/de-002.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Wie wird das Wort richtig geschrieben? &bdquo;Der Flu... ist tief.&ldquo;</p>",
"answers": [
{"text": "Flus", "correct": false},
{"text": "Fluss", "correct": true},
{"text": "Fluß", "correct": false}
]
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-sach-001",
title = "Natur & Tiere: Winterschlaf",
subject = "Sachunterricht",
grade = 3,
downloadUrl = "https://apps.zum.de/api/h5p/sach-001.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Welches dieser Tiere h&auml;lt einen echten <strong>Winterschlaf</strong>?</p>",
"answers": [
{"text": "Igel", "correct": true},
{"text": "Eichh&ouml;rnchen", "correct": false},
{"text": "Amsel", "correct": false},
{"text": "Fuchs", "correct": false}
]
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-sach-002",
title = "Planetensystem: Sonnennähe",
subject = "Sachunterricht",
grade = 4,
downloadUrl = "https://apps.zum.de/api/h5p/sach-002.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>Welcher Planet ist der Sonne am n&auml;chsten?</p>",
"answers": [
{"text": "Venus", "correct": false},
{"text": "Mars", "correct": false},
{"text": "Merkur", "correct": true},
{"text": "Jupiter", "correct": false}
]
}
""".trimIndent(),
),
ZumOerItem(
id = "zum-eng-001",
title = "English Vocabulary: Colors & Animals",
subject = "Englisch",
grade = 4,
downloadUrl = "https://apps.zum.de/api/h5p/eng-001.h5p",
license = "CC BY-SA 4.0",
rawJsonContent =
"""
{
"question": "<p>What is the English word for &bdquo;Hund&ldquo;?</p>",
"answers": [
{"text": "Cat", "correct": false},
{"text": "Dog", "correct": true},
{"text": "Bird", "correct": false},
{"text": "Rabbit", "correct": false}
]
}
""".trimIndent(),
),
)
val normalizedSubject = subject?.let { mapSubject(it) }
val filtered =

View file

@ -0,0 +1,17 @@
package com.oliver.kidio.backend.ingestion
import kotlinx.serialization.Serializable
const val DEFAULT_LICENSE: String = "CC BY-SA 4.0"
@Serializable
data class ZumOerItem(
val id: String,
val title: String,
val subject: String,
val grade: Int? = null,
val downloadUrl: String,
val license: String = DEFAULT_LICENSE,
val library: String? = null,
val rawJsonContent: String? = null,
)

View file

@ -0,0 +1,74 @@
[
{
"id": "zum-math-001",
"title": "Grundrechenarten: Multiplikation 3. Klasse",
"subject": "Mathematik",
"grade": 3,
"downloadUrl": "https://apps.zum.de/api/h5p/math-001.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Was ist das Ergebnis von <strong>6 &times; 8</strong>?</p>\", \"answers\": [ {\"text\": \"42\", \"correct\": false}, {\"text\": \"48\", \"correct\": true}, {\"text\": \"54\", \"correct\": false}, {\"text\": \"56\", \"correct\": false} ] }"
},
{
"id": "zum-math-002",
"title": "Zahlenraum bis 100: Addition",
"subject": "Mathematik",
"grade": 2,
"downloadUrl": "https://apps.zum.de/api/h5p/math-002.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Berechne: 34 + 28</p>\", \"answers\": [ {\"text\": \"52\", \"correct\": false}, {\"text\": \"62\", \"correct\": true}, {\"text\": \"64\", \"correct\": false} ] }"
},
{
"id": "zum-math-003",
"title": "Geometrie: Formen erkennen",
"subject": "Mathematik",
"grade": 4,
"downloadUrl": "https://apps.zum.de/api/h5p/math-003.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>\", \"correct\": true }"
},
{
"id": "zum-de-001",
"title": "Wortarten: Nomen erkennen",
"subject": "Deutsch",
"grade": 2,
"downloadUrl": "https://apps.zum.de/api/h5p/de-001.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Welches der folgenden W&ouml;rter ist ein <strong>Nomen (Namenwort)</strong>?</p>\", \"answers\": [ {\"text\": \"schnell\", \"correct\": false}, {\"text\": \"laufen\", \"correct\": false}, {\"text\": \"Schule\", \"correct\": true}, {\"text\": \"sch&ouml;n\", \"correct\": false} ] }"
},
{
"id": "zum-de-002",
"title": "Rechtschreibung: Doppel-s oder ss",
"subject": "Deutsch",
"grade": 4,
"downloadUrl": "https://apps.zum.de/api/h5p/de-002.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Wie wird das Wort richtig geschrieben? &bdquo;Der Flu... ist tief.&ldquo;</p>\", \"answers\": [ {\"text\": \"Flus\", \"correct\": false}, {\"text\": \"Fluss\", \"correct\": true}, {\"text\": \"Fluß\", \"correct\": false} ] }"
},
{
"id": "zum-sach-001",
"title": "Natur & Tiere: Winterschlaf",
"subject": "Sachunterricht",
"grade": 3,
"downloadUrl": "https://apps.zum.de/api/h5p/sach-001.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Welches dieser Tiere h&auml;lt einen echten <strong>Winterschlaf</strong>?</p>\", \"answers\": [ {\"text\": \"Igel\", \"correct\": true}, {\"text\": \"Eichh&ouml;rnchen\", \"correct\": false}, {\"text\": \"Amsel\", \"correct\": false}, {\"text\": \"Fuchs\", \"correct\": false} ] }"
},
{
"id": "zum-sach-002",
"title": "Planetensystem: Sonnennähe",
"subject": "Sachunterricht",
"grade": 4,
"downloadUrl": "https://apps.zum.de/api/h5p/sach-002.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>Welcher Planet ist der Sonne am n&auml;chsten?</p>\", \"answers\": [ {\"text\": \"Venus\", \"correct\": false}, {\"text\": \"Mars\", \"correct\": false}, {\"text\": \"Merkur\", \"correct\": true}, {\"text\": \"Jupiter\", \"correct\": false} ] }"
},
{
"id": "zum-eng-001",
"title": "English Vocabulary: Colors & Animals",
"subject": "Englisch",
"grade": 4,
"downloadUrl": "https://apps.zum.de/api/h5p/eng-001.h5p",
"license": "CC BY-SA 4.0",
"rawJsonContent": "{ \"question\": \"<p>What is the English word for &bdquo;Hund&ldquo;?</p>\", \"answers\": [ {\"text\": \"Cat\", \"correct\": false}, {\"text\": \"Dog\", \"correct\": true}, {\"text\": \"Bird\", \"correct\": false}, {\"text\": \"Rabbit\", \"correct\": false} ] }"
}
]

View file

@ -1,6 +1,7 @@
package com.oliver.kidio.backend.com.oliver.kidio.backend.routing
package com.oliver.kidio.backend.routing
import com.oliver.kidio.backend.domain.models.AuthResponse
import com.oliver.kidio.backend.domain.models.ImportedTask
import com.oliver.kidio.backend.domain.models.SyncResponse
import com.oliver.kidio.backend.domain.models.Task
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
@ -19,6 +20,7 @@ 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.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@ -34,6 +36,41 @@ class ServerTest {
val jwtService = JwtService()
val repository = InMemoryKidioRepository()
runBlocking {
repository.upsertTask(
ImportedTask(
id = "t1",
externalSource = "manual",
subject = "Mathematik",
minAge = 6,
maxAge = 10,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 12 + 15?",
options = listOf("25", "27", "30", "22"),
correctAnswer = "27",
rewardMinutes = 10,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
repository.upsertTask(
ImportedTask(
id = "t2",
externalSource = "manual",
subject = "Mathematik",
minAge = 7,
maxAge = 12,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 8 x 7?",
options = listOf("54", "56", "64", "48"),
correctAnswer = "56",
rewardMinutes = 15,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
}
application {
install(ServerContentNegotiation) {
json()
@ -193,6 +230,41 @@ class ServerTest {
val jwtService = JwtService()
val repository = InMemoryKidioRepository()
runBlocking {
repository.upsertTask(
ImportedTask(
id = "t1",
externalSource = "manual",
subject = "Mathematik",
minAge = 6,
maxAge = 10,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 12 + 15?",
options = listOf("25", "27", "30", "22"),
correctAnswer = "27",
rewardMinutes = 10,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
repository.upsertTask(
ImportedTask(
id = "t2",
externalSource = "manual",
subject = "Mathematik",
minAge = 7,
maxAge = 12,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 8 x 7?",
options = listOf("54", "56", "64", "48"),
correctAnswer = "56",
rewardMinutes = 15,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
}
application {
install(ServerContentNegotiation) {
json()

View file

@ -1,5 +1,6 @@
package com.oliver.kidio.backend.routing
import com.oliver.kidio.backend.domain.models.ImportedTask
import com.oliver.kidio.backend.domain.models.Task
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
@ -17,6 +18,7 @@ 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.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@ -50,6 +52,41 @@ class TaskRoutesTest {
val jwtService = JwtService()
val repository = InMemoryKidioRepository()
runBlocking {
repository.upsertTask(
ImportedTask(
id = "t1",
externalSource = "manual",
subject = "Mathematik",
minAge = 6,
maxAge = 10,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 12 + 15?",
options = listOf("25", "27", "30", "22"),
correctAnswer = "27",
rewardMinutes = 10,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
repository.upsertTask(
ImportedTask(
id = "t2",
externalSource = "manual",
subject = "Mathematik",
minAge = 7,
maxAge = 12,
taskType = "MULTIPLE_CHOICE",
question = "Was ist 8 x 7?",
options = listOf("54", "56", "64", "48"),
correctAnswer = "56",
rewardMinutes = 15,
license = "CC BY-SA 4.0",
sourceUrl = "local",
),
)
}
application {
install(ServerContentNegotiation) {
json()