Unit testing ChannelOptions

Hi all,

How would one go about unit testing the changing of channel options from a handler? Using an EmbeddedChannel, it calls fatalError for getError(:) and setError(:value:). I have a handler that, when added to the pipeline, sets a channel option. It is similar to the sample from swift-nio-ssh:

It does this in the handler because, I assume, it is a child channel pipeline.

I have some other things being handled in this same handler in my code, such has handling user events properly. The call to handlerAdded(context:) will always make my tests fail. It's short so I can paste it here:

import Foundation
import NIO
import NIOSSH
import Logging

internal class SshSftpSubsystemServerHandler: ChannelDuplexHandler {
	typealias InboundIn = SSHChannelData
	typealias InboundOut = ByteBuffer
	typealias OutboundIn = ByteBuffer
	typealias OutboundOut = SSHChannelData

	enum HandlerError: Error, Equatable {
		case unexpectedChannelData
		case unexpectedDataBeforeInitialized
	}

	let logger: Logger

	var isSftpSubsystemInitialized = false

	init(logger: Logger) {
		self.logger = logger
	}

	func handlerAdded(context: ChannelHandlerContext) {
		context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
			context.fireErrorCaught(error)
		}
	}

	func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
		switch event {
		case let subsystemRequest as SSHChannelRequestEvent.SubsystemRequest where subsystemRequest.subsystem == "sftp":
			isSftpSubsystemInitialized = true
			context.triggerUserOutboundEvent(ChannelSuccessEvent(), promise: nil)
		case ChannelEvent.inputClosed:
			self.logger.info("Client \(String(describing: context.channel.remoteAddress)) disconnected.")
			context.close(promise: nil)
		default:
			context.fireUserInboundEventTriggered(event)
		}
	}

	func channelRead(context: ChannelHandlerContext, data: NIOAny) {
		guard isSftpSubsystemInitialized else {
			context.fireErrorCaught(HandlerError.unexpectedDataBeforeInitialized)
			return
		}

		let channelData = self.unwrapInboundIn(data)
		guard case let .byteBuffer(buffer) = channelData.data else {
			context.fireErrorCaught(HandlerError.unexpectedChannelData)
			return
		}

		// Pass along to next handler
		switch channelData.type {
		case .channel:
			context.fireChannelRead(self.wrapInboundOut(buffer))
		case .stdErr:
			// TODO: Log?
			break
		default:
			break
		}
	}

	func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
		guard isSftpSubsystemInitialized else {
			context.fireErrorCaught(HandlerError.unexpectedDataBeforeInitialized)
			return
		}

		let buffer = self.unwrapOutboundIn(data)
		let channelData = SSHChannelData(type: .channel, data: .byteBuffer(buffer))
		context.write(self.wrapOutboundOut(channelData), promise: promise)
	}
}

I suppose I could ignore unit testing the handlerAdded(context:) by moving it to a separate handler, so I can write tests for the user events in this handler. Additionally, I suppose something as simple as setting the value of an option is not too important to unit test (it's not like the options would do anything if set on an EmbeddedChannel anyway). But, I did want to ask here if there's an easy way of doing it.

The only way I can think of doing it right now would be using the ServerBootstrap to create everything as a normal application. But that would need to bind to an address and port, as well as need a client to connect to it to initialize the channel and everything. Doing that in a unit tests seems overkill.

There's no terribly easy way to do this right now, sadly, but my recommendation would be to not set channel options in handlers. You've bumped into the reason why: not all channels support all options, and it's good to try to be able to tolerate that in your handlers. Configuring the channel options from the outside (as part of the bootstraps) is the recommended flow.

I would agree that's a better way of doing it. I followed the NIOSSHServer example thinking it was best practice, but I realize it might have been implemented that way in order to separate the examples of the three different kind of SSH channels more clearly.

I've made the fix here in my repo: Move channel option set from handler to bootstrapper · Jman012/jlsftp@4d8b1d9 · GitHub

After all, in almost all other example servers/clients, the ChannelOptions are all set in the bootstrapper.