diff --git a/assets/monaco_ide.js b/assets/monaco_ide.js index d8d53d3..92b0f17 100644 --- a/assets/monaco_ide.js +++ b/assets/monaco_ide.js @@ -162,15 +162,25 @@ logToTerminal(`🧪 Führe automatisierte Tests aus (Strg+Shift+Enter)...\n----------------------------------------\n`, "info"); - const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal); + const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal, currentChapterData); - if (result.success) { + const isGenuineSuccess = Boolean( + result && + result.success === true && + result.total > 0 && + (!result.failures || result.failures === 0) && + (!result.errors || result.errors === 0) + ); + + if (isGenuineSuccess) { logToTerminal(`\n🎉 HERVORRAGEND! Alle ${result.total || 'erforderlichen'} Tests erfolgreich bestanden! (+100 XP)\n`, "success"); triggerSuccessCelebration(); recordChapterSolved(); } else { - logToTerminal(`\n⚠️ ${result.failures || 1} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning"); - if (result.rawOutput) { + const failCount = (result && (result.failures || result.errors)) ? (result.failures + (result.errors || 0)) : 1; + const errorMsg = (result && result.error) ? `\n❌ ${result.error}\n` : `\n⚠️ ${failCount} Test(s) fehlgeschlagen oder unvollständig. Überarbeite deinen Code!\n`; + logToTerminal(errorMsg, "warning"); + if (result && result.rawOutput) { suggestErrorInterpreter(result.rawOutput); } } diff --git a/assets/runners/cpp_runner.js b/assets/runners/cpp_runner.js index 0ef1e65..0ff212a 100644 --- a/assets/runners/cpp_runner.js +++ b/assets/runners/cpp_runner.js @@ -56,18 +56,154 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe GoogleTest (gtest) Suite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("TEST(") || l.trim().startsWith("// TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`[ RUN ] CppTestSuite.TestCase_${i}\n[ OK ] CppTestSuite.TestCase_${i} (0 ms)\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Der C/C++ Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback(`\n[==========] ${total} tests from 1 test suite ran. (1 ms total)\n[ PASSED ] ${total} tests.\n🎉 Alle GoogleTest Assertions erfolgreich bestanden!\n`, "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Führe GoogleTest (gtest) Suite aus...\n----------------------------------------\n", "info"); + + // Prüfe Klammer-Balance + let braceCount = 0; + for (const char of (userCode || "")) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + } + if (braceCount !== 0) { + logCallback(`\n❌ C++ Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" }; + } + + const testCases = []; + const testRegex = /TEST\s*\(\s*([A-Za-z0-9_]+)\s*,\s*([A-Za-z0-9_]+)\s*\)\s*\{([\s\S]*?)\n\}/g; + let match; + while ((match = testRegex.exec(testCode)) !== null) { + testCases.push({ + fullName: `${match[1]}.${match[2]}`, + target: match[2], + body: match[3] + }); + } + if (testCases.length === 0) { + const commentRegex = /\/\/\s*TEST:\s*(?:Teilziel\s+\d+\s*-\s*)?([A-Za-z0-9_]+)/gi; + while ((match = commentRegex.exec(testCode)) !== null) { + testCases.push({ + fullName: `CppTestSuite.${match[1]}`, + target: match[1], + body: "" + }); + } + } + if (testCases.length === 0) { + const userSymbols = [...(userCode || "").matchAll(/(?:[A-Za-z0-9_]+[*&\s]+)+([A-Za-z0-9_]+)\s*\([^)]*\)\s*\{/g)].map(m => m[1]).filter(s => s !== "if" && s !== "for" && s !== "while" && s !== "switch" && s !== "main"); + if (userSymbols.length > 0) { + userSymbols.forEach(s => testCases.push({ + fullName: `CppTestSuite.${s}`, + target: s, + body: "" + })); + } else { + testCases.push({ + fullName: "CppTestSuite.ExerciseLogic", + target: "ExerciseLogic", + body: "" + }); + } + } + + let passed = 0; + let failed = 0; + + for (const test of testCases) { + const testName = test.fullName; + let testPassed = true; + let failReason = ""; + + // Finde das zu testende C/C++ Symbol + let searchSymbol = ""; + if (test.target && userCode.includes(test.target)) { + searchSymbol = test.target; + } else if (test.body) { + // Extrahiere Funktionsaufrufe im Testkörper + const calls = [...test.body.matchAll(/\b([A-Za-z0-9_]{3,})\s*\(/g)].map(m => m[1]); + for (const c of calls) { + if (c !== "assert" && c !== "EXPECT_EQ" && c !== "ASSERT_TRUE" && c !== "sizeof" && c !== "memset" && userCode.includes(c)) { + searchSymbol = c; + break; + } + } + } + if (!searchSymbol) { + searchSymbol = test.target.replace(/^Test/, ""); + } + + if (searchSymbol !== "ExerciseLogic" && !userCode.includes(searchSymbol)) { + testPassed = false; + failReason = `Funktion, Klasse oder Methode '${searchSymbol}' nicht deklariert.`; + } else { + // Extrahiere exakten Funktionsrumpf innerhalb der geschweiften Klammern + let fnBody = ""; + if (searchSymbol === "ExerciseLogic") { + fnBody = userCode; + } else { + const fnIdx = userCode.indexOf(searchSymbol); + if (fnIdx !== -1) { + const openBrace = userCode.indexOf("{", fnIdx); + if (openBrace !== -1) { + let depth = 1; + let endIdx = openBrace + 1; + while (endIdx < userCode.length && depth > 0) { + if (userCode[endIdx] === '{') depth++; + else if (userCode[endIdx] === '}') depth--; + endIdx++; + } + fnBody = userCode.slice(openBrace + 1, endIdx - 1); + } + } + } + + const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); + const isStubOnly = ( + cleanBody === "" || + cleanBody === 'return "";' || + cleanBody === 'return 0;' || + cleanBody === 'return 0.0;' || + cleanBody === 'return false;' || + cleanBody === 'return nullptr;' || + cleanBody === 'return NULL;' || + cleanBody === 'return;' + ); + + if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) { + testPassed = false; + failReason = `Symbol '${searchSymbol}' enthält noch ungelöste TODOs/Teilziele.`; + } else if (isStubOnly) { + testPassed = false; + failReason = `Symbol '${searchSymbol}' gibt noch Standard-Platzhalterwerte zurück.`; + } + } + + if (testPassed) { + logCallback(`[ RUN ] ${testName}\n[ OK ] ${testName} (0 ms)\n`, "stdout"); + passed++; + } else { + logCallback(`[ RUN ] ${testName}\n[ FAILED ] ${testName} (0 ms)\n gtest error: ${failReason}\n`, "stderr"); + failed++; + } + } + + if (failed === 0 && passed > 0) { + logCallback(`\n[==========] ${testCases.length} tests from 1 test suite ran. (1 ms total)\n[ PASSED ] ${passed} tests.\n🎉 Alle GoogleTest Assertions erfolgreich bestanden!\n`, "success"); + return { success: true, total: testCases.length, passed, failures: 0 }; + } else { + logCallback(`\n[==========] ${testCases.length} tests from 1 test suite ran.\n[ FAILED ] ${failed} tests.\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning"); + return { success: false, total: testCases.length, passed, failures: failed }; + } } }; diff --git a/assets/runners/csharp_runner.js b/assets/runners/csharp_runner.js index a6161de..f858efd 100644 --- a/assets/runners/csharp_runner.js +++ b/assets/runners/csharp_runner.js @@ -56,18 +56,132 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe `dotnet test` (xUnit) Testsuite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("[Fact]") || l.trim().startsWith("[Theory]") || l.trim().startsWith("// TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`[xUnit.net 00:00:00.01] Test_${i} [PASS]\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Der C#-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\nPassed! - Failed: 0, Passed: " + total + ", Skipped: 0, Total: " + total + "\n🎉 Alle xUnit Tests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Führe `dotnet test` (xUnit) Testsuite aus...\n----------------------------------------\n", "info"); + + // Prüfe Klammer-Balance + let braceCount = 0; + for (const char of (userCode || "")) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + } + if (braceCount !== 0) { + logCallback(`\n❌ C# Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" }; + } + + const testCases = []; + const testRegex = /(?:void|public void|Task)\s+(Test[A-Za-z0-9_]+)\s*\(/g; + let match; + while ((match = testRegex.exec(testCode)) !== null) { + testCases.push(match[1]); + } + if (testCases.length === 0) { + const commentRegex = /\/\/\s*TEST:\s*(?:Testet\s+)?([A-Za-z0-9_]+)/gi; + while ((match = commentRegex.exec(testCode)) !== null) { + testCases.push(`Test_${match[1]}`); + } + } + if (testCases.length === 0) { + const userSymbols = [...(userCode || "").matchAll(/(?:public|private|static|\s)+\s+(?:class|[A-Za-z0-9_<>[\]]+)\s+([A-Za-z0-9_]+)\s*(?:\(|\{)/g)].map(m => m[1]).filter(s => s !== "class" && s !== "Aufgabe"); + if (userSymbols.length > 0) { + userSymbols.forEach(s => testCases.push(`Test_${s}`)); + } else { + testCases.push("TestExerciseLogic"); + } + } + + let passed = 0; + let failed = 0; + + for (const testName of testCases) { + const targetMethod = testName.replace(/^Test_?/, ""); + let testPassed = true; + let failReason = ""; + + if (targetMethod !== "ExerciseLogic" && !userCode.includes(targetMethod)) { + testPassed = false; + failReason = `Methode, Eigenschaft oder Klasse '${targetMethod}' nicht gefunden.`; + } else { + // Extrahiere exakten Methodenrumpf (unterstützt {} Blöcke und => Expressions) + let fnBody = ""; + let searchSym = targetMethod; + if (searchSym === "ExerciseLogic") { + fnBody = userCode; + } else { + const fnIdx = userCode.indexOf(searchSym); + if (fnIdx !== -1) { + const openBrace = userCode.indexOf("{", fnIdx); + const arrowIdx = userCode.indexOf("=>", fnIdx); + if (arrowIdx !== -1 && (openBrace === -1 || arrowIdx < openBrace)) { + // Expression-bodied member e.g. => ...; + const semiIdx = userCode.indexOf(";", arrowIdx); + if (semiIdx !== -1) { + fnBody = userCode.slice(arrowIdx + 2, semiIdx); + } + } else if (openBrace !== -1) { + let depth = 1; + let endIdx = openBrace + 1; + while (endIdx < userCode.length && depth > 0) { + if (userCode[endIdx] === '{') depth++; + else if (userCode[endIdx] === '}') depth--; + endIdx++; + } + fnBody = userCode.slice(openBrace + 1, endIdx - 1); + } + } + } + + const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); + const isStubOnly = ( + cleanBody === "" || + cleanBody === 'throw new NotImplementedException();' || + cleanBody === 'throw new NotImplementedException()' || + cleanBody === 'return "";' || + cleanBody === 'return 0;' || + cleanBody === 'return 0.0;' || + cleanBody === 'return false;' || + cleanBody === 'return null;' || + cleanBody === 'return;' + ); + + if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) { + testPassed = false; + failReason = `Methode '${targetMethod}' enthält noch ungelöste TODOs/Teilziele.`; + } else if (/throw new NotImplementedException/i.test(fnBody)) { + testPassed = false; + failReason = `Methode '${targetMethod}' wirft noch 'NotImplementedException()'.`; + } else if (isStubOnly) { + testPassed = false; + failReason = `Methode '${targetMethod}' gibt noch Standard-Platzhalterwerte zurück.`; + } + } + + if (testPassed) { + logCallback(`[xUnit.net 00:00:00.01] ${testName} [PASS]\n`, "stdout"); + passed++; + } else { + logCallback(`[xUnit.net 00:00:00.01] ${testName} [FAIL]\n Assert.Equal() Failure: ${failReason}\n`, "stderr"); + failed++; + } + } + + if (failed === 0 && passed > 0) { + logCallback(`\nPassed! - Failed: 0, Passed: ${passed}, Skipped: 0, Total: ${testCases.length}\n🎉 Alle xUnit Tests erfolgreich bestanden!\n`, "success"); + return { success: true, total: testCases.length, passed, failures: 0 }; + } else { + logCallback(`\nFailed! - Failed: ${failed}, Passed: ${passed}, Skipped: 0, Total: ${testCases.length}\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning"); + return { success: false, total: testCases.length, passed, failures: failed }; + } } }; diff --git a/assets/runners/go_runner.js b/assets/runners/go_runner.js index 53c9ffb..fbfc93c 100644 --- a/assets/runners/go_runner.js +++ b/assets/runners/go_runner.js @@ -56,18 +56,104 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe `go test -v ./...` Test-Suite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("func Test") || l.trim().startsWith("// TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`=== RUN TestFunction_${i}\n--- PASS: TestFunction_${i} (0.00s)\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Der Go-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Funktionen und entferne die TODO-Hinweise.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\nPASS\nok exercise/package 0.004s\n🎉 Alle Go-Unittests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Führe `go test -v ./...` Test-Suite aus...\n----------------------------------------\n", "info"); + + // Prüfe Klammer-Balance + let braceCount = 0; + for (const char of (userCode || "")) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + } + if (braceCount !== 0) { + logCallback(`\n❌ Go Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" }; + } + + const testCases = []; + const testRegex = /func\s+(Test[A-Za-z0-9_]+)\s*\(/g; + let match; + while ((match = testRegex.exec(testCode)) !== null) { + testCases.push(match[1]); + } + if (testCases.length === 0) testCases.push("TestExerciseLogic"); + + let passed = 0; + let failed = 0; + + for (const testName of testCases) { + const targetFn = testName.replace(/^Test/, ""); + const fnRegex = new RegExp(`func\\s+${targetFn}\\b`); + + let testPassed = true; + let failReason = ""; + + if (targetFn !== "ExerciseLogic" && !fnRegex.test(userCode) && !userCode.includes(targetFn)) { + testPassed = false; + failReason = `Funktion '${targetFn}' nicht deklariert oder falsche Signatur.`; + } else { + // Extrahiere exakten Funktionsrumpf innerhalb der geschweiften Klammern + let fnBody = ""; + const fnIdx = userCode.indexOf(targetFn); + if (fnIdx !== -1) { + const openBrace = userCode.indexOf("{", fnIdx); + if (openBrace !== -1) { + let depth = 1; + let endIdx = openBrace + 1; + while (endIdx < userCode.length && depth > 0) { + if (userCode[endIdx] === '{') depth++; + else if (userCode[endIdx] === '}') depth--; + endIdx++; + } + fnBody = userCode.slice(openBrace + 1, endIdx - 1); + } + } + + const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); + const isStubOnly = ( + cleanBody === "" || + cleanBody === 'return "", 0, 0, false' || + cleanBody === 'return 0.0' || + cleanBody === 'return 0' || + cleanBody === 'return ""' || + cleanBody === 'return false' || + cleanBody === 'return' + ); + + if (/\/\/\s*TODO:/i.test(fnBody)) { + testPassed = false; + failReason = `Funktion '${targetFn}' enthält noch ungelöste TODO-Anweisungen.`; + } else if (isStubOnly) { + testPassed = false; + failReason = `Funktion '${targetFn}' gibt noch Standard-Platzhalterwerte zurück.`; + } + } + + if (testPassed) { + logCallback(`=== RUN ${testName}\n--- PASS: ${testName} (0.00s)\n`, "stdout"); + passed++; + } else { + logCallback(`=== RUN ${testName}\n--- FAIL: ${testName} (0.00s)\n exercise_test.go: ${failReason}\n`, "stderr"); + failed++; + } + } + + if (failed === 0 && passed > 0) { + logCallback(`\nPASS\nok exercise/package 0.004s\n🎉 Alle ${passed} Go-Unittests erfolgreich bestanden!\n`, "success"); + return { success: true, total: testCases.length, passed, failures: 0 }; + } else { + logCallback(`\nFAIL\nexit status 1\nFAIL exercise/package 0.005s\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning"); + return { success: false, total: testCases.length, passed, failures: failed }; + } } }; diff --git a/assets/runners/html_css_runner.js b/assets/runners/html_css_runner.js index 807a48a..67de2ec 100644 --- a/assets/runners/html_css_runner.js +++ b/assets/runners/html_css_runner.js @@ -54,7 +54,16 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Die HTML/CSS-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Tags und CSS-Regeln.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } + } + logCallback("🧪 Führe automatisierte DOM- & CSS-Prüfungen aus...\n----------------------------------------\n", "info"); const parser = new DOMParser(); diff --git a/assets/runners/java_runner.js b/assets/runners/java_runner.js index 40108ae..0e97667 100644 --- a/assets/runners/java_runner.js +++ b/assets/runners/java_runner.js @@ -56,18 +56,154 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe JUnit 5 Testsuite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("@Test") || l.trim().startsWith("// TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`[✔] JUnit 5 Test ${i}/${total} PASSED\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Der Java-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\n🎉 JUnit 5: Alle Tests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Führe JUnit 5 Testsuite aus...\n----------------------------------------\n", "info"); + + // Prüfe Klammer-Balance + let braceCount = 0; + for (const char of (userCode || "")) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + } + if (braceCount !== 0) { + logCallback(`\n❌ Java Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" }; + } + + const testCases = []; + const testRegex = /(?:void|public void|public static void)\s+(test[A-Za-z0-9_]+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g; + let match; + while ((match = testRegex.exec(testCode)) !== null) { + testCases.push({ + name: match[1], + body: match[2] + }); + } + if (testCases.length === 0) { + const commentRegex = /\/\/\s*TEST:\s*(?:Testet\s+)?([A-Za-z0-9_]+)/gi; + while ((match = commentRegex.exec(testCode)) !== null) { + testCases.push({ + name: `test${match[1]}`, + body: "" + }); + } + } + if (testCases.length === 0) { + const userSymbols = [...(userCode || "").matchAll(/(?:public|private|protected|static|\s)+\s+(?:class|record|[A-Za-z0-9_<>[\]]+)\s+([A-Za-z0-9_]+)\s*(?:\(|\{)/g)].map(m => m[1]).filter(s => s !== "class" && s !== "record" && s !== "Aufgabe" && s !== "Musterloesung"); + if (userSymbols.length > 0) { + userSymbols.forEach(s => testCases.push({ name: `test${s}`, body: "" })); + } else { + testCases.push({ name: "testAufgabeLogic", body: "" }); + } + } + + let passed = 0; + let failed = 0; + + for (const test of testCases) { + const testName = test.name; + let testPassed = true; + let failReason = ""; + + // Ermittle das Zielsymbol (Klasse oder Methode) + let searchSymbol = ""; + const targetMethod = testName.replace(/^test_?/, ""); + + if (userCode.includes(targetMethod)) { + searchSymbol = targetMethod; + } else if (test.body) { + // Extrahiere Aufrufe wie Aufgabe.XYZ oder Klassen/Methodennamen im Test-Body + const aufgabeMatches = [...test.body.matchAll(/Aufgabe\.([A-Za-z0-9_]+)/g)].map(m => m[1]); + for (const a of aufgabeMatches) { + if (userCode.includes(a)) { + searchSymbol = a; + break; + } + } + if (!searchSymbol) { + const calls = [...test.body.matchAll(/\b([A-Za-z0-9_]{3,})\b/g)].map(m => m[1]); + for (const c of calls) { + if (c !== "assertEquals" && c !== "assertTrue" && c !== "assertFalse" && c !== "assert" && c !== "Aufgabe" && userCode.includes(c)) { + searchSymbol = c; + break; + } + } + } + } + if (!searchSymbol) { + searchSymbol = targetMethod; + } + + if (searchSymbol !== "AufgabeLogic" && !userCode.includes(searchSymbol)) { + testPassed = false; + failReason = `Symbol, Methode oder Klasse '${searchSymbol}' nicht im Java-Code gefunden.`; + } else { + // Extrahiere exakten Methodenrumpf innerhalb der geschweiften Klammern + let fnBody = ""; + if (searchSymbol === "AufgabeLogic") { + fnBody = userCode; + } else { + const fnIdx = userCode.indexOf(searchSymbol); + if (fnIdx !== -1) { + const openBrace = userCode.indexOf("{", fnIdx); + if (openBrace !== -1) { + let depth = 1; + let endIdx = openBrace + 1; + while (endIdx < userCode.length && depth > 0) { + if (userCode[endIdx] === '{') depth++; + else if (userCode[endIdx] === '}') depth--; + endIdx++; + } + fnBody = userCode.slice(openBrace + 1, endIdx - 1); + } + } + } + + const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); + const isStubOnly = ( + cleanBody === "" || + cleanBody === 'return "";' || + cleanBody === 'return 0;' || + cleanBody === 'return 0.0;' || + cleanBody === 'return false;' || + cleanBody === 'return null;' || + cleanBody === 'return;' + ); + + if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) { + testPassed = false; + failReason = `Bereich '${searchSymbol}' enthält noch ungelöste TODOs/Teilziele.`; + } else if (isStubOnly) { + testPassed = false; + failReason = `Bereich '${searchSymbol}' gibt noch Standard-Platzhalterwerte zurück.`; + } + } + + if (testPassed) { + logCallback(`[✔] JUnit 5: ${testName}() PASSED\n`, "stdout"); + passed++; + } else { + logCallback(`[✘] JUnit 5: ${testName}() FAILED\n AssertionError: ${failReason}\n`, "stderr"); + failed++; + } + } + + if (failed === 0 && passed > 0) { + logCallback(`\n🎉 JUnit 5: Alle ${passed} Tests erfolgreich bestanden!\n`, "success"); + return { success: true, total: testCases.length, passed, failures: 0 }; + } else { + logCallback(`\n⚠️ JUnit 5: ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning"); + return { success: false, total: testCases.length, passed, failures: failed }; + } } }; diff --git a/assets/runners/js_runner.js b/assets/runners/js_runner.js index 1b1afaa..3a48061 100644 --- a/assets/runners/js_runner.js +++ b/assets/runners/js_runner.js @@ -126,13 +126,22 @@ /** * Führt JS Unittests aus */ - async runTests(userCode, testCode, logCallback) { + async runTests(userCode, testCode, logCallback, chapterContext) { const sec = this.validateSecurity(userCode); if (!sec.safe) { logCallback(`\n${sec.error}\n\n`, "error"); return { success: false, error: sec.error }; } + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Die JavaScript-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } + } + return new Promise((resolve) => { const fullScript = ` let total = 0; diff --git a/assets/runners/powershell_runner.js b/assets/runners/powershell_runner.js index 2f855d8..1a6e351 100644 --- a/assets/runners/powershell_runner.js +++ b/assets/runners/powershell_runner.js @@ -58,26 +58,76 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe automatisierte Pester v5 Test-Suite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("It ") || l.trim().startsWith("# TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`[+] Pester Test ${i}/${total} bestanden [Passed]\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Das PowerShell-Skript wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\n🎉 Pester v5 Suite: Alle Tests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Starte automatisierte Pester Testsuite in isolierter PowerShell 7+ Sandbox...\n----------------------------------------\n", "info"); + + try { + const token = window.AUTH ? window.AUTH.getToken() : localStorage.getItem("auth_token"); + const isAD = chapterContext && (chapterContext.courseId === "active_directory" || chapterContext.courseId === "ad"); + const payload = { + language: isAD ? "active_directory" : "powershell", + base_path: (chapterContext && chapterContext.basePath) ? chapterContext.basePath : "", + user_code: userCode, + task_file: (chapterContext && chapterContext.taskFile) ? chapterContext.taskFile : "aufgabe.ps1", + test_file: (chapterContext && chapterContext.testFile) ? chapterContext.testFile : "test_aufgabe.ps1" + }; + + const res = await fetch("/api/runners/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { "Authorization": `Bearer ${token}` } : {}) + }, + body: JSON.stringify(payload) + }); + + const data = await res.json(); + + if (data.stdout) { + logCallback(data.stdout, "stdout"); + } + if (data.stderr && data.stderr.trim()) { + logCallback(data.stderr, "stderr"); + } + + if (data.error && !data.stdout) { + logCallback(`\n❌ Fehler: ${data.error}\n`, "error"); + } + + return { + success: Boolean(data.success), + total: data.total || 1, + passed: data.passed || 0, + failures: data.failures || (data.success ? 0 : 1), + rawOutput: (data.stdout || "") + "\n" + (data.stderr || "") + }; + } catch (err) { + logCallback(`\n❌ Verbindungsfehler zum Backend-Runner: ${err.message}\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, rawOutput: err.message }; + } } }; if (window.RunnerRegistry) { window.RunnerRegistry.register(PowerShellAdapter); + window.RunnerRegistry.register({ ...PowerShellAdapter, id: "active_directory" }); + window.RunnerRegistry.register({ ...PowerShellAdapter, id: "pwsh" }); } else { document.addEventListener("DOMContentLoaded", () => { - if (window.RunnerRegistry) window.RunnerRegistry.register(PowerShellAdapter); + if (window.RunnerRegistry) { + window.RunnerRegistry.register(PowerShellAdapter); + window.RunnerRegistry.register({ ...PowerShellAdapter, id: "active_directory" }); + window.RunnerRegistry.register({ ...PowerShellAdapter, id: "pwsh" }); + } }); } })(); diff --git a/assets/runners/python_runner.js b/assets/runners/python_runner.js index 6aee077..11ca44f 100644 --- a/assets/runners/python_runner.js +++ b/assets/runners/python_runner.js @@ -47,7 +47,9 @@ }); } - pyodideInstance = await window.loadPyodide(); + pyodideInstance = await window.loadPyodide({ + indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/" + }); // Sandbox-Härtung im Pyodide-Interpreter aktivieren if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) { @@ -113,41 +115,58 @@ /** * Führt automatisierte Unittests aus (🧪 Tests starten) */ - async runTests(userCode, testCode, logCallback) { + async runTests(userCode, testCode, logCallback, chapterContext) { const securityCheck = this.validateSecurity(userCode); if (!securityCheck.safe) { logCallback(`\n${securityCheck.error}\n\n`, "error"); return { success: false, error: securityCheck.error }; } + // Guard: Prüfe ob Startercode noch völlig unberührt ist + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Aufgabe noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } + } + const py = await this.loadEngine(logCallback); if (!py) return { success: false, error: "Pyodide nicht verfügbar" }; try { py.FS.writeFile("aufgabe.py", userCode); + py.FS.writeFile("test_aufgabe.py", testCode); const testRunnerScript = ` -import unittest, io, sys +import importlib, io, sys, unittest from unittest import TextTestRunner +if '.' not in sys.path: + sys.path.insert(0, '.') + for mod in list(sys.modules.keys()): if mod in ('aufgabe', 'test_aufgabe'): del sys.modules[mod] import aufgabe +import test_aufgabe +importlib.reload(aufgabe) +importlib.reload(test_aufgabe) -${testCode} - -suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +loader = unittest.TestLoader() +suite = loader.loadTestsFromModule(test_aufgabe) stream = io.StringIO() runner = TextTestRunner(stream=stream, verbosity=2) result = runner.run(suite) output_text = stream.getvalue() -success = result.wasSuccessful() total_runs = result.testsRun failures = len(result.failures) errors = len(result.errors) +success = (total_runs > 0) and (failures == 0) and (errors == 0) and result.wasSuccessful() + (success, total_runs, failures, errors, output_text) `; @@ -166,8 +185,10 @@ errors = len(result.errors) logCallback(outputText + "\n", "stdout"); + const hasPassed = Boolean(isSuccess) && totalRuns > 0 && fails === 0 && errs === 0; + return { - success: Boolean(isSuccess), + success: hasPassed, total: totalRuns, passed: totalRuns - (fails + errs), failures: fails, diff --git a/assets/runners/rust_runner.js b/assets/runners/rust_runner.js index d11305a..507be17 100644 --- a/assets/runners/rust_runner.js +++ b/assets/runners/rust_runner.js @@ -56,18 +56,107 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe `cargo test` Suite aus...\n----------------------------------------\n", "info"); - - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("#[test]") || l.trim().startsWith("// TEST:")); - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`test test_case_${i} ... ok\n`, "stdout"); + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Der Rust-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\ntest result: ok. " + total + " passed; 0 failed; 0 ignored\n🎉 Alle Cargo Tests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed: total, failures: 0 }; + logCallback("🧪 Führe `cargo test` Suite aus...\n----------------------------------------\n", "info"); + + // Prüfe Klammer-Balance + let braceCount = 0; + for (const char of (userCode || "")) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + } + if (braceCount !== 0) { + logCallback(`\n❌ Rust Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" }; + } + + const testCases = []; + const testRegex = /fn\s+(test_[A-Za-z0-9_]+)\s*\(/g; + let match; + while ((match = testRegex.exec(testCode)) !== null) { + testCases.push(match[1]); + } + if (testCases.length === 0) testCases.push("test_exercise_logic"); + + let passed = 0; + let failed = 0; + + for (const testName of testCases) { + const targetFn = testName.replace(/^test_/, ""); + let testPassed = true; + let failReason = ""; + + if (targetFn !== "exercise_logic" && !userCode.includes(targetFn)) { + testPassed = false; + failReason = `Funktion oder Typ '${targetFn}' nicht im Rust-Code gefunden.`; + } else { + let fnBody = ""; + const fnIdx = userCode.indexOf(targetFn); + if (fnIdx !== -1) { + const openBrace = userCode.indexOf("{", fnIdx); + if (openBrace !== -1) { + let depth = 1; + let endIdx = openBrace + 1; + while (endIdx < userCode.length && depth > 0) { + if (userCode[endIdx] === '{') depth++; + else if (userCode[endIdx] === '}') depth--; + endIdx++; + } + fnBody = userCode.slice(openBrace + 1, endIdx - 1); + } + } + + const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); + const isStubOnly = ( + cleanBody === "" || + cleanBody === 'todo!()' || + cleanBody === 'todo!();' || + cleanBody === 'unimplemented!()' || + cleanBody === 'unimplemented!();' || + cleanBody === '0' || + cleanBody === '0.0' || + cleanBody === '()' || + cleanBody === 'false' || + cleanBody === 'true' + ); + + if (/todo!\(\)/.test(cleanBody) || /unimplemented!\(\)/.test(cleanBody)) { + testPassed = false; + failReason = `Enthält noch ungelöstes 'todo!()' oder 'unimplemented!()' Makro.`; + } else if (/\/\/\s*TODO:/i.test(fnBody)) { + testPassed = false; + failReason = `Enthält noch ungelöste TODO-Anweisungen.`; + } else if (isStubOnly) { + testPassed = false; + failReason = `Funktion '${targetFn}' gibt noch Standard-Platzhalterwerte zurück.`; + } + } + + if (testPassed) { + logCallback(`test ${testName} ... ok\n`, "stdout"); + passed++; + } else { + logCallback(`test ${testName} ... FAILED\n src/lib.rs: ${failReason}\n`, "stderr"); + failed++; + } + } + + if (failed === 0 && passed > 0) { + logCallback(`\ntest result: ok. ${passed} passed; 0 failed; 0 ignored\n🎉 Alle Cargo Tests erfolgreich bestanden!\n`, "success"); + return { success: true, total: testCases.length, passed, failures: 0 }; + } else { + logCallback(`\ntest result: FAILED. ${passed} passed; ${failed} failed; 0 ignored\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning"); + return { success: false, total: testCases.length, passed, failures: failed }; + } } }; diff --git a/assets/runners/shell_runner.js b/assets/runners/shell_runner.js index 2897dc1..2063664 100644 --- a/assets/runners/shell_runner.js +++ b/assets/runners/shell_runner.js @@ -63,29 +63,77 @@ return { success: true }; }, - async runTests(userCode, testCode, logCallback) { - logCallback("🧪 Führe automatisierte Shell-Unittests (BATS-Stil) aus...\n----------------------------------------\n", "info"); - - // Simuliere Test-Ausführung - const testLines = testCode.split("\n").filter(l => l.trim().startsWith("# TEST:") || l.trim().startsWith("assert")); - let passed = 0; - let total = Math.max(testLines.length, 4); - - for (let i = 1; i <= total; i++) { - logCallback(`✓ Test ${i}/${total} erfolgreich bestanden.\n`, "stdout"); - passed++; + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Das Shell-Skript wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } } - logCallback("\n🎉 100% aller Shell-Tests erfolgreich bestanden!\n", "success"); - return { success: true, total, passed, failures: 0 }; + logCallback("🧪 Starte automatisierte Testsuite in isolierter Linux-Sandbox...\n----------------------------------------\n", "info"); + + try { + const token = window.AUTH ? window.AUTH.getToken() : localStorage.getItem("auth_token"); + const payload = { + language: (chapterContext && (chapterContext.courseId === "git" ? "git" : (chapterContext.courseId === "dns_records" ? "dns" : "bash"))) || "bash", + base_path: (chapterContext && chapterContext.basePath) ? chapterContext.basePath : "", + user_code: userCode, + task_file: (chapterContext && chapterContext.taskFile) ? chapterContext.taskFile : "aufgabe.sh", + test_file: (chapterContext && chapterContext.testFile) ? chapterContext.testFile : "test_aufgabe.sh" + }; + + const res = await fetch("/api/runners/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { "Authorization": `Bearer ${token}` } : {}) + }, + body: JSON.stringify(payload) + }); + + const data = await res.json(); + + if (data.stdout) { + logCallback(data.stdout, "stdout"); + } + if (data.stderr && data.stderr.trim()) { + logCallback(data.stderr, "stderr"); + } + + if (data.error && !data.stdout) { + logCallback(`\n❌ Fehler: ${data.error}\n`, "error"); + } + + return { + success: Boolean(data.success), + total: data.total || 1, + passed: data.passed || 0, + failures: data.failures || (data.success ? 0 : 1), + rawOutput: (data.stdout || "") + "\n" + (data.stderr || "") + }; + } catch (err) { + logCallback(`\n❌ Verbindungsfehler zum Backend-Runner: ${err.message}\n`, "error"); + return { success: false, total: 1, passed: 0, failures: 1, rawOutput: err.message }; + } } }; if (window.RunnerRegistry) { window.RunnerRegistry.register(ShellAdapter); + window.RunnerRegistry.register({ ...ShellAdapter, id: "git" }); + window.RunnerRegistry.register({ ...ShellAdapter, id: "dns_records" }); + window.RunnerRegistry.register({ ...ShellAdapter, id: "dns" }); } else { document.addEventListener("DOMContentLoaded", () => { - if (window.RunnerRegistry) window.RunnerRegistry.register(ShellAdapter); + if (window.RunnerRegistry) { + window.RunnerRegistry.register(ShellAdapter); + window.RunnerRegistry.register({ ...ShellAdapter, id: "git" }); + window.RunnerRegistry.register({ ...ShellAdapter, id: "dns_records" }); + window.RunnerRegistry.register({ ...ShellAdapter, id: "dns" }); + } }); } })(); diff --git a/assets/runners/sql_runner.js b/assets/runners/sql_runner.js index ac4369d..06d514b 100644 --- a/assets/runners/sql_runner.js +++ b/assets/runners/sql_runner.js @@ -126,7 +126,16 @@ } }, - async runTests(userCode, testCode, logCallback) { + async runTests(userCode, testCode, logCallback, chapterContext) { + if (chapterContext && chapterContext.starterCode) { + const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim(); + const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim(); + if (cleanUser === cleanStarter) { + logCallback("❌ FEHLER: Die SQL-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Tabellen und Abfragen.\n", "error"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" }; + } + } + const SQL = await this.loadEngine(logCallback); if (!SQL) return { success: false, error: "SQL-Engine nicht geladen" }; @@ -141,10 +150,29 @@ logCallback(formatted, "stdout"); db.close(); - return { success: true, total: 3, passed: 3, failures: 0 }; + + if (!testResults || testResults.length === 0) { + logCallback("⚠️ Die Validierungsabfrage ergab kein Tabellenergebnis.\n", "warning"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Keine Ergebnisdaten" }; + } + + let totalRows = 0; + for (const res of testResults) { + if (res.values && res.values.length > 0) { + totalRows += res.values.length; + } + } + + if (totalRows === 0) { + logCallback("⚠️ Die Testabfrage lieferte 0 Ergebniszeilen zurück. Überprüfe WHERE-Bedingungen und Datensätze!\n", "warning"); + return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "0 Ergebniszeilen" }; + } + + const totalTests = Math.max(testResults.length, 3); + return { success: true, total: totalTests, passed: totalTests, failures: 0 }; } catch (err) { logCallback(`\n❌ Test-Fehler im SQL:\n${err.message}\n`, "error"); - return { success: false, error: err.message, failures: 1 }; + return { success: false, error: err.message, total: 1, passed: 0, failures: 1, rawOutput: err.message }; } } }; diff --git a/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1 b/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1 index 7bcc28a..aafbe42 100644 --- a/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1 +++ b/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1 @@ -21,7 +21,7 @@ function Get-ADDomainArchitecture { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "01_ad_architektur_und_wald_baum_domaene" diff --git a/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1 b/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1 index cfe2b0a..308a607 100644 --- a/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1 +++ b/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1 @@ -21,7 +21,7 @@ function Get-FSMORoleOwner { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "02_domaenencontroller_und_fsmo_rollen" diff --git a/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1 b/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1 index af1e6b5..5d37fa1 100644 --- a/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1 +++ b/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1 @@ -21,7 +21,7 @@ function New-ADStructureOU { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "03_organisationseinheiten_und_struktur" diff --git a/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1 b/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1 index 22986da..d2c84ea 100644 --- a/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1 +++ b/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1 @@ -21,7 +21,7 @@ function Sync-ADReplicationSite { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "04_standorte_subnetze_und_replikation" diff --git a/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1 b/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1 index 92ed89f..4c56c75 100644 --- a/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1 +++ b/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1 @@ -21,7 +21,7 @@ function New-EnterpriseADUser { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "05_benutzerkonten_und_attribute" diff --git a/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1 b/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1 index b8d230d..3470249 100644 --- a/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1 +++ b/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1 @@ -21,7 +21,7 @@ function Add-DomainComputerAccount { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "06_computerkonten_und_domaenenbeitritt" diff --git a/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1 b/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1 index d8a8628..4693a9f 100644 --- a/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1 +++ b/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1 @@ -21,7 +21,7 @@ function New-SecurityGroupHierarchy { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "07_sicherheitsgruppen_und_bereichstypen" diff --git a/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1 b/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1 index 2562c85..cbc6ae1 100644 --- a/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1 +++ b/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1 @@ -21,7 +21,7 @@ function Deploy-AGDLPFramework { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "08_das_agdlp_prinzip_in_der_praxis" diff --git a/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1 b/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1 index b53265a..081c5c9 100644 --- a/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1 +++ b/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1 @@ -21,7 +21,7 @@ function New-TargetedGPO { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "09_gpo_grundlagen_und_lsdous" diff --git a/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1 b/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1 index 681b611..b1858d1 100644 --- a/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1 +++ b/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1 @@ -21,7 +21,7 @@ function Set-GPOLoopbackPolicy { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "10_computerkonfiguration_vs_benutzerkonfiguration" diff --git a/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1 b/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1 index ad7e79b..349b3ce 100644 --- a/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1 +++ b/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1 @@ -21,7 +21,7 @@ function New-WmiGpoFilter { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "11_sicherheitsfilterung_und_wmi_filter" diff --git a/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1 b/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1 index 0b8f9d1..2af4fa2 100644 --- a/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1 +++ b/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1 @@ -21,7 +21,7 @@ function Invoke-GPODiagnostics { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "12_gpo_troubleshooting_gpupdate_und_gpresult" diff --git a/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1 b/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1 index 09fbd56..9de832e 100644 --- a/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1 +++ b/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1 @@ -21,7 +21,7 @@ function Get-ADUserReport { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "13_powershell_activedirectory_modul_basics" diff --git a/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1 b/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1 index 6dbeb5f..5b3e1f0 100644 --- a/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1 +++ b/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1 @@ -21,7 +21,7 @@ function Import-ADUsersFromCSV { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "14_csv_bulk_import_und_provisioning" diff --git a/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1 b/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1 index 2384550..b3a16cb 100644 --- a/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1 +++ b/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1 @@ -21,7 +21,7 @@ function Set-ShareAndNtfsSecurity { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "15_ntfs_und_share_berechtigungssteuerung" diff --git a/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1 b/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1 index 722a574..f08bf42 100644 --- a/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1 +++ b/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1 @@ -21,7 +21,7 @@ function Invoke-ADEnterpriseAutomationSuite { # TODO: Implementiere die geforderten Active Directory Schritte $result = [PSCustomObject]@{ - Success = $true + Success = $false Domain = $Domain Status = "Ready" Module = "16_master_abschlussprojekt_ad_automation_suite" diff --git a/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh b/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh index b395edd..33ccdbb 100755 --- a/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh +++ b/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh @@ -13,7 +13,7 @@ analyze_dns_hierarchy() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh b/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh index 698374a..65e5fe1 100755 --- a/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh +++ b/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh @@ -13,7 +13,7 @@ trace_dns_resolution() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh b/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh index b99e789..b32eb90 100755 --- a/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh +++ b/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh @@ -13,7 +13,7 @@ inspect_dns_transport() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh b/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh index f90223a..e44f88f 100755 --- a/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh +++ b/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh @@ -13,7 +13,7 @@ calculate_ttl_migration() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh b/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh index 2825b45..8983ee2 100755 --- a/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh +++ b/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh @@ -13,7 +13,7 @@ configure_a_aaaa_records() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh b/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh index 5e0be8c..6e16296 100755 --- a/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh +++ b/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh @@ -13,7 +13,7 @@ validate_cname_rules() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh b/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh index cd46091..c6e081b 100755 --- a/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh +++ b/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh @@ -13,7 +13,7 @@ generate_soa_header() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh b/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh index d66cb5f..78a4709 100755 --- a/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh +++ b/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh @@ -13,7 +13,7 @@ configure_ns_and_ptr() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh b/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh index 872cde6..09bbf66 100755 --- a/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh +++ b/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh @@ -13,7 +13,7 @@ setup_mx_records() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh b/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh index 3756739..192a7ea 100755 --- a/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh +++ b/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh @@ -13,7 +13,7 @@ create_spf_record() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh b/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh index 8e35064..60d2328 100755 --- a/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh +++ b/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh @@ -13,7 +13,7 @@ publish_dkim_key() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh b/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh index 711bbb9..38c1ebe 100755 --- a/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh +++ b/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh @@ -13,7 +13,7 @@ deploy_dmarc_policy() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh b/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh index bc2ed14..4552cc2 100755 --- a/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh +++ b/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh @@ -13,7 +13,7 @@ configure_ad_srv_records() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh b/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh index 3f21ead..eb12ff5 100755 --- a/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh +++ b/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh @@ -13,7 +13,7 @@ setup_caa_and_dnssec() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh b/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh index 834dae8..ed54669 100755 --- a/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh +++ b/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh @@ -13,7 +13,7 @@ diagnose_dns_health() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh b/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh index cd387b2..7ed1f3f 100755 --- a/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh +++ b/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh @@ -13,7 +13,7 @@ audit_dns_zone() { echo "▶ Analysiere DNS-Konfiguration für $domain..." # TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen - return 0 + return 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/courses/git/01_git_init_und_repo_basics/aufgabe.sh b/courses/git/01_git_init_und_repo_basics/aufgabe.sh index 46cae10..8616efe 100755 --- a/courses/git/01_git_init_und_repo_basics/aufgabe.sh +++ b/courses/git/01_git_init_und_repo_basics/aufgabe.sh @@ -14,7 +14,7 @@ init_repository() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe init_repository in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/02_staging_commits_und_historie/aufgabe.sh b/courses/git/02_staging_commits_und_historie/aufgabe.sh index a015ea4..a04b1e8 100755 --- a/courses/git/02_staging_commits_und_historie/aufgabe.sh +++ b/courses/git/02_staging_commits_und_historie/aufgabe.sh @@ -14,7 +14,7 @@ manage_commits() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe manage_commits in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh b/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh index 6a70684..1ba2b6c 100755 --- a/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh +++ b/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh @@ -14,7 +14,7 @@ diff_inspection() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe diff_inspection in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh b/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh index 5f96d33..9b31399 100755 --- a/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh +++ b/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh @@ -14,7 +14,7 @@ setup_gitignore() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe setup_gitignore in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/05_branching_und_switch/aufgabe.sh b/courses/git/05_branching_und_switch/aufgabe.sh index db0ab82..9df43ee 100755 --- a/courses/git/05_branching_und_switch/aufgabe.sh +++ b/courses/git/05_branching_und_switch/aufgabe.sh @@ -14,7 +14,7 @@ manage_branches() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe manage_branches in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh b/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh index 48ff5b1..5e593df 100755 --- a/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh +++ b/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh @@ -14,7 +14,7 @@ execute_merges() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe execute_merges in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh b/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh index 9a79a86..9777c0a 100755 --- a/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh +++ b/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh @@ -14,7 +14,7 @@ resolve_conflicts() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe resolve_conflicts in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh b/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh index 7bcd10c..777dcab 100755 --- a/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh +++ b/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh @@ -14,7 +14,7 @@ use_stash() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe use_stash in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh b/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh index 5842df3..6b7574f 100755 --- a/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh +++ b/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh @@ -14,7 +14,7 @@ setup_remote() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe setup_remote in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh b/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh index 8984f20..f22fb71 100755 --- a/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh +++ b/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh @@ -14,7 +14,7 @@ sync_upstream() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe sync_upstream in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/11_git_tags_und_releases/aufgabe.sh b/courses/git/11_git_tags_und_releases/aufgabe.sh index 03ddaee..e0ad830 100755 --- a/courses/git/11_git_tags_und_releases/aufgabe.sh +++ b/courses/git/11_git_tags_und_releases/aufgabe.sh @@ -14,7 +14,7 @@ manage_tags() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe manage_tags in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh b/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh index 2827362..25e563b 100755 --- a/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh +++ b/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh @@ -14,7 +14,7 @@ cherry_pick_commit() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe cherry_pick_commit in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh b/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh index 2c65169..f05d2cf 100755 --- a/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh +++ b/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh @@ -14,7 +14,7 @@ rebase_feature() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe rebase_feature in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh b/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh index 38e7a6a..c4b25bd 100755 --- a/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh +++ b/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh @@ -14,7 +14,7 @@ interactive_rebase() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe interactive_rebase in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh b/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh index 9704855..d31c984 100755 --- a/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh +++ b/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh @@ -14,7 +14,7 @@ setup_pre_commit_hook() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe setup_pre_commit_hook in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh b/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh index f570e8c..b30f9d8 100755 --- a/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh +++ b/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh @@ -14,7 +14,7 @@ master_git_flow() { # TODO: Implementiere die geforderten Git-Befehle echo "Führe master_git_flow in $repo_dir aus..." - return 0 + return 1 } # Direkter Aufruf bei Skript-Ausführung diff --git a/server.py b/server.py index 15d078e..9e7c516 100644 --- a/server.py +++ b/server.py @@ -25,6 +25,9 @@ import re import uuid import urllib.parse import ipaddress +import subprocess +import tempfile +import shutil from pathlib import Path from collections import defaultdict @@ -314,18 +317,33 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): def is_path_blocked(self, path: str) -> bool: clean_path = urllib.parse.unquote(path).split("?")[0].lower() clean_path = os.path.normpath(clean_path).replace("\\", "/") - blocked_extensions = ( - ".db", ".sqlite", ".sqlite3", ".secret_key", ".py", ".sh", ".php", - ".jsonl", ".env", ".bak", ".sql", ".log", ".conf", ".ini", ".yaml", ".yml" - ) - if any(clean_path.endswith(ext) for ext in blocked_extensions): + + # 1. System-, Konfigurations- und geheime Dateien immer strikt sperren + if any(seg in clean_path for seg in ["/.git", "/.system_generated", "/.gemini", "platform_data.db", ".secret_key", ".env", ".jsonl"]): return True if "/." in clean_path or clean_path.startswith("."): return True if clean_path.startswith("/exams/") or clean_path == "/exams": return True - if any(seg in clean_path for seg in ["/.git", "/.system_generated", "/.gemini", "platform_data.db", ".secret_key"]): + if clean_path in ("/server.py", "server.py"): return True + + # 2. Gefährliche Endungen für Server-Root und PHP/DB + if any(clean_path.endswith(ext) for ext in [".db", ".sqlite", ".sqlite3", ".secret_key", ".php", ".env", ".bak", ".conf"]): + return True + + # 3. Kursdateien in den Lehrpfaden und Kursordnern explizit erlauben + is_course_file = ( + clean_path.startswith("/lehrpfad_") or + clean_path.startswith("lehrpfad_") or + clean_path.startswith("/courses/") or + clean_path.startswith("courses/") + ) + if not is_course_file: + blocked_outside = (".py", ".sh", ".sql", ".log", ".ini", ".yaml", ".yml") + if any(clean_path.endswith(ext) for ext in blocked_outside): + return True + return False def do_GET(self): @@ -1056,6 +1074,250 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): conn.close() return self.send_json({"success": True, "message": "Einstellungen gespeichert"}) + # 14. AUTOMATISIERTER RUNNER TESTSUITE ENDPUNKT (BASH, GIT, DNS, POWERSHELL, AD) + elif path == "/api/runners/test": + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert. Bitte melde dich an, um Tests auszuführen."}, 401) + + language = str(body.get("language", "")).strip().lower() + base_path = str(body.get("base_path", "")).strip() + user_code = str(body.get("user_code", ""))[:100_000] + task_file = str(body.get("task_file", "")).strip() + test_file = str(body.get("test_file", "")).strip() + + if not base_path or ".." in base_path or base_path.startswith("/") or "\\" in base_path: + return self.send_json({"error": "Ungültiger Pfad"}, 400) + + chapter_dir = os.path.normpath(os.path.join(str(BASE_DIR), base_path)) + if not os.path.isdir(chapter_dir) or not chapter_dir.startswith(str(BASE_DIR)): + return self.send_json({"error": "Kapitelverzeichnis nicht gefunden"}, 404) + + # Security: Gefährliche Shell/PowerShell-Befehle blockieren + danger_patterns = [ + r"\brm\s+-[rf]*\s+/(?:\s|$)", + r"\bmkfs\b", + r"\bdd\s+if=", + r"\bshutdown\b", + r"\breboot\b", + r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;", + r"\bcurl\b.*\|\s*(?:bash|sh)\b", + r"Remove-Item\s+-Recurse\s+[\'\"]?/(?:[\'\"]|\s|$)", + r"Format-Volume" + ] + for pat in danger_patterns: + if re.search(pat, user_code, re.IGNORECASE): + return self.send_json({ + "success": False, + "error": "Sicherheits-Sperre: Unzulässiger Systembefehl im Code erkannt.", + "stdout": "❌ Ausführung durch Sicherheitsfilter blockiert.\n", + "failures": 1, + "total": 1 + }, 400) + + sandbox = tempfile.mkdtemp(prefix="test_sandbox_") + try: + # 1. BASH / GIT / DNS EXECUTION + if language in ("bash", "shell", "git", "dns_records", "dns"): + actual_task_file = task_file or "aufgabe.sh" + actual_test_file = test_file or "test_aufgabe.sh" + src_test = os.path.join(chapter_dir, actual_test_file) + if not os.path.isfile(src_test): + return self.send_json({"error": f"Testdatei '{actual_test_file}' nicht gefunden"}, 404) + + shutil.copy(src_test, os.path.join(sandbox, actual_test_file)) + user_script_path = os.path.join(sandbox, actual_task_file) + with open(user_script_path, "w", encoding="utf-8") as f: + f.write(user_code) + os.chmod(user_script_path, 0o755) + os.chmod(os.path.join(sandbox, actual_test_file), 0o755) + + env = os.environ.copy() + env["HOME"] = sandbox + env["TERM"] = "xterm-256color" + + proc = subprocess.run( + ["bash", actual_test_file, actual_task_file], + cwd=sandbox, + capture_output=True, + text=True, + timeout=12, + env=env + ) + + stdout = proc.stdout or "" + stderr = proc.stderr or "" + exit_code = proc.returncode + + passed_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+bestanden", stdout) + fail_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+fehlgeschlagen", stdout) + + if exit_code == 0: + failures = 0 + passed = int(passed_match.group(1)) if passed_match else 4 + total = int(passed_match.group(2)) if passed_match else passed + else: + if fail_match: + failures = int(fail_match.group(1)) + total = int(fail_match.group(2)) + passed = max(0, total - failures) + else: + failures = 1 + passed = 0 + total = 1 + + return self.send_json({ + "success": exit_code == 0 and failures == 0 and total > 0, + "exit_code": exit_code, + "stdout": stdout, + "stderr": stderr, + "total": total, + "passed": passed, + "failures": failures + }) + + # 2. POWERSHELL / ACTIVE DIRECTORY EXECUTION + elif language in ("powershell", "pwsh", "active_directory", "ad"): + actual_task_file = task_file or "aufgabe.ps1" + actual_test_file = test_file or "test_aufgabe.ps1" + src_test = os.path.join(chapter_dir, actual_test_file) + if not os.path.isfile(src_test): + return self.send_json({"error": f"Testdatei '{actual_test_file}' nicht gefunden"}, 404) + + shutil.copy(src_test, os.path.join(sandbox, actual_test_file)) + user_script_path = os.path.join(sandbox, actual_task_file) + with open(user_script_path, "w", encoding="utf-8") as f: + f.write(user_code) + + pester_shim = f""" +function Describe ($name, [ScriptBlock]$block) {{ + Write-Host "`n🧪 Testsuite: $name" -ForegroundColor Cyan + & $block +}} +function Context ($name, [ScriptBlock]$block) {{ + Write-Host " 📂 $name" -ForegroundColor DarkCyan + & $block +}} +$script:PesterTotal = 0 +$script:PesterPassed = 0 +$script:PesterFailed = 0 + +function It ($name, [ScriptBlock]$block) {{ + $script:PesterTotal++ + try {{ + & $block + Write-Host " [+] $name [Passed]" -ForegroundColor Green + $script:PesterPassed++ + }} catch {{ + Write-Host " [-] $name [Failed]" -ForegroundColor Red + Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray + $script:PesterFailed++ + }} +}} + +function Should {{ + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $true)] + $Actual, + [switch]$Be, + [switch]$Not, + [switch]$BeGreaterThan, + [switch]$BeLessThan, + [switch]$Match, + [Parameter(Position = 0)] + $Expected + ) + process {{ + if ($Be) {{ + if ("$Actual" -ne "$Expected") {{ + throw "Expected: $Expected, but got: $Actual" + }} + }} elseif ($Not) {{ + if ("$Actual" -eq "$Expected") {{ + throw "Expected not: $Expected, but got equal value" + }} + }} elseif ($Match) {{ + if ("$Actual" -notmatch "$Expected") {{ + throw "Expected $Actual to match $Expected" + }} + }} else {{ + if ("$Actual" -ne "$Expected") {{ + throw "Expected: $Expected, but got: $Actual" + }} + }} + }} +}} + +. ./{actual_task_file} +. ./{actual_test_file} + +Write-Host "`n📊 Gesamtergebnis: $script:PesterPassed von $script:PesterTotal Tests bestanden." +if ($script:PesterFailed -gt 0 -or $script:PesterTotal -eq 0) {{ + Write-Host "❌ $script:PesterFailed Test(s) fehlgeschlagen!" -ForegroundColor Red + exit 1 +}} else {{ + Write-Host "🎉 Alle $script:PesterTotal Tests erfolgreich bestanden!" -ForegroundColor Green + exit 0 +}} +""" + runner_path = os.path.join(sandbox, "run_pester_test.ps1") + with open(runner_path, "w", encoding="utf-8") as f: + f.write(pester_shim) + + proc = subprocess.run( + ["pwsh", "-NoProfile", "-NonInteractive", "-File", "run_pester_test.ps1"], + cwd=sandbox, + capture_output=True, + text=True, + timeout=12 + ) + + stdout = proc.stdout or "" + stderr = proc.stderr or "" + exit_code = proc.returncode + + passed_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+bestanden", stdout) + if passed_match: + passed = int(passed_match.group(1)) + total = int(passed_match.group(2)) + failures = total - passed + else: + if exit_code == 0: + passed = 4 + total = 4 + failures = 0 + else: + passed = 0 + total = 1 + failures = 1 + + return self.send_json({ + "success": exit_code == 0 and failures == 0 and total > 0, + "exit_code": exit_code, + "stdout": stdout, + "stderr": stderr, + "total": total, + "passed": passed, + "failures": failures + }) + + else: + return self.send_json({"error": f"Kein Server-Runner für Sprache '{language}' konfiguriert"}, 400) + + except subprocess.TimeoutExpired: + return self.send_json({ + "success": False, + "error": "Zeitüberschreitung: Testausführung dauerte länger als 12 Sekunden.", + "stdout": "⏱️ Zeitüberschreitung bei der Testausführung.\n", + "failures": 1, + "total": 1 + }, 408) + except Exception as e: + return self.send_json({"error": f"Serverfehler bei Testausführung: {str(e)}"}, 500) + finally: + shutil.rmtree(sandbox, ignore_errors=True) + return self.send_json({"error": "Endpunkt nicht gefunden"}, 404) def run_server(): diff --git a/workspace.html b/workspace.html index eb2bfae..550da85 100644 --- a/workspace.html +++ b/workspace.html @@ -357,20 +357,20 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + +