Skip to content

Commit 7a37e82

Browse files
authored
Merge pull request #59 from orlandos-nl/jo/end-to-end-tests
End-to-end tests, from Citadel Client to Citadel Server
2 parents b976c33 + 902eed0 commit 7a37e82

File tree

4 files changed

+280
-6
lines changed

4 files changed

+280
-6
lines changed

Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
"repositoryURL": "https://github.com/Joannis/swift-nio-ssh.git",
6161
"state": {
6262
"branch": null,
63-
"revision": "d5fc603de485eca5a1e657361e3a8452875d108b",
64-
"version": "0.3.0"
63+
"revision": "01e03b888734b03f1005b0ca329d7b5af50208e7",
64+
"version": "0.3.2"
6565
}
6666
}
6767
]

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ let package = Package(
1616
),
1717
],
1818
dependencies: [
19-
.package(name: "swift-nio-ssh", url: "https://github.com/Joannis/swift-nio-ssh.git", "0.3.0" ..< "0.4.0"),
19+
.package(name: "swift-nio-ssh", url: "https://github.com/Joannis/swift-nio-ssh.git", "0.3.2" ..< "0.4.0"),
2020
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2121
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"),
2222
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "2.1.0"),

Sources/Citadel/ClientSession.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import NIO
22
import NIOSSH
33

4+
struct AuthenticationFailed: Error, Equatable {}
45
final class ClientHandshakeHandler: ChannelInboundHandler {
56
typealias InboundIn = Any
67

@@ -14,8 +15,7 @@ final class ClientHandshakeHandler: ChannelInboundHandler {
1415
init(eventLoop: EventLoop) {
1516
let promise = eventLoop.makePromise(of: Void.self)
1617
self.promise = promise
17-
18-
struct AuthenticationFailed: Error {}
18+
1919
eventLoop.scheduleTask(in: .seconds(10)) {
2020
promise.fail(AuthenticationFailed())
2121
}
@@ -138,7 +138,7 @@ final class SSHClientSession {
138138
}
139139
}
140140

141-
public struct InvalidHostKey: Error {}
141+
public struct InvalidHostKey: Error, Equatable {}
142142

143143
/// A host key validator that can be used to validate an SSH host key. This can be used to validate the host key against a set of trusted keys, or to accept any key.
144144
public struct SSHHostKeyValidator: NIOSSHClientServerAuthenticationDelegate {
+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
@testable import Citadel
2+
import Crypto
3+
import NIO
4+
import NIOSSH
5+
import XCTest
6+
7+
final class AuthDelegate: NIOSSHServerUserAuthenticationDelegate {
8+
var supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods
9+
var handle: @Sendable (NIOSSHUserAuthenticationRequest, EventLoopPromise<NIOSSHUserAuthenticationOutcome>) -> Void
10+
11+
init(
12+
supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods,
13+
handle: @Sendable @escaping (NIOSSHUserAuthenticationRequest, EventLoopPromise<NIOSSHUserAuthenticationOutcome>) -> Void
14+
) {
15+
self.supportedAuthenticationMethods = supportedAuthenticationMethods
16+
self.handle = handle
17+
}
18+
19+
func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise<NIOSSHUserAuthenticationOutcome>) {
20+
handle(request, responsePromise)
21+
}
22+
}
23+
24+
struct InvalidCredentials: Error, Equatable {}
25+
26+
final class EndToEndTests: XCTestCase {
27+
func runTest<ExpectedError: Error & Equatable>(
28+
credentials: SSHAuthenticationMethod,
29+
hostKeyValidator: SSHHostKeyValidator,
30+
expectedError: ExpectedError
31+
) async throws {
32+
try await runTest(
33+
credentials: credentials,
34+
hostKeyValidator: hostKeyValidator,
35+
perform: { _, _ in
36+
XCTFail("Should never get here")
37+
},
38+
matchingError: { error in
39+
guard let error = error as? ExpectedError else {
40+
return false
41+
}
42+
43+
return error == expectedError
44+
},
45+
expectsFailure: true
46+
)
47+
}
48+
49+
func runTest(
50+
credentials: SSHAuthenticationMethod = .passwordBased(username: "citadel", password: "test"),
51+
hostKey: NIOSSHPrivateKey = .init(p521Key: .init()),
52+
hostKeyValidator: SSHHostKeyValidator = .acceptAnything(),
53+
perform: (SSHServer, SSHClient) async throws -> Void,
54+
matchingError matchError: (Error) -> Bool,
55+
expectsFailure: Bool
56+
) async throws {
57+
let authDelegate = AuthDelegate(supportedAuthenticationMethods: .password) { request, promise in
58+
switch request.request {
59+
case .password(.init(password: "test")) where request.username == "citadel":
60+
promise.succeed(.success)
61+
default:
62+
promise.fail(InvalidCredentials())
63+
}
64+
}
65+
let server = try await SSHServer.host(
66+
host: "localhost",
67+
port: 2222,
68+
hostKeys: [
69+
hostKey
70+
],
71+
authenticationDelegate: authDelegate
72+
)
73+
74+
do {
75+
let client = try await SSHClient.connect(
76+
host: "localhost",
77+
port: 2222,
78+
authenticationMethod: credentials,
79+
hostKeyValidator: hostKeyValidator,
80+
reconnect: .never
81+
)
82+
83+
if expectsFailure {
84+
XCTFail("Client was not supposed to connect successfully")
85+
} else {
86+
try await perform(server, client)
87+
}
88+
89+
try await client.close()
90+
} catch {
91+
guard matchError(error) else {
92+
try await server.close()
93+
throw error
94+
}
95+
}
96+
97+
try await server.close()
98+
}
99+
100+
func testServerRejectsPassword() async throws {
101+
try await runTest(
102+
credentials: .passwordBased(
103+
username: "citadel",
104+
password: "wrong"
105+
),
106+
hostKeyValidator: .acceptAnything(),
107+
expectedError: AuthenticationFailed()
108+
)
109+
}
110+
111+
func testServerRejectsUsername() async throws {
112+
try await runTest(
113+
credentials: .passwordBased(
114+
username: "citadel2",
115+
password: "test"
116+
),
117+
hostKeyValidator: .acceptAnything(),
118+
expectedError: AuthenticationFailed()
119+
)
120+
}
121+
122+
func testClientRejectsHostKey() async throws {
123+
try await runTest(
124+
credentials: .passwordBased(
125+
username: "citadel",
126+
password: "test"
127+
),
128+
hostKeyValidator: .trustedKeys([]),
129+
expectedError: InvalidHostKey()
130+
)
131+
}
132+
133+
func testClientConnectsSuccessfully() async throws {
134+
try await runTest(
135+
perform: { _, _ in },
136+
matchingError: { _ in false },
137+
expectsFailure: false
138+
)
139+
}
140+
141+
func testClientRejectsWrongHostKey() async throws {
142+
let hostKey = NIOSSHPrivateKey(p521Key: .init())
143+
try await runTest(
144+
hostKey: hostKey,
145+
hostKeyValidator: .trustedKeys([
146+
hostKey.publicKey
147+
]),
148+
perform: { _, _ in },
149+
matchingError: { _ in false },
150+
expectsFailure: false
151+
)
152+
}
153+
154+
func testSimpleSFTP() async throws {
155+
final class SFTP: SFTPDelegate {
156+
var didCreateDirectory = false
157+
func createDirectory(_ filePath: String, withAttributes: SFTPFileAttributes, context: SSHContext) async throws -> SFTPStatusCode {
158+
XCTAssertEqual(context.username, "citadel")
159+
XCTAssertEqual(filePath, "/test/citadel/sftp")
160+
didCreateDirectory = true
161+
return .ok
162+
}
163+
}
164+
165+
try await runTest(
166+
perform: { server, client in
167+
let sftpServer = SFTP()
168+
server.enableSFTP(withDelegate: sftpServer)
169+
let sftp = try await client.openSFTP()
170+
try await sftp.createDirectory(atPath: "/test/citadel/sftp")
171+
XCTAssertTrue(sftpServer.didCreateDirectory)
172+
},
173+
matchingError: { _ in false },
174+
expectsFailure: false
175+
)
176+
}
177+
178+
func testExecExitCode() async throws {
179+
final class Exec: ExecDelegate {
180+
struct CommandContext: ExecCommandContext {
181+
func terminate() async throws {
182+
// Always fine
183+
}
184+
}
185+
186+
var ranCommand = false
187+
func setEnvironmentValue(_ value: String, forKey key: String) async throws {}
188+
func start(command: String, outputHandler: ExecOutputHandler) async throws -> ExecCommandContext {
189+
XCTAssertEqual(command, "ls")
190+
defer { ranCommand = true }
191+
192+
if !ranCommand {
193+
// First command always fails
194+
outputHandler.succeed(exitCode: 1)
195+
} else {
196+
// Successive commmands succeed
197+
outputHandler.succeed(exitCode: 0)
198+
}
199+
200+
return CommandContext()
201+
}
202+
}
203+
204+
try await runTest(
205+
perform: { server, client in
206+
let execServer = Exec()
207+
server.enableExec(withDelegate: execServer)
208+
209+
do {
210+
_ = try await client.executeCommand("ls")
211+
XCTFail("Shouldn't succeed on the first attempt")
212+
} catch let error as SSHClient.CommandFailed where error.exitCode == 1 {}
213+
214+
XCTAssertTrue(execServer.ranCommand)
215+
216+
_ = try await client.executeCommand("ls")
217+
},
218+
matchingError: { _ in false },
219+
expectsFailure: false
220+
)
221+
}
222+
}
223+
224+
extension SFTPDelegate {
225+
func fileAttributes(atPath path: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPFileAttributes {
226+
throw ShouldNotGetHere()
227+
}
228+
229+
func openFile(_ filePath: String, withAttributes: Citadel.SFTPFileAttributes, flags: Citadel.SFTPOpenFileFlags, context: Citadel.SSHContext) async throws -> Citadel.SFTPFileHandle {
230+
throw ShouldNotGetHere()
231+
}
232+
233+
func removeFile(_ filePath: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
234+
throw ShouldNotGetHere()
235+
}
236+
237+
func createDirectory(_ filePath: String, withAttributes: Citadel.SFTPFileAttributes, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
238+
throw ShouldNotGetHere()
239+
}
240+
241+
func removeDirectory(_ filePath: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
242+
throw ShouldNotGetHere()
243+
}
244+
245+
func realPath(for canonicalUrl: String, context: Citadel.SSHContext) async throws -> [Citadel.SFTPPathComponent] {
246+
throw ShouldNotGetHere()
247+
}
248+
249+
func openDirectory(atPath path: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPDirectoryHandle {
250+
throw ShouldNotGetHere()
251+
}
252+
253+
func setFileAttributes(to attributes: Citadel.SFTPFileAttributes, atPath path: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
254+
throw ShouldNotGetHere()
255+
}
256+
257+
func addSymlink(linkPath: String, targetPath: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
258+
throw ShouldNotGetHere()
259+
}
260+
261+
func readSymlink(atPath path: String, context: Citadel.SSHContext) async throws -> [Citadel.SFTPPathComponent] {
262+
throw ShouldNotGetHere()
263+
}
264+
265+
func rename(oldPath: String, newPath: String, flags: UInt32, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
266+
throw ShouldNotGetHere()
267+
}
268+
}
269+
270+
struct ShouldNotGetHere: Error {
271+
init() {
272+
XCTFail("Should not get here")
273+
}
274+
}

0 commit comments

Comments
 (0)