Skip to content

Commit ee6369c

Browse files
committed
chore: expand testing a little bit more
Just pushing us over 90% coverage.
1 parent 4fee849 commit ee6369c

3 files changed

Lines changed: 124 additions & 2 deletions

File tree

protocol/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ dependencies {
7878
api(libs.kaitai.runtime)
7979
implementation(kotlin("stdlib"))
8080
testImplementation(kotlin("test"))
81+
testImplementation(libs.junit.jupiter.api)
82+
testRuntimeOnly(libs.junit.jupiter.engine)
8183
kaitaiCompiler(libs.kaitai.compiler)
8284
}
8385

sshlib/src/test/kotlin/org/connectbot/sshlib/client/FakeSshServer.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,14 @@ class FakeSshServer(
109109
private val receivedChannelOpenConfirmations = Channel<SshMsgChannelOpenConfirmation>(Channel.UNLIMITED)
110110
private val receivedChannelOpenFailures = Channel<SshMsgChannelOpenFailure>(Channel.UNLIMITED)
111111

112-
fun start() {
113-
scope.launch(coroutineContext) { serve() }
112+
fun start(ignoreTransportErrors: Boolean = false) {
113+
scope.launch(coroutineContext) {
114+
if (ignoreTransportErrors) {
115+
runCatching { serve() }
116+
} else {
117+
serve()
118+
}
119+
}
114120
}
115121

116122
/**
@@ -741,6 +747,7 @@ class FakeSshServer(
741747
}
742748

743749
suspend fun sendChannelWindowAdjust(recipientChannel: Int, bytesToAdd: Long) {
750+
require(bytesToAdd in 0..0xFFFF_FFFFL) { "bytesToAdd must fit SSH uint32" }
744751
val payload = ByteBuffer.allocate(8)
745752
.putInt(recipientChannel)
746753
.putInt(bytesToAdd.toInt())

sshlib/src/test/kotlin/org/connectbot/sshlib/client/SshConnectionFlowTest.kt

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,50 @@ class SshConnectionFlowTest {
6060
override suspend fun verify(key: PublicKey): Boolean = true
6161
}
6262

63+
@Test
64+
fun `connect returns host key rejected when verifier rejects server key`() = runTest {
65+
val dispatcher = StandardTestDispatcher(testScheduler)
66+
val (clientTransport, serverTransport) = PipedTransport.create()
67+
val server = FakeSshServer(serverTransport, backgroundScope, dispatcher)
68+
server.start(ignoreTransportErrors = true)
69+
70+
val rejectingVerifier = object : HostKeyVerifier {
71+
override suspend fun verify(key: PublicKey): Boolean = false
72+
}
73+
val connection = SshConnection(
74+
transport = clientTransport,
75+
hostKeyVerifier = rejectingVerifier,
76+
coroutineDispatcher = dispatcher,
77+
)
78+
79+
try {
80+
assertIs<ConnectResult.HostKeyRejected>(connectInBackground(connection, backgroundScope, dispatcher))
81+
} finally {
82+
connection.close()
83+
}
84+
}
85+
86+
@Test
87+
fun `connect returns algorithm mismatch when kex negotiation has no match`() = runTest {
88+
val dispatcher = StandardTestDispatcher(testScheduler)
89+
val (clientTransport, serverTransport) = PipedTransport.create()
90+
val server = FakeSshServer(serverTransport, backgroundScope, dispatcher)
91+
server.kexAlgorithms = "unsupported-kex@example.com"
92+
server.start(ignoreTransportErrors = true)
93+
94+
val connection = SshConnection(
95+
transport = clientTransport,
96+
hostKeyVerifier = acceptAllVerifier,
97+
coroutineDispatcher = dispatcher,
98+
)
99+
100+
try {
101+
assertIs<ConnectResult.AlgorithmMismatch>(connectInBackground(connection, backgroundScope, dispatcher))
102+
} finally {
103+
connection.close()
104+
}
105+
}
106+
63107
@Test
64108
fun `password authentication handles success and failure replies`() = runTest {
65109
connectedFixture { connection, server, dispatcher ->
@@ -95,6 +139,21 @@ class SshConnectionFlowTest {
95139
}
96140
}
97141

142+
@Test
143+
fun `public key authentication handles failure reply`() = runTest {
144+
connectedFixture { connection, server, dispatcher ->
145+
val privateKeyData = Files.readString(Paths.get("src/test/resources/keys/ed25519_unencrypted"))
146+
val privateKey = PrivateKeyReader.read(privateKeyData)
147+
148+
val auth = async(dispatcher) { connection.authenticatePublicKey("user", privateKey) }
149+
val request = withTimeout(5_000) { server.awaitUserauthRequest() }
150+
assertEquals("publickey", request.methodName().value())
151+
server.sendUserauthFailure(setOf("password"), partialSuccess = false)
152+
153+
assertIs<AuthResult.Failure>(withTimeout(5_000) { auth.await() })
154+
}
155+
}
156+
98157
@Test
99158
fun `direct keyboard interactive authentication handles info request`() = runTest {
100159
connectedFixture { connection, server, dispatcher ->
@@ -123,6 +182,49 @@ class SshConnectionFlowTest {
123182
}
124183
}
125184

185+
@Test
186+
fun `strategy authentication succeeds when none auth is accepted`() = runTest {
187+
connectedFixture { connection, server, dispatcher ->
188+
val auth = async(dispatcher) { connection.authenticate("user", EmptyAuthHandler()) }
189+
val none = withTimeout(5_000) { server.awaitUserauthRequest() }
190+
assertEquals("none", none.methodName().value())
191+
server.sendUserauthSuccess()
192+
193+
assertEquals(AuthResult.Success, withTimeout(5_000) { auth.await() })
194+
}
195+
}
196+
197+
@Test
198+
fun `strategy authentication delivers banners while discovering methods`() = runTest {
199+
connectedFixture { connection, server, dispatcher ->
200+
val banners = mutableListOf<String>()
201+
val observedMethods = mutableListOf<Set<String>>()
202+
val handler = object : EmptyAuthHandler() {
203+
override suspend fun onAuthMethodsAvailable(methods: Set<String>) {
204+
observedMethods.add(methods)
205+
}
206+
207+
override suspend fun onPasswordNeeded(): String = "secret"
208+
209+
override suspend fun onBanner(message: String) {
210+
banners.add(message)
211+
}
212+
}
213+
214+
val auth = async(dispatcher) { connection.authenticate("user", handler) }
215+
assertEquals("none", withTimeout(5_000) { server.awaitUserauthRequest() }.methodName().value())
216+
server.sendUserauthBanner("maintenance window")
217+
server.sendUserauthFailure(setOf("password"), partialSuccess = false)
218+
219+
assertEquals("password", withTimeout(5_000) { server.awaitUserauthRequest() }.methodName().value())
220+
server.sendUserauthSuccess()
221+
222+
assertEquals(AuthResult.Success, withTimeout(5_000) { auth.await() })
223+
assertEquals(listOf("maintenance window"), banners)
224+
assertEquals(listOf(setOf("password")), observedMethods)
225+
}
226+
}
227+
126228
@Test
127229
fun `strategy authentication discovers methods and succeeds with password`() = runTest {
128230
connectedFixture { connection, server, dispatcher ->
@@ -143,6 +245,17 @@ class SshConnectionFlowTest {
143245
}
144246
}
145247

248+
@Test
249+
fun `strategy authentication fails when password is unavailable`() = runTest {
250+
connectedFixture { connection, server, dispatcher ->
251+
val auth = async(dispatcher) { connection.authenticate("user", EmptyAuthHandler()) }
252+
assertEquals("none", withTimeout(5_000) { server.awaitUserauthRequest() }.methodName().value())
253+
server.sendUserauthFailure(setOf("password"), partialSuccess = false)
254+
255+
assertIs<AuthResult.Failure>(withTimeout(5_000) { auth.await() })
256+
}
257+
}
258+
146259
@Test
147260
fun `strategy authentication handles keyboard interactive prompt`() = runTest {
148261
connectedFixture { connection, server, dispatcher ->

0 commit comments

Comments
 (0)