From ab6d1204e60f395e30d04ad4da519783955c6ac6 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Wed, 25 Feb 2026 14:43:23 +0800 Subject: [PATCH] refactor(test): split web host api tests by domain --- .../work/slhaf/hub/WebAuthAndScriptApiTest.kt | 106 ++++++ .../kotlin/work/slhaf/hub/WebHostApiTest.kt | 347 ------------------ .../work/slhaf/hub/WebHostTestSupport.kt | 51 +++ .../kotlin/work/slhaf/hub/WebRunApiTest.kt | 123 +++++++ .../work/slhaf/hub/WebSubTokenApiTest.kt | 98 +++++ 5 files changed, 378 insertions(+), 347 deletions(-) create mode 100644 src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt delete mode 100644 src/test/kotlin/work/slhaf/hub/WebHostApiTest.kt create mode 100644 src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt create mode 100644 src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt create mode 100644 src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt diff --git a/src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt b/src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt new file mode 100644 index 0000000..06c3172 --- /dev/null +++ b/src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt @@ -0,0 +1,106 @@ +package work.slhaf.hub + +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.put +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WebAuthAndScriptApiTest : WebHostTestSupport() { + @Test + fun healthAndUnauthorized() = withApp { _ -> + val health = client.get("/health") + assertEquals(HttpStatusCode.OK, health.status) + assertEquals("OK", health.bodyAsText()) + + val scripts = client.get("/scripts") + assertEquals(HttpStatusCode.Unauthorized, scripts.status) + assertTrue(scripts.bodyAsText().contains("unauthorized")) + } + + @Test + fun scriptCrudMetaRunAndValidation() = withApp { scriptsDir -> + val create = client.post("/scripts/demo") { + bearerRoot() + setBody( + """ + // @desc: demo api + // @timeout: 10s + // @param: name | required=false | default=world | desc=Name to greet + val args: Array = emptyArray() + val kv = args.mapNotNull { + val i = it.indexOf('=') + if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) + }.toMap() + println("hi " + (kv["name"] ?: "world")) + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.Created, create.status) + + val list = client.get("/scripts") { bearerRoot() } + assertEquals(HttpStatusCode.OK, list.status) + assertTrue(list.bodyAsText().contains("demo")) + + val source = client.get("/scripts/demo") { bearerRoot() } + assertEquals(HttpStatusCode.OK, source.status) + assertTrue(source.bodyAsText().contains("@desc: demo api")) + + val meta = client.get("/meta/demo") { bearerRoot() } + assertEquals(HttpStatusCode.OK, meta.status) + val metaText = meta.bodyAsText() + assertTrue(metaText.contains("\"script\":\"demo\"")) + assertTrue(metaText.contains("\"timeoutMs\":10000")) + + val run = client.get("/run/demo?name=Alice") { bearerRoot() } + assertEquals(HttpStatusCode.OK, run.status) + assertTrue(run.bodyAsText().contains("hi Alice")) + + val invalidUpdate = client.put("/scripts/demo") { + bearerRoot() + setBody( + """ + // @desc: bad metadata + // @param: user name | required=maybe | xxx=1 + val args: Array = emptyArray() + println("bad") + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.BadRequest, invalidUpdate.status) + val invalidText = invalidUpdate.bodyAsText() + assertTrue(invalidText.contains("metadata validation failed")) + assertTrue(invalidText.contains("examples:")) + assertTrue(invalidText.contains("@param:")) + + val remove = client.delete("/scripts/demo") { bearerRoot() } + assertEquals(HttpStatusCode.OK, remove.status) + + assertFalse((scriptsDir.resolve("demo.hub.kts")).toFile().exists()) + } + + @Test + fun metadataRequiresExplicitRequiredField() = withApp { _ -> + val create = client.post("/scripts/badmeta") { + bearerRoot() + setBody( + """ + // @desc: bad metadata + // @param: name | default=world | desc=missing required + val args: Array = emptyArray() + println("ok") + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.BadRequest, create.status) + val body = create.bodyAsText() + assertTrue(body.contains("metadata validation failed")) + assertTrue(body.contains("missing required option")) + } +} diff --git a/src/test/kotlin/work/slhaf/hub/WebHostApiTest.kt b/src/test/kotlin/work/slhaf/hub/WebHostApiTest.kt deleted file mode 100644 index 25b3270..0000000 --- a/src/test/kotlin/work/slhaf/hub/WebHostApiTest.kt +++ /dev/null @@ -1,347 +0,0 @@ -package work.slhaf.hub - -import io.ktor.client.request.delete -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.put -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.server.testing.testApplication -import kotlinx.coroutines.sync.Semaphore -import kotlin.io.path.createTempDirectory -import kotlin.io.path.writeText -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class WebHostApiTest { - private val tempDirs = mutableListOf() - - @AfterTest - fun cleanup() { - tempDirs.forEach { path -> - runCatching { path.toFile().deleteRecursively() } - } - tempDirs.clear() - } - - @Test - fun healthAndUnauthorized() = withApp { _ -> - val health = client.get("/health") - assertEquals(HttpStatusCode.OK, health.status) - assertEquals("OK", health.bodyAsText()) - - val scripts = client.get("/scripts") - assertEquals(HttpStatusCode.Unauthorized, scripts.status) - assertTrue(scripts.bodyAsText().contains("unauthorized")) - } - - @Test - fun scriptCrudMetaRunAndValidation() = withApp { scriptsDir -> - val create = client.post("/scripts/demo") { - bearerRoot() - setBody( - """ - // @desc: demo api - // @timeout: 10s - // @param: name | required=false | default=world | desc=Name to greet - val args: Array = emptyArray() - val kv = args.mapNotNull { - val i = it.indexOf('=') - if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) - }.toMap() - println("hi " + (kv["name"] ?: "world")) - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.Created, create.status) - - val list = client.get("/scripts") { bearerRoot() } - assertEquals(HttpStatusCode.OK, list.status) - assertTrue(list.bodyAsText().contains("demo")) - - val source = client.get("/scripts/demo") { bearerRoot() } - assertEquals(HttpStatusCode.OK, source.status) - assertTrue(source.bodyAsText().contains("@desc: demo api")) - - val meta = client.get("/meta/demo") { bearerRoot() } - assertEquals(HttpStatusCode.OK, meta.status) - val metaText = meta.bodyAsText() - assertTrue(metaText.contains("\"script\":\"demo\"")) - assertTrue(metaText.contains("\"timeoutMs\":10000")) - - val run = client.get("/run/demo?name=Alice") { bearerRoot() } - assertEquals(HttpStatusCode.OK, run.status) - assertTrue(run.bodyAsText().contains("hi Alice")) - - val invalidUpdate = client.put("/scripts/demo") { - bearerRoot() - setBody( - """ - // @desc: bad metadata - // @param: user name | required=maybe | xxx=1 - val args: Array = emptyArray() - println("bad") - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.BadRequest, invalidUpdate.status) - val invalidText = invalidUpdate.bodyAsText() - assertTrue(invalidText.contains("metadata validation failed")) - assertTrue(invalidText.contains("examples:")) - assertTrue(invalidText.contains("@param:")) - - val remove = client.delete("/scripts/demo") { bearerRoot() } - assertEquals(HttpStatusCode.OK, remove.status) - - assertFalse((scriptsDir.resolve("demo.hub.kts")).toFile().exists()) - } - - @Test - fun subTokenAccessControlAndFiltering() = withApp { scriptsDir -> - scriptsDir.resolve("allowed.hub.kts").writeText( - """ - // @desc: allowed script - val args: Array = emptyArray() - println("allowed") - """.trimIndent() - ) - scriptsDir.resolve("blocked.hub.kts").writeText( - """ - // @desc: blocked script - val args: Array = emptyArray() - println("blocked") - """.trimIndent() - ) - - val createSub = client.post("/subtokens/demo-sub") { - bearerRoot() - setBody("allowed") - } - assertEquals(HttpStatusCode.Created, createSub.status) - val token = extractJsonField(createSub.bodyAsText(), "token") - assertNotNull(token) - - val type = client.get("/type") { bearer(token) } - assertEquals(HttpStatusCode.OK, type.status) - val typeText = type.bodyAsText() - assertTrue(typeText.contains("\"tokenType\":\"sub\"")) - assertTrue(typeText.contains("\"subTokenName\":\"demo-sub\"")) - - val scripts = client.get("/scripts") { bearer(token) } - assertEquals(HttpStatusCode.OK, scripts.status) - val scriptList = scripts.bodyAsText() - assertTrue(scriptList.contains("allowed")) - assertFalse(scriptList.contains("blocked")) - - val metaAllowed = client.get("/meta/allowed") { bearer(token) } - assertEquals(HttpStatusCode.OK, metaAllowed.status) - - val metaBlocked = client.get("/meta/blocked") { bearer(token) } - assertEquals(HttpStatusCode.Forbidden, metaBlocked.status) - - val runAllowed = client.get("/run/allowed") { bearer(token) } - assertEquals(HttpStatusCode.OK, runAllowed.status) - - val runBlocked = client.get("/run/blocked") { bearer(token) } - assertEquals(HttpStatusCode.Forbidden, runBlocked.status) - - val createScript = client.post("/scripts/not-allowed") { - bearer(token) - setBody("val args: Array = emptyArray()\nprintln(\"x\")") - } - assertEquals(HttpStatusCode.Forbidden, createScript.status) - - val listSubTokens = client.get("/subtokens") { bearer(token) } - assertEquals(HttpStatusCode.Forbidden, listSubTokens.status) - - val typeByPath = client.get("/u/demo-sub@$token/type") - assertEquals(HttpStatusCode.OK, typeByPath.status) - assertTrue(typeByPath.bodyAsText().contains("\"tokenType\":\"sub\"")) - - val scriptsByPath = client.get("/u/demo-sub@$token/scripts") - assertEquals(HttpStatusCode.OK, scriptsByPath.status) - assertTrue(scriptsByPath.bodyAsText().contains("allowed")) - assertFalse(scriptsByPath.bodyAsText().contains("blocked")) - - val metaByPathAllowed = client.get("/u/demo-sub@$token/meta/allowed") - assertEquals(HttpStatusCode.OK, metaByPathAllowed.status) - - val metaByPathBlocked = client.get("/u/demo-sub@$token/meta/blocked") - assertEquals(HttpStatusCode.Forbidden, metaByPathBlocked.status) - - val runByPathAllowed = client.get("/u/demo-sub@$token/run/allowed") - assertEquals(HttpStatusCode.OK, runByPathAllowed.status) - - val runByPathBlocked = client.get("/u/demo-sub@$token/run/blocked") - assertEquals(HttpStatusCode.Forbidden, runByPathBlocked.status) - - val invalidPathAuth = client.get("/u/demo-sub@invalid-token/scripts") - assertEquals(HttpStatusCode.Unauthorized, invalidPathAuth.status) - } - - @Test - fun runTimeoutReturnsRequestTimeout() = withApp { _ -> - val create = client.post("/scripts/slow") { - bearerRoot() - setBody( - """ - // @desc: slow script - // @timeout: 1ms - val args: Array = emptyArray() - Thread.sleep(100) - println("done") - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.Created, create.status) - - val run = client.get("/run/slow") { bearerRoot() } - assertEquals(HttpStatusCode.RequestTimeout, run.status) - assertTrue(run.bodyAsText().contains("timed out")) - } - - @Test - fun metadataRequiresExplicitRequiredField() = withApp { _ -> - val create = client.post("/scripts/badmeta") { - bearerRoot() - setBody( - """ - // @desc: bad metadata - // @param: name | default=world | desc=missing required - val args: Array = emptyArray() - println("ok") - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.BadRequest, create.status) - val body = create.bodyAsText() - assertTrue(body.contains("metadata validation failed")) - assertTrue(body.contains("missing required option")) - } - - @Test - fun runErrorResponseIncludesParamsAndRequiredCheck() = withApp { _ -> - val create = client.post("/scripts/runner") { - bearerRoot() - setBody( - """ - // @desc: run test - // @param: must | required=true | default=world | desc=Must be provided explicitly - // @param: boom | required=false | default=false | desc=Trigger runtime failure - val args: Array = emptyArray() - val kv = args.mapNotNull { - val i = it.indexOf('=') - if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) - }.toMap() - if ((kv["boom"] ?: "false").equals("true", ignoreCase = true)) { - error("boom") - } - println("must=" + (kv["must"] ?: "none")) - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.Created, create.status) - - val missingRequired = client.get("/run/runner") { bearerRoot() } - assertEquals(HttpStatusCode.BadRequest, missingRequired.status) - val missingBody = missingRequired.bodyAsText() - assertTrue(missingBody.contains("missing required params: must")) - assertTrue(missingBody.contains("params:")) - assertTrue(missingBody.contains("must (required=true")) - - val runtimeError = client.get("/run/runner?must=ok&boom=true") { bearerRoot() } - assertEquals(HttpStatusCode.InternalServerError, runtimeError.status) - val runtimeErrorBody = runtimeError.bodyAsText() - assertTrue(runtimeErrorBody.contains("script execution failed")) - assertTrue(runtimeErrorBody.contains("params:")) - assertTrue(runtimeErrorBody.contains("boom (required=false")) - } - - @Test - fun defaultParamValueIsInjectedIntoHostArgs() = withApp { _ -> - val create = client.post("/scripts/defaults") { - bearerRoot() - setBody( - """ - // @desc: default args test - // @param: name | required=false | default=world | desc=Name fallback - val args: Array = emptyArray() - val kv = args.mapNotNull { - val i = it.indexOf('=') - if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) - }.toMap() - println("name=" + (kv["name"] ?: "missing")) - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.Created, create.status) - - val runWithoutArg = client.get("/run/defaults") { bearerRoot() } - assertEquals(HttpStatusCode.OK, runWithoutArg.status) - assertTrue(runWithoutArg.bodyAsText().contains("name=world")) - - val runWithArg = client.get("/run/defaults?name=alice") { bearerRoot() } - assertEquals(HttpStatusCode.OK, runWithArg.status) - assertTrue(runWithArg.bodyAsText().contains("name=alice")) - } - - @Test - fun lateinitArgsDeclarationIsSupported() = withApp { _ -> - val create = client.post("/scripts/lateinit-args") { - bearerRoot() - setBody( - """ - // @desc: lateinit args - // @param: name | required=false | default=world | desc=Name fallback - lateinit var args: Array - val kv = args.mapNotNull { - val i = it.indexOf('=') - if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) - }.toMap() - println("name=" + (kv["name"] ?: "missing")) - """.trimIndent() - ) - } - assertEquals(HttpStatusCode.Created, create.status) - - val run = client.get("/run/lateinit-args") { bearerRoot() } - assertEquals(HttpStatusCode.OK, run.status) - assertTrue(run.bodyAsText().contains("name=world")) - } - - private fun withApp(testBlock: suspend io.ktor.server.testing.ApplicationTestBuilder.(java.nio.file.Path) -> Unit) { - val scriptsDir = createTempDirectory("webhost-api-test-") - tempDirs.add(scriptsDir) - - testApplication { - val security = createHostSecurity(scriptsDir.toFile(), ROOT_TOKEN) - application { - webModule(scriptsDir.toFile(), security, Semaphore(4)) - } - testBlock(scriptsDir) - } - } - - private fun io.ktor.client.request.HttpRequestBuilder.bearer(token: String) { - headers.append(HttpHeaders.Authorization, "Bearer $token") - } - - private fun io.ktor.client.request.HttpRequestBuilder.bearerRoot() { - bearer(ROOT_TOKEN) - } - - private fun extractJsonField(json: String, field: String): String? { - val regex = Regex("\"" + Regex.escape(field) + "\":\"([^\"]*)\"") - return regex.find(json)?.groupValues?.getOrNull(1) - } - - companion object { - private const val ROOT_TOKEN = "root-test-token" - } -} diff --git a/src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt b/src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt new file mode 100644 index 0000000..d1b9b44 --- /dev/null +++ b/src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt @@ -0,0 +1,51 @@ +package work.slhaf.hub + +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.http.HttpHeaders +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import kotlinx.coroutines.sync.Semaphore +import kotlin.io.path.createTempDirectory +import kotlin.test.AfterTest + +abstract class WebHostTestSupport { + private val tempDirs = mutableListOf() + + @AfterTest + fun cleanup() { + tempDirs.forEach { path -> + runCatching { path.toFile().deleteRecursively() } + } + tempDirs.clear() + } + + protected fun withApp(testBlock: suspend ApplicationTestBuilder.(java.nio.file.Path) -> Unit) { + val scriptsDir = createTempDirectory("webhost-api-test-") + tempDirs.add(scriptsDir) + + testApplication { + val security = createHostSecurity(scriptsDir.toFile(), ROOT_TOKEN) + application { + webModule(scriptsDir.toFile(), security, Semaphore(4)) + } + testBlock(scriptsDir) + } + } + + protected fun HttpRequestBuilder.bearer(token: String) { + headers.append(HttpHeaders.Authorization, "Bearer $token") + } + + protected fun HttpRequestBuilder.bearerRoot() { + bearer(ROOT_TOKEN) + } + + protected fun extractJsonField(json: String, field: String): String? { + val regex = Regex("\\\"" + Regex.escape(field) + "\\\":\\\"([^\\\"]*)\\\"") + return regex.find(json)?.groupValues?.getOrNull(1) + } + + companion object { + private const val ROOT_TOKEN = "root-test-token" + } +} diff --git a/src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt b/src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt new file mode 100644 index 0000000..654c526 --- /dev/null +++ b/src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt @@ -0,0 +1,123 @@ +package work.slhaf.hub + +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class WebRunApiTest : WebHostTestSupport() { + @Test + fun runTimeoutReturnsRequestTimeout() = withApp { _ -> + val create = client.post("/scripts/slow") { + bearerRoot() + setBody( + """ + // @desc: slow script + // @timeout: 1ms + val args: Array = emptyArray() + Thread.sleep(100) + println("done") + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.Created, create.status) + + val run = client.get("/run/slow") { bearerRoot() } + assertEquals(HttpStatusCode.RequestTimeout, run.status) + assertTrue(run.bodyAsText().contains("timed out")) + } + + @Test + fun runErrorResponseIncludesParamsAndRequiredCheck() = withApp { _ -> + val create = client.post("/scripts/runner") { + bearerRoot() + setBody( + """ + // @desc: run test + // @param: must | required=true | default=world | desc=Must be provided explicitly + // @param: boom | required=false | default=false | desc=Trigger runtime failure + val args: Array = emptyArray() + val kv = args.mapNotNull { + val i = it.indexOf('=') + if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) + }.toMap() + if ((kv["boom"] ?: "false").equals("true", ignoreCase = true)) { + error("boom") + } + println("must=" + (kv["must"] ?: "none")) + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.Created, create.status) + + val missingRequired = client.get("/run/runner") { bearerRoot() } + assertEquals(HttpStatusCode.BadRequest, missingRequired.status) + val missingBody = missingRequired.bodyAsText() + assertTrue(missingBody.contains("missing required params: must")) + assertTrue(missingBody.contains("params:")) + assertTrue(missingBody.contains("must (required=true")) + + val runtimeError = client.get("/run/runner?must=ok&boom=true") { bearerRoot() } + assertEquals(HttpStatusCode.InternalServerError, runtimeError.status) + val runtimeErrorBody = runtimeError.bodyAsText() + assertTrue(runtimeErrorBody.contains("script execution failed")) + assertTrue(runtimeErrorBody.contains("params:")) + assertTrue(runtimeErrorBody.contains("boom (required=false")) + } + + @Test + fun defaultParamValueIsInjectedIntoHostArgs() = withApp { _ -> + val create = client.post("/scripts/defaults") { + bearerRoot() + setBody( + """ + // @desc: default args test + // @param: name | required=false | default=world | desc=Name fallback + val args: Array = emptyArray() + val kv = args.mapNotNull { + val i = it.indexOf('=') + if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) + }.toMap() + println("name=" + (kv["name"] ?: "missing")) + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.Created, create.status) + + val runWithoutArg = client.get("/run/defaults") { bearerRoot() } + assertEquals(HttpStatusCode.OK, runWithoutArg.status) + assertTrue(runWithoutArg.bodyAsText().contains("name=world")) + + val runWithArg = client.get("/run/defaults?name=alice") { bearerRoot() } + assertEquals(HttpStatusCode.OK, runWithArg.status) + assertTrue(runWithArg.bodyAsText().contains("name=alice")) + } + + @Test + fun lateinitArgsDeclarationIsSupported() = withApp { _ -> + val create = client.post("/scripts/lateinit-args") { + bearerRoot() + setBody( + """ + // @desc: lateinit args + // @param: name | required=false | default=world | desc=Name fallback + lateinit var args: Array + val kv = args.mapNotNull { + val i = it.indexOf('=') + if (i <= 0) null else it.substring(0, i) to it.substring(i + 1) + }.toMap() + println("name=" + (kv["name"] ?: "missing")) + """.trimIndent() + ) + } + assertEquals(HttpStatusCode.Created, create.status) + + val run = client.get("/run/lateinit-args") { bearerRoot() } + assertEquals(HttpStatusCode.OK, run.status) + assertTrue(run.bodyAsText().contains("name=world")) + } +} diff --git a/src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt b/src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt new file mode 100644 index 0000000..ac1723c --- /dev/null +++ b/src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt @@ -0,0 +1,98 @@ +package work.slhaf.hub + +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class WebSubTokenApiTest : WebHostTestSupport() { + @Test + fun subTokenAccessControlAndFiltering() = withApp { scriptsDir -> + scriptsDir.resolve("allowed.hub.kts").writeText( + """ + // @desc: allowed script + val args: Array = emptyArray() + println("allowed") + """.trimIndent() + ) + scriptsDir.resolve("blocked.hub.kts").writeText( + """ + // @desc: blocked script + val args: Array = emptyArray() + println("blocked") + """.trimIndent() + ) + + val createSub = client.post("/subtokens/demo-sub") { + bearerRoot() + setBody("allowed") + } + assertEquals(HttpStatusCode.Created, createSub.status) + val token = extractJsonField(createSub.bodyAsText(), "token") + assertNotNull(token) + + val type = client.get("/type") { bearer(token) } + assertEquals(HttpStatusCode.OK, type.status) + val typeText = type.bodyAsText() + assertTrue(typeText.contains("\"tokenType\":\"sub\"")) + assertTrue(typeText.contains("\"subTokenName\":\"demo-sub\"")) + + val scripts = client.get("/scripts") { bearer(token) } + assertEquals(HttpStatusCode.OK, scripts.status) + val scriptList = scripts.bodyAsText() + assertTrue(scriptList.contains("allowed")) + assertFalse(scriptList.contains("blocked")) + + val metaAllowed = client.get("/meta/allowed") { bearer(token) } + assertEquals(HttpStatusCode.OK, metaAllowed.status) + + val metaBlocked = client.get("/meta/blocked") { bearer(token) } + assertEquals(HttpStatusCode.Forbidden, metaBlocked.status) + + val runAllowed = client.get("/run/allowed") { bearer(token) } + assertEquals(HttpStatusCode.OK, runAllowed.status) + + val runBlocked = client.get("/run/blocked") { bearer(token) } + assertEquals(HttpStatusCode.Forbidden, runBlocked.status) + + val createScript = client.post("/scripts/not-allowed") { + bearer(token) + setBody("val args: Array = emptyArray()\nprintln(\"x\")") + } + assertEquals(HttpStatusCode.Forbidden, createScript.status) + + val listSubTokens = client.get("/subtokens") { bearer(token) } + assertEquals(HttpStatusCode.Forbidden, listSubTokens.status) + + val typeByPath = client.get("/u/demo-sub@$token/type") + assertEquals(HttpStatusCode.OK, typeByPath.status) + assertTrue(typeByPath.bodyAsText().contains("\"tokenType\":\"sub\"")) + + val scriptsByPath = client.get("/u/demo-sub@$token/scripts") + assertEquals(HttpStatusCode.OK, scriptsByPath.status) + assertTrue(scriptsByPath.bodyAsText().contains("allowed")) + assertFalse(scriptsByPath.bodyAsText().contains("blocked")) + + val metaByPathAllowed = client.get("/u/demo-sub@$token/meta/allowed") + assertEquals(HttpStatusCode.OK, metaByPathAllowed.status) + + val metaByPathBlocked = client.get("/u/demo-sub@$token/meta/blocked") + assertEquals(HttpStatusCode.Forbidden, metaByPathBlocked.status) + + val runByPathAllowed = client.get("/u/demo-sub@$token/run/allowed") + assertEquals(HttpStatusCode.OK, runByPathAllowed.status) + + val runByPathBlocked = client.get("/u/demo-sub@$token/run/blocked") + assertEquals(HttpStatusCode.Forbidden, runByPathBlocked.status) + + val invalidPathAuth = client.get("/u/demo-sub@invalid-token/scripts") + assertEquals(HttpStatusCode.Unauthorized, invalidPathAuth.status) + } +}