Skip to content

Commit 3fe5b68

Browse files
authored
Merge pull request #759 from VladGaluska/KTLN-793
KTLN-793 Quick Look at Ktor Client
2 parents f81e29d + 79ac69c commit 3fe5b68

File tree

6 files changed

+304
-1
lines changed

6 files changed

+304
-1
lines changed

kotlin-ktor/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ dependencies {
1414
implementation("com.expediagroup", "graphql-kotlin-ktor-server", graphQLKotlinVersion)
1515
implementation("com.expediagroup", "graphql-kotlin-client-jackson", graphQLKotlinVersion)
1616

17+
implementation("io.ktor", "ktor-client-core", ktorVersion)
1718
implementation("io.ktor", "ktor-server-core", ktorVersion)
1819
implementation("io.ktor", "ktor-server-netty", ktorVersion)
1920
implementation("io.ktor", "ktor-server-websockets", ktorVersion)
21+
implementation("io.ktor", "ktor-client-auth", ktorVersion)
22+
implementation("io.ktor", "ktor-client-websockets", ktorVersion)
23+
implementation("io.ktor", "ktor-serialization-jackson", ktorVersion)
24+
implementation("io.ktor", "ktor-client-content-negotiation", ktorVersion)
2025

26+
27+
testImplementation("io.ktor", "ktor-client-mock", ktorVersion)
2128
testImplementation("io.ktor", "ktor-server-tests", ktorVersion)
22-
testImplementation("io.ktor", "ktor-client-content-negotiation", ktorVersion)
2329
testImplementation("io.ktor", "ktor-serialization-kotlinx-json", ktorVersion)
2430
testImplementation("org.jetbrains.kotlin", "kotlin-test-junit", "1.9.10")
2531
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.baeldung.client
2+
3+
const val CARS = """
4+
[
5+
{
6+
"id": 0,
7+
"name": "Car 1",
8+
"driver": 0
9+
},
10+
{
11+
"id": 1,
12+
"name": "Car 2",
13+
"driver": 1
14+
}
15+
]
16+
"""
17+
18+
val DRIVERS = listOf(
19+
"""
20+
{
21+
"id": 0,
22+
"name": "John"
23+
}
24+
""",
25+
"""
26+
{
27+
"id": 1,
28+
"name": "Mike"
29+
}
30+
"""
31+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.baeldung.client
2+
3+
import io.ktor.server.application.*
4+
import io.ktor.server.engine.*
5+
import io.ktor.server.netty.*
6+
import io.ktor.server.routing.*
7+
import io.ktor.server.websocket.*
8+
import io.ktor.websocket.*
9+
10+
fun main() {
11+
embeddedServer(Netty, port = 8080, module = Application::websocketsModule).start(wait = true)
12+
}
13+
14+
fun Application.websocketsModule() {
15+
install(WebSockets)
16+
routing {
17+
webSocket("/messages") {
18+
send("Hi!")
19+
for (frame in incoming) {
20+
frame as? Frame.Text ?: continue
21+
val receivedText = frame.readText()
22+
if (receivedText.equals("Bye!", ignoreCase = true)) {
23+
close(CloseReason(CloseReason.Codes.NORMAL, "Client said bye!"))
24+
}
25+
}
26+
}
27+
webSocket("/driver") {
28+
send("Which driver would you like to see?")
29+
for (frame in incoming) {
30+
frame as? Frame.Text ?: continue
31+
val receivedText = frame.readText()
32+
if (receivedText.equals("Bye!", ignoreCase = true)) {
33+
close(CloseReason(CloseReason.Codes.NORMAL, "Client said bye!"))
34+
}
35+
val receivedDriverId = receivedText.toIntOrNull() ?: continue
36+
send(DRIVERS[receivedDriverId])
37+
}
38+
}
39+
}
40+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.baeldung.client
2+
3+
class Driver(val id: Int, val name: String)
4+
5+
class Car(val id: Int, val name: String, val driver: Int)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.baeldung.client
2+
3+
import io.ktor.client.engine.mock.*
4+
import io.ktor.http.*
5+
import io.ktor.utils.io.*
6+
7+
val mockEngine = MockEngine {
8+
request -> when {
9+
request.headers["Authorization"] != "Basic YmFlbGR1bmc6YmFlbGR1bmc=" -> respond(
10+
content = "Wrong credentials!",
11+
status = HttpStatusCode.Unauthorized
12+
)
13+
request.url.fullPath == "/cars" && request.method == HttpMethod.Get -> respond(
14+
content = ByteReadChannel(CARS),
15+
status = HttpStatusCode.OK,
16+
headers = headersOf(HttpHeaders.ContentType, "application/json")
17+
)
18+
request.url.fullPath.startsWith("/driver") && request.method == HttpMethod.Get -> {
19+
val driverId = request.url.parameters["id"]?.toIntOrNull() ?: 0
20+
respond(
21+
content = ByteReadChannel(DRIVERS[driverId]),
22+
status = HttpStatusCode.OK,
23+
headers = headersOf(HttpHeaders.ContentType, "application/json")
24+
)
25+
}
26+
request.url.fullPath == "/car" && request.method == HttpMethod.Put -> respond(
27+
content = ByteReadChannel("Created!"),
28+
status = HttpStatusCode.OK,
29+
headers = headersOf(HttpHeaders.ContentType, "application/json")
30+
)
31+
request.url.fullPath == "/driver" && request.method == HttpMethod.Put -> respond(
32+
content = ByteReadChannel("Created!"),
33+
status = HttpStatusCode.OK,
34+
headers = headersOf(HttpHeaders.ContentType, "application/json")
35+
)
36+
else -> respond(
37+
content = ByteReadChannel("Unknown Request!"),
38+
status = HttpStatusCode.NotFound
39+
)
40+
}
41+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.baeldung.client
2+
3+
import io.ktor.client.*
4+
import io.ktor.client.call.*
5+
import io.ktor.client.plugins.*
6+
import io.ktor.client.plugins.auth.*
7+
import io.ktor.client.plugins.auth.providers.*
8+
import io.ktor.client.plugins.contentnegotiation.*
9+
import io.ktor.client.plugins.websocket.*
10+
import io.ktor.client.request.*
11+
import io.ktor.client.statement.*
12+
import io.ktor.http.*
13+
import io.ktor.serialization.jackson.*
14+
import io.ktor.server.engine.*
15+
import io.ktor.server.netty.*
16+
import io.ktor.websocket.*
17+
import kotlinx.coroutines.runBlocking
18+
import org.junit.AfterClass
19+
import kotlin.test.*
20+
21+
class RequestsUnitTest {
22+
23+
companion object {
24+
25+
private val client = HttpClient(mockEngine) {
26+
expectSuccess = true
27+
install(ContentNegotiation) {
28+
jackson()
29+
}
30+
install(Auth) {
31+
basic {
32+
credentials {
33+
BasicAuthCredentials(username = "baeldung", password = "baeldung")
34+
}
35+
sendWithoutRequest { _ -> true }
36+
}
37+
}
38+
}
39+
40+
// Different to the regular client, as that one is using a mock engine
41+
private val websocketsClient = HttpClient {
42+
install(WebSockets) {
43+
contentConverter = JacksonWebsocketContentConverter()
44+
}
45+
}
46+
47+
private val noAuthClient = HttpClient(mockEngine) {
48+
install(ContentNegotiation) {
49+
jackson()
50+
}
51+
}
52+
53+
@JvmStatic
54+
@AfterClass
55+
fun afterTests() {
56+
client.close()
57+
}
58+
}
59+
60+
@Test
61+
fun `when fetching cars then should get two cars`() {
62+
runBlocking {
63+
with(client.get("/cars")) {
64+
assertEquals(HttpStatusCode.OK, status)
65+
val cars: List<Car> = body()
66+
assertEquals(2, cars.size)
67+
assertTrue { cars.any { car -> car.name == "Car 1" } }
68+
}
69+
}
70+
}
71+
72+
@Test
73+
fun `when fetching driver zero then should get driver`() {
74+
runBlocking {
75+
with(client.get("/driver?id=0")) {
76+
assertEquals(HttpStatusCode.OK, status)
77+
val driver: Driver = body()
78+
assertEquals(0, driver.id)
79+
}
80+
}
81+
}
82+
83+
@Test
84+
fun `when creating car then should succeed`() {
85+
runBlocking {
86+
with(client.put("/car") {
87+
contentType(ContentType.Application.Json)
88+
setBody(Car(id = 2, name = "Car 3", driver = 1))
89+
}) {
90+
assertEquals(HttpStatusCode.OK, status)
91+
assertEquals("Created!", bodyAsText())
92+
}
93+
}
94+
}
95+
96+
@Test
97+
fun `when requesting unknown endpoint then should throw exception`() {
98+
runBlocking {
99+
try {
100+
client.get("/this-does-not-exist")
101+
} catch (exception: ClientRequestException) {
102+
return@runBlocking
103+
}
104+
fail("Did not throw an exception!")
105+
}
106+
}
107+
108+
@Test
109+
fun `when creating driver then should succeed`() {
110+
runBlocking {
111+
with(client.put("/driver") {
112+
contentType(ContentType.Application.Json)
113+
setBody(Driver(id = 2, name = "Jack"))
114+
}) {
115+
assertEquals(HttpStatusCode.OK, status)
116+
assertEquals("Created!", bodyAsText())
117+
}
118+
}
119+
}
120+
121+
@Test
122+
fun `when sending websocket messages then should receive them`() {
123+
val server = startEmbeddedServer()
124+
runBlocking {
125+
websocketsClient.webSocket(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/messages") {
126+
var message = incoming.receive() as? Frame.Text
127+
while (message == null) {
128+
message = incoming.receive() as? Frame.Text
129+
}
130+
assertEquals("Hi!", String(message.data))
131+
send("Bye!")
132+
}
133+
}
134+
server.stop()
135+
}
136+
137+
@Test
138+
fun `when requesting websocket driver then should receive and deserialize`() {
139+
val server = startEmbeddedServer()
140+
runBlocking {
141+
websocketsClient.webSocket(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/driver") {
142+
var message = incoming.receive() as? Frame.Text
143+
while (message == null) {
144+
message = incoming.receive() as? Frame.Text
145+
}
146+
assertEquals("Which driver would you like to see?", String(message.data))
147+
send("0")
148+
val driver = receiveDeserialized<Driver>()
149+
assertEquals(0, driver.id)
150+
send("Bye!")
151+
}
152+
}
153+
server.stop()
154+
}
155+
156+
@Test
157+
fun `when not sending authentication then should not succeed`() {
158+
runBlocking {
159+
with(noAuthClient.get("/cars")) {
160+
assertEquals(HttpStatusCode.Unauthorized, status)
161+
}
162+
}
163+
}
164+
165+
private fun startEmbeddedServer(): NettyApplicationEngine {
166+
val env = applicationEngineEnvironment {
167+
module {
168+
websocketsModule()
169+
}
170+
connector {
171+
host = "0.0.0.0"
172+
port = 8080
173+
}
174+
}
175+
val server = embeddedServer(Netty, env)
176+
server.start(false)
177+
return server
178+
}
179+
180+
}

0 commit comments

Comments
 (0)