swift-nio-extras/Tests/NIOExtrasTests/JSONRPCFramingContentLengthHeaderDecoderTests.swift
Johannes Weiss c38fb10f50 JSONRPC framing: Content-Length (#53)
Motivation:

JSPN-RPC uses various framing methods, one is Content-Length based
framing. NIOExtras should provide encoder/decoders for this.

Modifications:

Add such codecs.

Result:

NIOExtras more useful
2019-05-13 10:37:25 -07:00

185 lines
7.8 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import XCTest
import NIO
import NIOExtras
final class JSONRPCFramingContentLengthHeaderDecoderTests: XCTestCase {
private var channel: EmbeddedChannel! // not a real network connection
override func setUp() {
self.channel = EmbeddedChannel()
// let's add the framing handler to the pipeline as that's what we're testing here.
XCTAssertNoThrow(try self.channel.pipeline.addHandler(ByteToMessageHandler(NIOJSONRPCFraming.ContentLengthHeaderFrameDecoder())).wait())
// this pretends to connect the channel to this IP address.
XCTAssertNoThrow(self.channel.connect(to: try .init(ipAddress: "1.2.3.4", port: 5678)))
}
override func tearDown() {
if self.channel.isActive {
// this makes sure that the channel is clean (no errors, no left-overs in the channel, etc)
XCTAssertNoThrow(XCTAssertTrue(try self.channel.finish().isClean))
}
self.channel = nil
}
private func buffer(string: String) -> ByteBuffer {
var buffer = self.channel.allocator.buffer(capacity: string.utf8.count)
buffer.writeString(string)
return buffer
}
private func buffer(byte: UInt8) -> ByteBuffer {
var buffer = self.channel.allocator.buffer(capacity: 1)
buffer.writeInteger(byte)
return buffer
}
private func readInboundString() throws -> String? {
return try self.channel.readInbound(as: ByteBuffer.self).map {
String(decoding: $0.readableBytesView, as: Unicode.UTF8.self)
}
}
func testBasicMessage() {
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: "Content-Length: 1\r\n\r\nX")))
// we expect exactly one "X" to come out at the other end.
XCTAssertNoThrow(try XCTAssertEqual("X", self.readInboundString()))
}
func testEmptyMessage() {
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: "Content-Length: 0\r\n\r\n")))
// we expect exactly one empty String to come out at the other end.
XCTAssertNoThrow(try XCTAssertEqual("", self.readInboundString()))
}
func testWrongCasing() {
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: "CoNtEnT-LeNgTh: 1\r\n\r\nX")))
// we expect exactly one "X" to come out at the other end.
XCTAssertNoThrow(try XCTAssertEqual("X", self.readInboundString()))
}
func testTechnicallyInvalidButWeAreNicePeople() {
// this writes a bunch of messages that are technically not okay, but we're fine with them
let coupleOfMessages = "Content-Length:1\r\n\r\nX" + // space after colon missing
/* */ "Content-Length : 1\r\n\r\nX" + // extra space before colon
/* */ " Content-Length: 1\r\n\r\nX" + // extra space at the beginning of the header
/* */ "Content-Length: 1\n\r\nX" // \r missing
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: coupleOfMessages)))
for _ in 0..<4 {
XCTAssertNoThrow(try XCTAssertEqual("X", self.readInboundString()))
}
}
func testLongerMessage() {
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: "Content-Length: 5\r\n\r\n12345")))
XCTAssertNoThrow(try XCTAssertEqual("12345", self.readInboundString()))
}
func testSomePointlessExtraHeaders() {
let s = "foo: bar\r\nContent-Length: 4\r\nbuz: qux\r\n\r\n1234"
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: s)))
XCTAssertNoThrow(try XCTAssertEqual("1234", self.readInboundString()))
}
func testDripAndMassFeedMessages() {
let messagesAndExpectedOutput: [(String, String)] =
[ ("Content-Length: 1\r\n\r\n1", "1"),
("Content-Length: 0\r\n\r\n", ""),
("foo: bar\r\nContent-Length: 7\r\nbuz: qux\r\n\r\nqwerasd", "qwerasd"),
("content-lengTH: 1 \r\n\r\nX", "X")
]
// drip feed (byte by byte)
for (message, expected) in messagesAndExpectedOutput {
for byte in message.utf8 {
// before the last byte, no output should happen
XCTAssertNoThrow(XCTAssertNil(try channel.readInbound(), "premature output for '\(message)'"))
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(byte: byte)))
}
XCTAssertNoThrow(try XCTAssertEqual(expected, self.readInboundString()))
}
// mass feed (many messages in one go)
let everything = messagesAndExpectedOutput.map { $0.0 }.reduce("", +)
// 3 times every message
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: everything + everything + everything)))
for _ in 0..<3 {
for expected in messagesAndExpectedOutput.map({$0.1}) {
XCTAssertNoThrow(try XCTAssertEqual(expected, self.readInboundString()))
}
}
}
func testErrorNoContentLengthHeader() {
let s = "Content-Type: text/plain\r\n\r\n12345"
XCTAssertThrowsError(try self.channel.writeInbound(self.buffer(string: s))) { error in
if let error = error as? NIOJSONRPCFraming.ContentLengthHeaderFrameDecoder.DecodingError {
XCTAssertEqual(.missingContentLengthHeader, error)
} else {
XCTFail("unexpected error: \(error)")
}
}
// this shouldn't produce output
XCTAssertNoThrow(try XCTAssertNil(self.channel.readInbound(as: ByteBuffer.self)))
}
func testErrorNotEnoughDataAtEOF() {
let s = "Content-Length: 4\r\n\r\n123" // only three bytes payload, not 4
XCTAssertNoThrow(try self.channel.writeInbound(self.buffer(string: s)))
XCTAssertNoThrow(try XCTAssertNil(self.channel.readInbound()))
XCTAssertThrowsError(try self.channel.finish()) { error in
if case .some(.leftoverDataWhenDone(let leftOvers)) = error as? ByteToMessageDecoderError {
XCTAssertEqual("123", String(decoding: leftOvers.readableBytesView, as: Unicode.UTF8.self))
} else {
XCTFail("unexpected error: \(error)")
}
}
}
func testErrorNegativeContentLength() {
let s = "Content-Length: -1\r\n\r\n"
XCTAssertThrowsError(try self.channel.writeInbound(self.buffer(string: s))) { error in
if let error = error as? NIOJSONRPCFraming.ContentLengthHeaderFrameDecoder.DecodingError {
XCTAssertEqual(.illegalContentLengthHeaderValue(" -1\r\n"), error)
} else {
XCTFail("unexpected error: \(error)")
}
}
// this shouldn't produce output
XCTAssertNoThrow(try XCTAssertNil(self.channel.readInbound(as: ByteBuffer.self)))
}
func testErrorNotANumberContentLength() {
let s = "Content-Length: a\r\n\r\n"
XCTAssertThrowsError(try self.channel.writeInbound(self.buffer(string: s))) { error in
if let error = error as? NIOJSONRPCFraming.ContentLengthHeaderFrameDecoder.DecodingError {
XCTAssertEqual(.illegalContentLengthHeaderValue(" a\r\n"), error)
} else {
XCTFail("unexpected error: \(error)")
}
}
// this shouldn't produce output
XCTAssertNoThrow(try XCTAssertNil(self.channel.readInbound(as: ByteBuffer.self)))
}
}