refactor(test): split web host api tests by domain
This commit is contained in:
106
src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt
Normal file
106
src/test/kotlin/work/slhaf/hub/WebAuthAndScriptApiTest.kt
Normal file
@@ -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<String> = 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<String> = 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<String> = 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"))
|
||||
}
|
||||
}
|
||||
@@ -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<java.nio.file.Path>()
|
||||
|
||||
@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<String> = 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<String> = 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<String> = emptyArray()
|
||||
println("allowed")
|
||||
""".trimIndent()
|
||||
)
|
||||
scriptsDir.resolve("blocked.hub.kts").writeText(
|
||||
"""
|
||||
// @desc: blocked script
|
||||
val args: Array<String> = 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<String> = 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<String> = 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<String> = 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<String> = 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<String> = 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<String>
|
||||
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"
|
||||
}
|
||||
}
|
||||
51
src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt
Normal file
51
src/test/kotlin/work/slhaf/hub/WebHostTestSupport.kt
Normal file
@@ -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<java.nio.file.Path>()
|
||||
|
||||
@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"
|
||||
}
|
||||
}
|
||||
123
src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt
Normal file
123
src/test/kotlin/work/slhaf/hub/WebRunApiTest.kt
Normal file
@@ -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<String> = 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<String> = 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<String> = 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<String>
|
||||
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"))
|
||||
}
|
||||
}
|
||||
98
src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt
Normal file
98
src/test/kotlin/work/slhaf/hub/WebSubTokenApiTest.kt
Normal file
@@ -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<String> = emptyArray()
|
||||
println("allowed")
|
||||
""".trimIndent()
|
||||
)
|
||||
scriptsDir.resolve("blocked.hub.kts").writeText(
|
||||
"""
|
||||
// @desc: blocked script
|
||||
val args: Array<String> = 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<String> = 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user