mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-11-05 01:26:20 +00:00
Add a couple of tests for HTTP
This commit is contained in:
parent
9cca908bff
commit
12ca8583f4
@ -0,0 +1,135 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap
|
||||
import io.netty.buffer.ByteBufUtil
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.*
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||
import io.netty.handler.codec.http.*
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Runs a small HTTP server to run alongside [TestHttpApi]
|
||||
*/
|
||||
object HttpServer {
|
||||
const val PORT: Int = 8378
|
||||
const val URL: String = "http://127.0.0.1:$PORT"
|
||||
const val WS_URL: String = "ws://127.0.0.1:$PORT/ws"
|
||||
|
||||
fun runServer(run: () -> Unit) {
|
||||
val workerGroup: EventLoopGroup = NioEventLoopGroup(2)
|
||||
try {
|
||||
val ch = ServerBootstrap()
|
||||
.group(workerGroup)
|
||||
.channel(NioServerSocketChannel::class.java)
|
||||
.childHandler(
|
||||
object : ChannelInitializer<SocketChannel>() {
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
val p: ChannelPipeline = ch.pipeline()
|
||||
p.addLast(HttpServerCodec())
|
||||
p.addLast(HttpContentCompressor())
|
||||
p.addLast(HttpObjectAggregator(8192))
|
||||
p.addLast(HttpServerHandler())
|
||||
p.addLast(WebSocketServerCompressionHandler())
|
||||
p.addLast(WebSocketServerProtocolHandler("/ws", null, true))
|
||||
p.addLast(WebSocketFrameHandler())
|
||||
}
|
||||
},
|
||||
).bind(PORT).sync().channel()
|
||||
try {
|
||||
run()
|
||||
} finally {
|
||||
ch.close().sync()
|
||||
}
|
||||
} finally {
|
||||
workerGroup.shutdownGracefully()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A HTTP handler which hosts `/` (a simple static page) and `/ws` (see [WebSocketFrameHandler])
|
||||
*/
|
||||
private class HttpServerHandler : SimpleChannelInboundHandler<FullHttpRequest>() {
|
||||
companion object {
|
||||
private val CONTENT = "Hello, world!".toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
override fun channelReadComplete(ctx: ChannelHandlerContext) {
|
||||
ctx.flush()
|
||||
}
|
||||
|
||||
public override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) {
|
||||
when (request.uri()) {
|
||||
"/", "/index.html" -> handleIndex(ctx, request)
|
||||
"/ws" -> handleWebsocket(ctx, request)
|
||||
else -> sendHttpResponse(ctx, request, DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.NOT_FOUND))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIndex(ctx: ChannelHandlerContext, request: FullHttpRequest) {
|
||||
sendHttpResponse(
|
||||
ctx,
|
||||
request,
|
||||
DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, Unpooled.wrappedBuffer(CONTENT)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleWebsocket(ctx: ChannelHandlerContext, request: FullHttpRequest) {
|
||||
if (!request.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true)) {
|
||||
return sendHttpResponse(ctx, request, DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST))
|
||||
}
|
||||
|
||||
ctx.fireChannelRead(request.retain())
|
||||
}
|
||||
|
||||
private fun sendHttpResponse(ctx: ChannelHandlerContext, request: FullHttpRequest, response: FullHttpResponse) {
|
||||
// Generate an error page if response getStatus code is not OK (200).
|
||||
val responseStatus = response.status()
|
||||
if (responseStatus.code() != 200) {
|
||||
ByteBufUtil.writeUtf8(response.content(), responseStatus.toString())
|
||||
HttpUtil.setContentLength(response, response.content().readableBytes().toLong())
|
||||
}
|
||||
|
||||
// Send the response and close the connection if necessary.
|
||||
val keepAlive = HttpUtil.isKeepAlive(request) && responseStatus.code() == 200
|
||||
HttpUtil.setKeepAlive(response, keepAlive)
|
||||
val future = ctx.writeAndFlush(response)
|
||||
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic WS server which just sends back the original message.
|
||||
*/
|
||||
private class WebSocketFrameHandler : SimpleChannelInboundHandler<WebSocketFrame>() {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, frame: WebSocketFrame) {
|
||||
if (frame is TextWebSocketFrame) {
|
||||
// Send the uppercase string back.
|
||||
val request = frame.text()
|
||||
ctx.channel().writeAndFlush(TextWebSocketFrame(request.uppercase()))
|
||||
} else {
|
||||
throw UnsupportedOperationException("unsupported frame type: ${frame.javaClass.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is HandshakeComplete) {
|
||||
// Channel upgrade to websocket, remove WebSocketIndexPageHandler.
|
||||
ctx.pipeline().remove(HttpServerHandler::class.java)
|
||||
} else {
|
||||
super.userEventTriggered(ctx, evt)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http
|
||||
|
||||
import dan200.computercraft.api.lua.Coerced
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle
|
||||
import dan200.computercraft.core.apis.http.HttpServer.URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.WS_URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.runServer
|
||||
import dan200.computercraft.core.apis.http.options.Action
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule
|
||||
import dan200.computercraft.core.apis.http.request.HttpResponseHandle
|
||||
import dan200.computercraft.core.apis.http.websocket.WebsocketHandle
|
||||
import dan200.computercraft.test.core.computer.LuaTaskRunner
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
class TestHttpApi {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun before() {
|
||||
CoreConfig.httpRules = listOf(AddressRule.parse("*", OptionalInt.empty(), Action.ALLOW.toPartial()))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
fun after() {
|
||||
CoreConfig.httpRules = Collections.unmodifiableList(
|
||||
listOf(
|
||||
AddressRule.parse("\$private", OptionalInt.empty(), Action.DENY.toPartial()),
|
||||
AddressRule.parse("*", OptionalInt.empty(), Action.ALLOW.toPartial()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Connects to a HTTP server`() {
|
||||
runServer {
|
||||
LuaTaskRunner.runTest {
|
||||
val httpApi = addApi(HTTPAPI(environment))
|
||||
assertThat("http.request succeeded", httpApi.request(ObjectArguments(URL)), array(equalTo(true)))
|
||||
|
||||
val result = pullEvent("http_success")
|
||||
assertThat(result, array(equalTo("http_success"), equalTo(URL), isA(HttpResponseHandle::class.java)))
|
||||
|
||||
val handle = result[2] as HttpResponseHandle
|
||||
val reader = handle.extra.iterator().next() as EncodedReadableHandle
|
||||
assertThat(reader.readAll(), array(equalTo("Hello, world!")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Connects to websocket`() {
|
||||
runServer {
|
||||
LuaTaskRunner.runTest {
|
||||
val httpApi = addApi(HTTPAPI(environment))
|
||||
assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true)))
|
||||
|
||||
val connectEvent = pullEvent()
|
||||
assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
|
||||
|
||||
val websocket = connectEvent[2] as WebsocketHandle
|
||||
websocket.send(Coerced("Hello"), Optional.of(false))
|
||||
|
||||
val message = websocket.receive(Optional.empty()).await()
|
||||
assertThat("Received a return message", message, array(equalTo("HELLO"), equalTo(false)))
|
||||
|
||||
websocket.close()
|
||||
|
||||
val closeEvent = pullEvent("websocket_closed")
|
||||
assertThat(
|
||||
"Websocket was closed",
|
||||
closeEvent,
|
||||
array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package http
|
||||
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
import dan200.computercraft.core.apis.http.options.Action
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule
|
||||
import dan200.computercraft.test.core.computer.LuaTaskRunner
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
@Disabled("Requires some setup locally.")
|
||||
class TestHttpApi {
|
||||
companion object {
|
||||
private const val WS_ADDRESS = "ws://127.0.0.1:8080"
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun before() {
|
||||
CoreConfig.httpRules = listOf(AddressRule.parse("*", OptionalInt.empty(), Action.ALLOW.toPartial()))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
fun after() {
|
||||
CoreConfig.httpRules = Collections.unmodifiableList(
|
||||
listOf(
|
||||
AddressRule.parse("\$private", OptionalInt.empty(), Action.DENY.toPartial()),
|
||||
AddressRule.parse("*", OptionalInt.empty(), Action.ALLOW.toPartial()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Connects to websocket`() {
|
||||
LuaTaskRunner.runTest {
|
||||
val httpApi = addApi(HTTPAPI(environment))
|
||||
|
||||
val result = httpApi.websocket(ObjectArguments(WS_ADDRESS))
|
||||
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
|
||||
|
||||
val event = pullEvent()
|
||||
assertEquals("websocket_success", event[0]) {
|
||||
"Websocket failed to connect: ${event.contentToString()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ import dan200.computercraft.api.lua.LuaException
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment
|
||||
import dan200.computercraft.test.core.apis.BasicApiEnvironment
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.time.Duration
|
||||
@ -21,11 +20,7 @@ class LuaTaskRunner : AbstractLuaTaskContext() {
|
||||
private val apis = mutableListOf<ILuaAPI>()
|
||||
|
||||
val environment: IAPIEnvironment = object : BasicApiEnvironment(BasicEnvironment()) {
|
||||
override fun queueEvent(event: String?, vararg args: Any?) {
|
||||
if (eventStream.trySend(Event(event, args)).isFailure) {
|
||||
throw IllegalStateException("Queue is full")
|
||||
}
|
||||
}
|
||||
override fun queueEvent(event: String?, vararg args: Any?) = this@LuaTaskRunner.queueEvent(event, args)
|
||||
|
||||
override fun shutdown() {
|
||||
super.shutdown()
|
||||
@ -46,21 +41,13 @@ class LuaTaskRunner : AbstractLuaTaskContext() {
|
||||
environment.shutdown()
|
||||
}
|
||||
|
||||
private suspend fun run() {
|
||||
for (event in eventStream) {
|
||||
queueEvent(event.name, event.args)
|
||||
}
|
||||
}
|
||||
|
||||
private class Event(val name: String?, val args: Array<out Any?>)
|
||||
|
||||
companion object {
|
||||
fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) {
|
||||
runBlocking {
|
||||
withTimeout(timeout) {
|
||||
val runner = LuaTaskRunner()
|
||||
launch { runner.run() }
|
||||
runner.use { fn(runner) }
|
||||
LuaTaskRunner().use { fn(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user