Skip to content

Commit 81209e4

Browse files
committed
grpc: Add missing tests
1 parent e9d54ab commit 81209e4

File tree

6 files changed

+162
-97
lines changed

6 files changed

+162
-97
lines changed

grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/credentials.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ package kotlinx.rpc.grpc.client
2525
* @see GrpcInsecureClientCredentials
2626
* @see GrpcCallCredentials
2727
*/
28-
public sealed class GrpcClientCredentials
28+
@OptIn(ExperimentalSubclassOptIn::class)
29+
@SubclassOptInRequired
30+
public interface GrpcClientCredentials
2931

3032
/**
3133
* Combines client credentials with call credentials.
@@ -91,7 +93,7 @@ public fun GrpcClientCredentials.combine(other: GrpcCallCredentials): GrpcClient
9193
*
9294
* @see GrpcTlsClientCredentials
9395
*/
94-
public class GrpcInsecureClientCredentials : GrpcClientCredentials()
96+
public class GrpcInsecureClientCredentials : GrpcClientCredentials
9597

9698
/**
9799
* TLS/SSL credentials for secure transport.
@@ -139,7 +141,7 @@ public class GrpcInsecureClientCredentials : GrpcClientCredentials()
139141
* @see GrpcTlsClientCredentialsBuilder
140142
* @see GrpcInsecureClientCredentials
141143
*/
142-
public class GrpcTlsClientCredentials(internal val configure: GrpcTlsClientCredentialsBuilder.() -> Unit = {}) : GrpcClientCredentials()
144+
public class GrpcTlsClientCredentials(internal val configure: GrpcTlsClientCredentialsBuilder.() -> Unit = {}) : GrpcClientCredentials
143145

144146
/**
145147
* Builder for configuring [GrpcTlsClientCredentials].
@@ -231,7 +233,7 @@ internal val GrpcClientCredentials.realCallCredentials
231233
internal class GrpcCombinedClientCredentials private constructor(
232234
internal val clientCredentials: GrpcClientCredentials,
233235
internal val callCredentials: GrpcCallCredentials,
234-
): GrpcClientCredentials() {
236+
): GrpcClientCredentials {
235237

236238
companion object {
237239
internal fun create(clientCredentials: GrpcClientCredentials, callCredentials: GrpcCallCredentials): GrpcCombinedClientCredentials {

grpc/grpc-client/src/jvmMain/kotlin/kotlinx/rpc/grpc/client/credentials.jvm.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.grpc.TlsChannelCredentials
1212
import kotlinx.coroutines.CoroutineScope
1313
import kotlinx.coroutines.launch
1414
import kotlinx.rpc.grpc.Status
15+
import kotlinx.rpc.grpc.internal.internalError
1516
import java.util.concurrent.Executor
1617
import kotlin.coroutines.CoroutineContext
1718
import kotlin.coroutines.cancellation.CancellationException
@@ -21,6 +22,7 @@ internal fun GrpcClientCredentials.toJvm(): ChannelCredentials {
2122
is GrpcCombinedClientCredentials -> clientCredentials.toJvm()
2223
is GrpcInsecureClientCredentials -> InsecureChannelCredentials.create()
2324
is GrpcTlsClientCredentials -> JvmTlsCLientCredentialBuilder().apply(configure).build()
25+
else -> internalError("Unknown client credentials type: $this")
2426
}
2527
}
2628

grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/credentials.native.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal fun GrpcClientCredentials.createRaw(): CPointer<grpc_channel_credential
2929
clientCredentials.createRaw()
3030
is GrpcInsecureClientCredentials -> grpc_insecure_credentials_create() ?: error("grpc_insecure_credentials_create() returned null")
3131
is GrpcTlsClientCredentials -> NativeTlsClientCredentialsBuilder().apply(configure).build()
32+
else -> internalError("Unknown client credentials type: $this")
3233
}
3334
}
3435

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.grpc.internal
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
@InternalRpcApi
10+
public fun internalError(message: String): Nothing {
11+
error("Unexpected internal error: $message. Please, report the issue here: https://github.com/Kotlin/kotlinx-rpc/issues/new?template=bug_report.md")
12+
}

grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt

Lines changed: 141 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.rpc.grpc.test.proto
66

7+
import kotlinx.coroutines.CompletableDeferred
78
import kotlinx.rpc.RpcServer
89
import kotlinx.rpc.grpc.GrpcMetadata
910
import kotlinx.rpc.grpc.Status
@@ -27,14 +28,27 @@ import kotlinx.rpc.grpc.test.assertGrpcFailure
2728
import kotlinx.rpc.grpc.test.invoke
2829
import kotlinx.rpc.registerService
2930
import kotlinx.rpc.withService
31+
import kotlin.coroutines.cancellation.CancellationException
3032
import kotlin.test.Test
3133
import kotlin.test.assertEquals
34+
import kotlin.test.assertNotNull
35+
import kotlin.time.Duration.Companion.milliseconds
3236

3337
class GrpcCallCredentialsTest : GrpcProtoTest() {
3438
override fun RpcServer.registerServices() {
3539
return registerService<EchoService> { EchoServiceImpl() }
3640
}
3741

42+
private fun assertAuthorizationHeaders(metadata: GrpcMetadata?, vararg expectedTokens: String) {
43+
assertNotNull(metadata, "Metadata should not be null")
44+
val authHeaders = metadata.getAll("authorization")
45+
assertNotNull(authHeaders, "Authorization headers should not be null")
46+
assertEquals(expectedTokens.size, authHeaders.size)
47+
expectedTokens.forEachIndexed { index, token ->
48+
assertEquals(token, authHeaders[index])
49+
}
50+
}
51+
3852
@Test
3953
fun `test simple combined call credentials - should succeed`() {
4054
var grpcMetadata: GrpcMetadata? = null
@@ -49,9 +63,7 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
4963
test = ::unaryCall
5064
)
5165

52-
val authHeaders = grpcMetadata?.getAll("authorization")
53-
assertEquals(1, authHeaders?.size)
54-
assertEquals("Bearer token", authHeaders?.single())
66+
assertAuthorizationHeaders(grpcMetadata, "Bearer token")
5567
}
5668

5769
@Test
@@ -68,10 +80,31 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
6880
},
6981
test = ::unaryCall
7082
)
71-
val authHeaders = grpcMetadata?.getAll("authorization")
72-
assertEquals(2, authHeaders?.size)
73-
assertEquals("Bearer token-1", authHeaders?.first())
74-
assertEquals("Bearer token-2", authHeaders?.get(1))
83+
84+
assertAuthorizationHeaders(grpcMetadata, "Bearer token-1", "Bearer token-2")
85+
}
86+
87+
@Test
88+
fun `test combine three or more call credentials at config and interceptor time - should succeed`() {
89+
var grpcMetadata: GrpcMetadata? = null
90+
val configCallCreds = (NoTLSBearerTokenCredentials("token-1") + NoTLSBearerTokenCredentials("token-2") + NoTLSBearerTokenCredentials("token-3"))
91+
runGrpcTest(
92+
configure = {
93+
credentials = plaintext() + configCallCreds
94+
},
95+
clientInterceptors = clientInterceptor {
96+
callOptions.callCredentials += NoTLSBearerTokenCredentials("token-4")
97+
proceed(it)
98+
},
99+
serverInterceptors = serverInterceptor {
100+
grpcMetadata = requestHeaders
101+
proceed(it)
102+
},
103+
test = ::unaryCall
104+
)
105+
106+
// 4 before 1 as callOption callCredentials are applied before client level ones
107+
assertAuthorizationHeaders(grpcMetadata, "Bearer token-4", "Bearer token-1", "Bearer token-2", "Bearer token-3")
75108
}
76109

77110
@Test
@@ -106,52 +139,27 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
106139
test = ::unaryCall
107140
)
108141

109-
val authHeaders = grpcMetadata?.getAll("authorization")
110-
assertEquals(1, authHeaders?.size)
111-
assertEquals("Bearer token", authHeaders?.single())
142+
assertAuthorizationHeaders(grpcMetadata, "Bearer token")
112143
}
113144

114145
@Test
115146
fun `test throw status exception - should fail with status`() {
116-
val throwingCallCredentials = object : GrpcCallCredentials {
117-
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
118-
throw StatusException(Status(StatusCode.UNIMPLEMENTED, "This is my custom exception"))
119-
}
120-
121-
override val requiresTransportSecurity: Boolean
122-
get() = false
123-
}
124-
125147
assertGrpcFailure(StatusCode.UNAVAILABLE, "This is my custom exception") {
126148
runGrpcTest(
127149
configure = {
128-
credentials = plaintext() + throwingCallCredentials
129-
},
130-
serverInterceptors = serverInterceptor {
131-
proceed(it)
150+
credentials = plaintext() + ThrowingCallCredentials()
132151
},
133152
test = ::unaryCall
134153
)
135154
}
136155
}
137156

138-
139157
@Test
140158
fun `test throw exception - should fail`() {
141-
val throwingCallCredentials = object : GrpcCallCredentials {
142-
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
143-
throw IllegalStateException("This is my custom exception")
144-
}
145-
override val requiresTransportSecurity: Boolean
146-
get() = false
147-
}
148159
assertGrpcFailure(StatusCode.UNAVAILABLE, "This is my custom exception") {
149160
runGrpcTest(
150161
configure = {
151-
credentials = plaintext() + throwingCallCredentials
152-
},
153-
serverInterceptors = serverInterceptor {
154-
proceed(it)
162+
credentials = plaintext() + ThrowingCallCredentials(IllegalStateException("This is my custom exception"))
155163
},
156164
test = ::unaryCall
157165
)
@@ -172,9 +180,8 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
172180
},
173181
test = ::unaryCall
174182
)
175-
val authHeaders = grpcMetadata?.getAll("authorization")
176-
assertEquals(1, authHeaders?.size)
177-
assertEquals("Bearer token", authHeaders?.single())
183+
184+
assertAuthorizationHeaders(grpcMetadata, "Bearer token")
178185
}
179186

180187
@Test
@@ -236,51 +243,87 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
236243
assertEquals("test.example.com", capturedAuthority)
237244
}
238245

239-
// @Test
240-
// fun `test long running call credentials - should succeed`() {
241-
// var grpcMetadata: GrpcMetadata? = null
242-
// class SlowCredentials(
243-
// val token: String
244-
// ) : GrpcCallCredentials {
245-
// override suspend fun Context.getRequestMetadata(): GrpcMetadata {
246-
// delay(1000)
247-
// return buildGrpcMetadata {
248-
// append(token, token)
249-
// }
250-
// }
251-
//
252-
// override val requiresTransportSecurity: Boolean
253-
// get() = false
254-
// }
255-
//
256-
// runGrpcTest(
257-
// configure = {
258-
// credentials = plaintext() + SlowCredentials("token-1")
259-
// },
260-
// clientInterceptors = clientInterceptor {
261-
// callOptions.callCredentials += SlowCredentials("token-2")
262-
// proceed(it)
263-
// },
264-
// serverInterceptors = serverInterceptor {
265-
// grpcMetadata = requestHeaders
266-
// proceed(it)
267-
// },
268-
// test = {
269-
// coroutineScope {
270-
// launch { unaryCall(it) }
271-
// delay(200)
272-
// cancel("Midcanceling")
273-
// }
274-
// }
275-
// )
276-
//
277-
// val authHeaders = grpcMetadata?.getAll("token-1")
278-
// assertEquals(1, authHeaders?.size)
279-
// assertEquals("token-1", authHeaders?.single())
280-
// val authHeaders2 = grpcMetadata?.getAll("token-2")
281-
// assertEquals(1, authHeaders2?.size)
282-
// assertEquals("token-2", authHeaders2?.single())
283-
// }
246+
@Test
247+
fun `test call credentials cancellation because of timeout - should fail`() {
248+
var callCredsCancelled = false
249+
val slowCredentials = object : GrpcCallCredentials {
250+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
251+
try {
252+
// block indefinitely to simulate a slow call credential.
253+
// this works even in a runTest coroutine dispatcher
254+
CompletableDeferred<Unit>().await()
255+
return buildGrpcMetadata {
256+
append("Authentication", "Bearer token")
257+
}
258+
} catch (err: CancellationException) {
259+
callCredsCancelled = true
260+
throw err
261+
}
262+
}
263+
264+
override val requiresTransportSecurity: Boolean
265+
get() = false
266+
}
267+
assertGrpcFailure(StatusCode.DEADLINE_EXCEEDED) {
268+
runGrpcTest(
269+
configure = {
270+
credentials = plaintext() + slowCredentials
271+
},
272+
clientInterceptors = clientInterceptor {
273+
callOptions.timeout = 100.milliseconds
274+
proceed(it)
275+
},
276+
test = ::unaryCall
277+
)
278+
}
279+
280+
// assert that the getRequestMetadata suspend method was cancelled
281+
assertEquals(true, callCredsCancelled)
282+
}
283+
284+
@Test
285+
fun `test call credentials should be called even if second fails - should fail`() {
286+
var calledCredentialHandler = false
287+
val someCredentials = object : PlaintextCallCredentials() {
288+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
289+
calledCredentialHandler = true
290+
return buildGrpcMetadata { }
291+
}
292+
}
293+
assertGrpcFailure(StatusCode.UNAVAILABLE) {
294+
runGrpcTest(
295+
configure = {
296+
credentials = plaintext() + someCredentials + ThrowingCallCredentials()
297+
},
298+
test = ::unaryCall
299+
)
300+
}
301+
302+
// assert that the getRequestMetadata suspend method was cancelled
303+
assertEquals(true, calledCredentialHandler)
304+
}
305+
306+
@Test
307+
fun `test call credentials should not be called if previous one fails - should fail`() {
308+
var calledCredentialHandler = false
309+
val someCredentials = object : PlaintextCallCredentials() {
310+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
311+
calledCredentialHandler = true
312+
return buildGrpcMetadata { }
313+
}
314+
}
315+
assertGrpcFailure(StatusCode.UNAVAILABLE) {
316+
runGrpcTest(
317+
configure = {
318+
credentials = plaintext() + ThrowingCallCredentials() + someCredentials
319+
},
320+
test = ::unaryCall
321+
)
322+
}
323+
324+
// assert that the getRequestMetadata suspend method was cancelled
325+
assertEquals(false, calledCredentialHandler)
326+
}
284327
}
285328

286329
private suspend fun unaryCall(grpcClient: GrpcClient) {
@@ -289,21 +332,31 @@ private suspend fun unaryCall(grpcClient: GrpcClient) {
289332
assertEquals("Echo", response.message)
290333
}
291334

335+
abstract class PlaintextCallCredentials : GrpcCallCredentials {
336+
override val requiresTransportSecurity: Boolean
337+
get() = false
338+
}
339+
340+
class ThrowingCallCredentials(
341+
private val exception: Throwable = StatusException(Status(StatusCode.UNIMPLEMENTED, "This is my custom exception"))
342+
) : PlaintextCallCredentials() {
343+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
344+
throw exception
345+
}
346+
}
347+
292348
class NoTLSBearerTokenCredentials(
293349
val token: String = "token"
294-
): GrpcCallCredentials {
350+
) : PlaintextCallCredentials() {
295351
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
296352
return buildGrpcMetadata {
297353
// potentially fetching the token from a secure storage
298354
append("Authorization", "Bearer $token")
299355
}
300356
}
301-
302-
override val requiresTransportSecurity: Boolean
303-
get() = false
304357
}
305358

306-
class TlsBearerTokenCredentials: GrpcCallCredentials {
359+
class TlsBearerTokenCredentials : GrpcCallCredentials {
307360
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
308361
return buildGrpcMetadata {
309362
append("Authorization", "Bearer token")

0 commit comments

Comments
 (0)