@@ -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