From 3c0e613b0f296434a0f73862bbed8970983570cb Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 7 Feb 2021 23:56:23 +0100
Subject: [PATCH] Additional new tests

---
 .../protocol/io/CoroutineChannel.kt           |   4 +-
 .../kuschku/quasseldroid/ExampleUnitTest.kt   |   2 +
 coverage-annotations/build.gradle.kts         |   7 +
 .../de/kuschku/codecoverage/Generated.kt      |  16 +--
 protocol/build.gradle.kts                     |   3 +-
 .../libquassel/protocol/io/ByteBufferUtil.kt  |  53 ++++----
 .../protocol/io/ChainedByteBuffer.kt          |  86 ++++++-------
 .../libquassel/protocol/io/StringEncoder.kt   |  68 ++++------
 .../primitive/ByteBufferSerializer.kt         |   2 +-
 .../serializers/primitive/QCharSerializer.kt  |   2 +-
 .../primitive/QVariantSerializer.kt           |   2 +-
 .../libquassel/protocol/types/SignedId.kt     |   3 +
 .../libquassel/protocol/variant/QVariant.kt   |  23 +++-
 .../libquassel/protocol/variant/QtType.kt     |  46 +++----
 .../protocol/features/FeatureSetTest.kt       |  74 +++++++++++
 .../protocol/io/ChainedByteBufferTest.kt      | 110 ++++++++++++++++
 .../protocol/io/StringEncoderTest.kt          |  91 +++++++++++++
 .../primitive/ByteBufferSerializerTest.kt     |  27 ++++
 .../primitive/DateTimeSerializerTest.kt       |   2 +-
 .../primitive/MessageSerializerTest.kt        |  10 +-
 .../primitive/QCharSerializerTest.kt          |   4 +-
 .../primitive/QVariantMapSerializerTest.kt    |  57 +++++++++
 .../primitive/StringSerializerTest.kt         |  63 +++++++--
 .../testutil/handshakeSerializerTest.kt       |  22 ++--
 .../testutil/matchers/BomMatcherChar.kt       |   2 +-
 .../testutil/matchers/ByteBufferMatcher.kt    |  13 +-
 .../protocol/testutil/matchers/MapMatcher.kt  |  62 +++++++++
 .../protocol/testutil/qtSerializerTest.kt     |  24 ++--
 .../testutil/quasselSerializerTest.kt         |  22 ++--
 .../libquassel/protocol/testutil/serialize.kt |  61 +++++++++
 .../testHandshakeSerializerEncoded.kt         |   4 +-
 .../testutil/testQtSerializerDirect.kt        |   6 +-
 .../testutil/testQtSerializerVariant.kt       |   4 +-
 .../testutil/testQuasselSerializerDirect.kt   |   5 +-
 .../testutil/testQuasselSerializerVariant.kt  |   4 +-
 .../libquassel/protocol/types/SignedIdTest.kt | 120 ++++++++++++++++++
 .../protocol/variant/QVariantTest.kt          |  46 +++++++
 settings.gradle.kts                           |   3 +-
 38 files changed, 922 insertions(+), 231 deletions(-)
 create mode 100644 coverage-annotations/build.gradle.kts
 rename protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoders.kt => coverage-annotations/src/main/kotlin/de/kuschku/codecoverage/Generated.kt (61%)
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/features/FeatureSetTest.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/ChainedByteBufferTest.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/StringEncoderTest.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QVariantMapSerializerTest.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/MapMatcher.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/serialize.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/types/SignedIdTest.kt
 create mode 100644 protocol/src/test/kotlin/de/kuschku/libquassel/protocol/variant/QVariantTest.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
index d400951e5..a2d3b88db 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
@@ -20,7 +20,6 @@
 package de.kuschku.quasseldroid.protocol.io
 
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
 import de.kuschku.quasseldroid.util.TlsInfo
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asCoroutineDispatcher
@@ -67,12 +66,11 @@ class CoroutineChannel {
   }
 
   suspend fun write(buffer: ByteBuffer): Int = runInterruptible(writeContext) {
-    buffer.print()
     this.channel.write(buffer)
   }
 
   suspend fun write(chainedBuffer: ChainedByteBuffer) {
-    for (buffer in chainedBuffer.buffers()) {
+    for (buffer in chainedBuffer.iterator()) {
       write(buffer)
     }
   }
diff --git a/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt b/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
index dae870cd4..6a9c5578c 100644
--- a/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
+++ b/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
@@ -3,6 +3,7 @@ package de.kuschku.quasseldroid
 import de.kuschku.libquassel.protocol.connection.ProtocolInfoSerializer
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
+import de.kuschku.libquassel.protocol.io.contentToString
 import de.kuschku.libquassel.protocol.messages.handshake.ClientInit
 import de.kuschku.libquassel.protocol.serializers.HandshakeSerializers
 import de.kuschku.libquassel.protocol.serializers.handshake.ClientInitAckSerializer
@@ -68,6 +69,7 @@ class ExampleUnitTest {
           channel.write(sizeBuffer)
           sizeBuffer.clear()
         }
+        println(sendBuffer.toBuffer().contentToString())
         channel.write(sendBuffer)
         channel.flush()
         sendBuffer.clear()
diff --git a/coverage-annotations/build.gradle.kts b/coverage-annotations/build.gradle.kts
new file mode 100644
index 000000000..82b248afb
--- /dev/null
+++ b/coverage-annotations/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+  kotlin("jvm")
+}
+
+dependencies {
+  implementation(kotlin("stdlib"))
+}
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoders.kt b/coverage-annotations/src/main/kotlin/de/kuschku/codecoverage/Generated.kt
similarity index 61%
rename from protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoders.kt
rename to coverage-annotations/src/main/kotlin/de/kuschku/codecoverage/Generated.kt
index b6f016609..7b3674fb8 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoders.kt
+++ b/coverage-annotations/src/main/kotlin/de/kuschku/codecoverage/Generated.kt
@@ -17,14 +17,10 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.protocol.io
+package de.kuschku.codecoverage
 
-import kotlin.concurrent.getOrSet
-
-private val ascii = ThreadLocal<StringEncoder>()
-private val utf8 = ThreadLocal<StringEncoder>()
-private val utf16 = ThreadLocal<StringEncoder>()
-
-fun stringEncoderAscii() = ascii.getOrSet { StringEncoder(Charsets.ISO_8859_1) }
-fun stringEncoderUtf8() = utf8.getOrSet { StringEncoder(Charsets.UTF_8) }
-fun stringEncoderUtf16() = utf16.getOrSet { StringEncoder(Charsets.UTF_16BE) }
+/**
+ * Used to mark inline functions as generated for jacoco
+ */
+@Retention(AnnotationRetention.SOURCE)
+annotation class Generated
diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts
index a01ee6993..493af5722 100644
--- a/protocol/build.gradle.kts
+++ b/protocol/build.gradle.kts
@@ -9,7 +9,7 @@ tasks.withType<Test> {
 }
 
 jacoco {
-  toolVersion = "0.8.6"
+  toolVersion = "0.8.3"
 }
 
 tasks.getByName<JacocoReport>("jacocoTestReport") {
@@ -26,6 +26,7 @@ dependencies {
   implementation(kotlin("stdlib"))
   implementation("org.threeten", "threetenbp", "1.4.0")
   api(project(":bitflags"))
+  api(project(":coverage-annotations"))
 
   val junit5Version: String by project.extra
   testImplementation("org.junit.jupiter", "junit-jupiter-api", junit5Version)
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ByteBufferUtil.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ByteBufferUtil.kt
index b2e8f7c4c..d4f7e0797 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ByteBufferUtil.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ByteBufferUtil.kt
@@ -21,49 +21,42 @@ package de.kuschku.libquassel.protocol.io
 
 import java.nio.ByteBuffer
 
-fun copyData(from: ByteBuffer, to: ByteBuffer, amount: Int = -1) {
-  val actualAmount =
-    if (amount >= 0) minOf(from.remaining(), to.remaining(), amount)
-    else minOf(from.remaining(), to.remaining())
-  for (i in 0 until actualAmount) {
-    to.put(from.get())
-  }/*
-  if (actualAmount > 0) {
-    val fromLimit = from.limit()
-    val toLimit = to.limit()
-    from.limit(from.position() + actualAmount)
-    to.limit(to.position() + actualAmount)
-    to.put(from)
-    from.limit(fromLimit)
-    to.limit(toLimit)
-  }
-  */
+fun copyData(from: ByteBuffer, to: ByteBuffer, desiredAmount: Int = -1) {
+  val limit = from.limit()
+  val availableAmount = minOf(from.remaining(), to.remaining())
+  val amount =
+    if (desiredAmount < 0) availableAmount
+    else minOf(availableAmount, desiredAmount)
+  from.limit(from.position() + amount)
+  to.put(from)
+  from.limit(limit)
+}
+
+fun copyData(from: ByteBuffer, desiredAmount: Int): ByteBuffer {
+  val to = ByteBuffer.allocate(minOf(from.remaining(), desiredAmount))
+  copyData(from, to, desiredAmount)
+  return to.flip()
 }
 
-val alphabet = charArrayOf(
+fun ByteBuffer?.isEmpty() = this == null || !this.hasRemaining()
+
+private val alphabet = charArrayOf(
   '0', '1', '2', '3', '4', '5', '6', '7',
   '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
 )
 
 fun ByteBuffer.contentToString(): String {
-  mark()
+  val position = position()
+  val limit = limit()
   var result = ""
-  while (remaining() > 0) {
+  while (hasRemaining()) {
     val byte = get()
     val upperNibble = byte.toInt() shr 4
     val lowerNibble = byte.toInt() % 16
     result += alphabet[(upperNibble + 16) % 16]
     result += alphabet[(lowerNibble + 16) % 16]
   }
-  reset()
+  limit(limit)
+  position(position)
   return result
 }
-
-fun ByteBuffer.print() = println(contentToString())
-
-fun copyData(from: ByteBuffer, amount: Int) = ByteBuffer.allocateDirect(amount).also {
-  if (amount > 0) {
-    copyData(from, it, amount)
-    it.clear()
-  }
-}
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ChainedByteBuffer.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ChainedByteBuffer.kt
index bb493d2ba..ba8aeade6 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ChainedByteBuffer.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/ChainedByteBuffer.kt
@@ -22,40 +22,31 @@ package de.kuschku.libquassel.protocol.io
 import java.nio.ByteBuffer
 import java.util.*
 
-class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct: Boolean = false) {
+class ChainedByteBuffer(
+  private val chunkSize: Int = 1024,
+  private val direct: Boolean = false,
+  private val limit: Long = 0,
+) : Iterable<ByteBuffer> {
   private val bufferList: MutableList<ByteBuffer> = ArrayList()
 
+  private var currentIndex = 0
+
   var size = 0
     private set
 
-  private var currentBuffer = 0
-
-  private fun allocate(size: Int) = when (direct) {
-    true -> ByteBuffer.allocateDirect(size)
-    false -> ByteBuffer.allocate(size)
-  }
-
-  private fun ensureSpace(size: Int) {
-    if (bufferList.isEmpty()) {
-      bufferList.add(allocate(bufferSize))
-    }
-    if (bufferList[currentBuffer].remaining() < size) {
-      currentBuffer += 1
+  private fun allocate(amount: Int): ByteBuffer {
+    require(limit <= 0 || size + amount <= limit) {
+      "Can not allocate $amount bytes, currently at $size, limit is $limit"
     }
-    if (currentBuffer == bufferList.size) {
-      bufferList.add(allocate(bufferSize))
-    }
-    this.size += size
+    return if (direct) ByteBuffer.allocateDirect(amount)
+    else ByteBuffer.allocate(amount)
   }
 
-  fun <T> withBuffer(length: Int = 0, f: (ByteBuffer) -> T): T {
-    ensureSpace(length)
-    val buffer = bufferList.last()
-    val positionBefore = buffer.position()
-    val result = f(buffer)
-    val positionAfter = buffer.position()
-    size += (positionAfter - positionBefore)
-    return result
+  private fun ensureSpace(requested: Int) {
+    if (bufferList.lastOrNull()?.remaining() ?: 0 < requested) {
+      bufferList.add(allocate(chunkSize))
+    }
+    size += requested
   }
 
   fun put(value: Byte) {
@@ -64,12 +55,6 @@ class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct:
     bufferList.last().put(value)
   }
 
-  fun putChar(value: Char) {
-    ensureSpace(2)
-
-    bufferList.last().putChar(value)
-  }
-
   fun putShort(value: Short) {
     ensureSpace(2)
 
@@ -101,9 +86,11 @@ class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct:
   }
 
   fun put(value: ByteBuffer) {
-    ensureSpace(value.remaining())
-
-    while (value.remaining() > 0) {
+    while (value.hasRemaining()) {
+      val requested = minOf(value.remaining(), chunkSize)
+      if (bufferList.lastOrNull()?.hasRemaining() != true) {
+        ensureSpace(requested)
+      }
       copyData(value, bufferList.last())
     }
   }
@@ -114,28 +101,29 @@ class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct:
 
   fun clear() {
     bufferList.clear()
-    currentBuffer = 0
     size = 0
   }
 
-  fun buffers() = sequence {
-    for (buffer in bufferList) {
-      buffer.flip()
-      val position = buffer.position()
-      val limit = buffer.limit()
-      yield(buffer)
-      buffer.position(position)
-      buffer.limit(limit)
-    }
-  }
+  override fun iterator() = ChainedByteBufferIterator(this)
 
   fun toBuffer(): ByteBuffer {
-    val byteBuffer = allocate(bufferSize * bufferList.size)
-    for (buffer in bufferList) {
-      buffer.flip()
+    val byteBuffer = allocate(chunkSize * bufferList.size)
+    for (buffer in iterator()) {
       byteBuffer.put(buffer)
     }
     byteBuffer.flip()
     return byteBuffer
   }
+
+  class ChainedByteBufferIterator(
+    private val buffer: ChainedByteBuffer
+  ) : Iterator<ByteBuffer> {
+    private var index = 0
+
+    override fun hasNext() =
+      index < buffer.bufferList.size
+
+    override fun next(): ByteBuffer =
+      buffer.bufferList[index++].duplicate().flip()
+  }
 }
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoder.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoder.kt
index 280f7b0a9..08ebb2a33 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoder.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/io/StringEncoder.kt
@@ -22,83 +22,71 @@ package de.kuschku.libquassel.protocol.io
 import java.nio.ByteBuffer
 import java.nio.CharBuffer
 import java.nio.charset.Charset
-import java.nio.charset.CoderResult
 
 class StringEncoder(charset: Charset) {
   private val encoder = charset.newEncoder()
   private val decoder = charset.newDecoder()
   private val charBuffer = CharBuffer.allocate(1024)
 
-  private fun charBuffer(length: Int): CharBuffer =
-    if (length < 1024) charBuffer.clear()
-    else CharBuffer.allocate(length)
-
-  fun encode(data: String?, target: ChainedByteBuffer) {
-    if (data == null) return
+  private fun charBuffer(length: Int): CharBuffer {
+    if (length < 1024) {
+      return charBuffer.clear()
+    } else {
+      return CharBuffer.allocate(length)
+    }
+  }
 
-    val charBuffer = charBuffer(data.length)
-    charBuffer.put(data)
-    charBuffer.flip()
+  private fun encodeInternal(data: CharBuffer): ByteBuffer {
     encoder.reset()
-    var result: CoderResult
-    do {
-      result = target.withBuffer(charBuffer.remaining()) {
-        encoder.encode(charBuffer, it, true)
-      }
-    } while (result == CoderResult.OVERFLOW)
+    return encoder.encode(data)
   }
 
   fun encode(data: String?): ByteBuffer {
-    if (data == null) {
+    if (data == null || !encoder.canEncode(data)) {
       return ByteBuffer.allocateDirect(0)
     }
 
     val charBuffer = charBuffer(data.length)
     charBuffer.put(data)
     charBuffer.flip()
-    encoder.reset()
-    return encoder.encode(charBuffer)
+    return encodeInternal(charBuffer)
   }
 
-  fun encodeChar(data: Char?, target: ChainedByteBuffer) {
-    if (data == null) {
-      target.putShort(0)
-    } else {
-      target.putChar(data)
+  fun encodeChar(data: Char?): ByteBuffer {
+    if (!encoder.canEncode(data ?: '\u0000')) {
+      return ByteBuffer.allocateDirect(2)
     }
+
+    val charBuffer = charBuffer(2)
+    charBuffer.put(data ?: '\u0000')
+    charBuffer.flip()
+    return encodeInternal(charBuffer)
   }
 
-  fun decode(source: ByteBuffer, length: Int): String {
+  private fun decodeInternal(source: ByteBuffer, length: Int): CharBuffer {
     val charBuffer = charBuffer(length)
     val oldlimit = source.limit()
     source.limit(source.position() + length)
     decoder.reset()
     decoder.decode(source, charBuffer, true).also {
       if (it.isError) {
+        charBuffer.put('\uFFFD')
         source.position(source.position() + it.length())
       }
     }
     source.limit(oldlimit)
-    charBuffer.flip()
-    return charBuffer.toString()
+    return charBuffer.flip()
+  }
+
+  fun decode(source: ByteBuffer, length: Int): String {
+    return decodeInternal(source, length).toString()
   }
 
   fun decode(source: ByteBuffer): String {
-    println("Called to decode ${source.contentToString()}")
-    val charBuffer = charBuffer(source.remaining())
-    decoder.reset()
-    decoder.decode(source, charBuffer, true).also {
-      if (it.isError) {
-        println("Encountered error: $it")
-        source.position(source.position() + it.length())
-      }
-    }
-    charBuffer.flip()
-    println("Result: $charBuffer")
-    return charBuffer.toString()
+    return decodeInternal(source, source.remaining()).toString()
   }
 
   fun decodeChar(source: ByteBuffer): Char {
-    return source.getChar()
+    return decodeInternal(source, 2).get()
   }
 }
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializer.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializer.kt
index d510b502d..f32731be3 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializer.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializer.kt
@@ -30,7 +30,7 @@ object ByteBufferSerializer : QtSerializer<ByteBuffer?> {
   override val javaType: Class<out ByteBuffer?> = ByteBuffer::class.java
 
   override fun serialize(buffer: ChainedByteBuffer, data: ByteBuffer?, featureSet: FeatureSet) {
-    IntSerializer.serialize(buffer, data?.remaining() ?: 0, featureSet)
+    IntSerializer.serialize(buffer, data?.remaining() ?: -1, featureSet)
     if (data != null) {
       buffer.put(data)
     }
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializer.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializer.kt
index e8147e54b..bf78fc8c5 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializer.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializer.kt
@@ -34,7 +34,7 @@ object QCharSerializer : QtSerializer<Char> {
   private fun encoder() = encoderLocal.getOrSet { StringEncoder(Charsets.UTF_16BE) }
 
   override fun serialize(buffer: ChainedByteBuffer, data: Char, featureSet: FeatureSet) {
-    encoder().encodeChar(data, buffer)
+    buffer.put(encoder().encodeChar(data))
   }
 
   override fun deserialize(buffer: ByteBuffer, featureSet: FeatureSet): Char {
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QVariantSerializer.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QVariantSerializer.kt
index 19dd1b435..18b9113a1 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QVariantSerializer.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/serializers/primitive/QVariantSerializer.kt
@@ -37,7 +37,7 @@ object QVariantSerializer : QtSerializer<QVariant_> {
   override fun serialize(buffer: ChainedByteBuffer, data: QVariant_, featureSet: FeatureSet) {
     IntSerializer.serialize(buffer, data.serializer.qtType.id, featureSet)
     BoolSerializer.serialize(buffer, false, featureSet)
-    if (data is QVariant.Custom && data.serializer.qtType == QtType.UserType) {
+    if (data is QVariant.Custom) {
       StringSerializerAscii.serialize(buffer, data.serializer.quasselType.typeName, featureSet)
     }
     data.serialize(buffer, featureSet)
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/types/SignedId.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/types/SignedId.kt
index fb3f1bae8..364ec2206 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/types/SignedId.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/types/SignedId.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.libquassel.protocol.types
 
+import de.kuschku.codecoverage.Generated
 import java.io.Serializable
 
 typealias SignedIdType = Int
@@ -35,8 +36,10 @@ interface SignedId<T> : Serializable, Comparable<SignedId<T>>
 
 @Suppress("NOTHING_TO_INLINE")
 @JvmName("isValidId")
+@Generated
 inline fun SignedId<SignedIdType>.isValid() = id > 0
 
 @Suppress("NOTHING_TO_INLINE")
 @JvmName("isValidId64")
+@Generated
 inline fun SignedId<SignedId64Type>.isValid() = id > 0
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QVariant.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QVariant.kt
index c4bf2986c..a006ff4c7 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QVariant.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QVariant.kt
@@ -35,7 +35,7 @@ typealias QVariantMap = Map<String, QVariant_>
 typealias QStringList = List<String?>
 
 sealed class QVariant<T> constructor(
-  internal val data: T,
+  private val data: T,
   open val serializer: QtSerializer<T>,
 ) {
   class Typed<T> internal constructor(data: T, serializer: QtSerializer<T>) :
@@ -54,8 +54,6 @@ sealed class QVariant<T> constructor(
   override fun toString() = when (data) {
     is ByteBuffer ->
       "QVariant(${serializer::class.java.simpleName}, ${data.contentToString()})"
-    is Array<*> ->
-      "QVariant(${serializer::class.java.simpleName}, ${Arrays.toString(data)})"
     else ->
       "QVariant(${serializer::class.java.simpleName}, $data)"
   }
@@ -65,6 +63,25 @@ sealed class QVariant<T> constructor(
     if (serializer.javaType == T::class.java && this.value() is T) this as QVariant<T>
     else null
 
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as QVariant<*>
+
+    if (data != other.data) return false
+    if (serializer != other.serializer) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = data?.hashCode() ?: 0
+    result = 31 * result + serializer.hashCode()
+    return result
+  }
+
+
   companion object {
     fun <T> of(data: T, serializer: QtSerializer<T>) = Typed(data, serializer)
     fun <T> of(data: T, serializer: QuasselSerializer<T>) = Custom(data, serializer)
diff --git a/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QtType.kt b/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QtType.kt
index c0b682a40..e8c4626d7 100644
--- a/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QtType.kt
+++ b/protocol/src/main/java/de/kuschku/libquassel/protocol/variant/QtType.kt
@@ -14,34 +14,32 @@
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License along
- * with this program. If not, see <http://www.gnu.org/licenses/>.
+ * with this program. If not, see <http:
  */
 
 package de.kuschku.libquassel.protocol.variant
 
-import java.util.*
-
 enum class QtType(val id: kotlin.Int) {
-  Void(0),//, VoidSerializer),
-  Bool(1),//, BoolSerializer),
-  Int(2),//, IntSerializer),
-  UInt(3),//, UIntSerializer),
+  Void(0),
+  Bool(1),
+  Int(2),
+  UInt(3),
   LongLong(4),
   ULongLong(5),
 
   Double(6),
-  QChar(7),//, CharSerializer),
-  QVariantMap(8),//, VariantMapSerializer),
-  QVariantList(9),//, VariantListSerializer),
+  QChar(7),
+  QVariantMap(8),
+  QVariantList(9),
 
-  QString(10),//, StringSerializer.UTF16),
-  QStringList(11),//, StringListSerializer),
-  QByteArray(12),//, ByteArraySerializer),
+  QString(10),
+  QStringList(11),
+  QByteArray(12),
 
   QBitArray(13),
   QDate(14),
-  QTime(15),//, TimeSerializer),
-  QDateTime(16),//, DateTimeSerializer),
+  QTime(15),
+  QDateTime(16),
   QUrl(17),
 
   QLocale(18),
@@ -91,26 +89,22 @@ enum class QtType(val id: kotlin.Int) {
   QQuaternion(86),
 
   VoidStar(128),
-  Long(129),//, LongSerializer),
-  Short(130),//, ShortSerializer),
-  Char(131),//, ByteSerializer),
-  ULong(132),//, ULongSerializer),
+  Long(129),
+  Short(130),
+  Char(131),
+  ULong(132),
 
-  UShort(133),//, UShortSerializer),
-  UChar(134),//, UByteSerializer),
+  UShort(133),
+  UChar(134),
   Float(135),
   QObjectStar(136),
   QWidgetStar(137),
 
-  QVariant(138),//, VariantSerializer),
+  QVariant(138),
 
   User(256),
   UserType(127);
 
-  val serializableName =
-    if (name.startsWith("Q")) name
-    else name.toLowerCase(Locale.ENGLISH)
-
   companion object {
     private val values = values().associateBy(QtType::id)
     fun of(id: kotlin.Int): QtType? = values[id]
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/features/FeatureSetTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/features/FeatureSetTest.kt
new file mode 100644
index 000000000..90088d3bf
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/features/FeatureSetTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.features
+
+import de.kuschku.bitflags.none
+import de.kuschku.bitflags.of
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class FeatureSetTest {
+  @Test
+  fun testParse() {
+    assertEquals(
+      emptyList<QuasselFeatureName>(),
+      FeatureSet.parse(
+        LegacyFeature.none(),
+        emptyList()
+      ).featureList()
+    )
+
+    assertEquals(
+      listOf(
+        QuasselFeature.SynchronizedMarkerLine.feature,
+        QuasselFeature.ExtendedFeatures.feature,
+        QuasselFeatureName("_unknownFeature")
+      ),
+      FeatureSet.parse(
+        LegacyFeature.of(
+          LegacyFeature.SynchronizedMarkerLine
+        ),
+        listOf(
+          QuasselFeature.ExtendedFeatures.feature,
+          QuasselFeatureName("_unknownFeature")
+        )
+      ).featureList()
+    )
+  }
+
+  @Test
+  fun testBuild() {
+    assertEquals(
+      emptyList<QuasselFeatureName>(),
+      FeatureSet.build(emptySet()).featureList()
+    )
+
+    assertEquals(
+      listOf(
+        QuasselFeature.SynchronizedMarkerLine.feature,
+        QuasselFeature.ExtendedFeatures.feature
+      ),
+      FeatureSet.build(setOf(
+        QuasselFeature.SynchronizedMarkerLine,
+        QuasselFeature.ExtendedFeatures
+      )).featureList()
+    )
+  }
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/ChainedByteBufferTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/ChainedByteBufferTest.kt
new file mode 100644
index 000000000..b76e10163
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/ChainedByteBufferTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.io
+
+import de.kuschku.libquassel.protocol.testutil.matchers.ByteBufferMatcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.jupiter.api.Assertions.assertDoesNotThrow
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.lang.AssertionError
+import java.nio.ByteBuffer
+
+class ChainedByteBufferTest {
+
+  @Test
+  fun testPutArray() {
+    validateArray(byteArrayOf())
+    validateArray(byteArrayOf(0x00))
+    validateArray(byteArrayOf(0x01))
+    validateArray(byteArrayOf(0xFF.toByte()))
+    validateArray(byteArrayOf(
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+      0x00, 0x01, -0x01, 0x00, 0x00, 0x01, -0x01, 0x00,
+    ))
+    validateArray(ByteArray(3000, Int::toByte))
+  }
+
+  @Test
+  fun testLimit() {
+    assertThrows<IllegalArgumentException>(
+      "Can not allocate 10 bytes, currently at 50, limit is 50"
+    ) {
+      ChainedByteBuffer(chunkSize = 10, limit = 50)
+        .put(ByteArray(70, Int::toByte))
+    }
+    assertDoesNotThrow {
+      ChainedByteBuffer(chunkSize = 10, limit = 70)
+        .put(ByteArray(70, Int::toByte))
+    }
+    assertDoesNotThrow {
+      ChainedByteBuffer(chunkSize = 10, limit = 70)
+        .put(ByteArray(50, Int::toByte))
+    }
+    assertDoesNotThrow {
+      ChainedByteBuffer(chunkSize = 10, limit = -1)
+        .put(ByteArray(50, Int::toByte))
+    }
+    assertDoesNotThrow {
+      ChainedByteBuffer(chunkSize = 10)
+        .put(ByteArray(50, Int::toByte))
+    }
+  }
+
+  @Test
+  fun testClear() {
+    val chained = ChainedByteBuffer(limit = 16384)
+    val array = ByteArray(3000, Int::toByte)
+    chained.put(array)
+    assertEquals(array.size, chained.size)
+    assertEquals(array.size, chained.toBuffer().remaining())
+    assertThat(chained.toBuffer(), ByteBufferMatcher(ByteBuffer.wrap(array)))
+    chained.clear()
+    assertEquals(0, chained.size)
+    assertEquals(0, chained.toBuffer().remaining())
+    assertThat(chained.toBuffer(), ByteBufferMatcher(ByteBuffer.allocate(0)))
+  }
+
+  private fun validateArray(array: ByteArray) {
+    fun validateArrayInternal(array: ByteArray, direct: Boolean) {
+      val bufferSize = 1024
+      val chained = ChainedByteBuffer(chunkSize = bufferSize, direct = direct, limit = 16384)
+      chained.put(array)
+      assertEquals(array.size, chained.size)
+      assertEquals(array.size, chained.toBuffer().remaining())
+      assertThat(chained.toBuffer(), ByteBufferMatcher(ByteBuffer.wrap(array)))
+      if (array.size < bufferSize && array.isNotEmpty()) {
+        assertEquals(array.size, chained.firstOrNull()?.remaining())
+        assertThat(chained.firstOrNull(), ByteBufferMatcher(ByteBuffer.wrap(array)))
+        assertEquals(1, chained.take(2).count())
+      }
+    }
+
+    validateArrayInternal(array, direct = true)
+    validateArrayInternal(array, direct = false)
+  }
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/StringEncoderTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/StringEncoderTest.kt
new file mode 100644
index 000000000..2b356d8bd
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/io/StringEncoderTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.io
+
+import de.kuschku.libquassel.protocol.testutil.byteBufferOf
+import de.kuschku.libquassel.protocol.testutil.matchers.ByteBufferMatcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.nio.ByteBuffer
+
+class StringEncoderTest {
+  private val ascii = StringEncoder(Charsets.ISO_8859_1)
+  private val utf8 = StringEncoder(Charsets.UTF_8)
+  private val utf16 = StringEncoder(Charsets.UTF_16BE)
+
+  @Test
+  fun testNullString() {
+    assertThat(
+      ascii.encode(null),
+      ByteBufferMatcher(ByteBuffer.allocate(0))
+    )
+    assertThat(
+      utf8.encode(null),
+      ByteBufferMatcher(ByteBuffer.allocate(0))
+    )
+    assertThat(
+      utf16.encode(null),
+      ByteBufferMatcher(ByteBuffer.allocate(0))
+    )
+  }
+
+  @Test
+  fun testNullChar() {
+    assertEquals(
+      1,
+      ascii.encodeChar(null).remaining()
+    )
+    assertThat(
+      ascii.encodeChar(null),
+      ByteBufferMatcher(byteBufferOf(0))
+    )
+
+    assertEquals(
+      1,
+      utf8.encodeChar(null).remaining()
+    )
+    assertThat(
+      utf8.encodeChar(null),
+      ByteBufferMatcher(byteBufferOf(0))
+    )
+
+    assertEquals(
+      2,
+      utf16.encodeChar(null).remaining()
+    )
+    assertThat(
+      utf16.encodeChar(null),
+      ByteBufferMatcher(byteBufferOf(0, 0)),
+    )
+  }
+
+  @Test
+  fun testUnencodableChar() {
+    assertEquals(
+      2,
+      ascii.encodeChar('\uFFFF').remaining()
+    )
+    assertThat(
+      ascii.encodeChar('\uFFFF'),
+      ByteBufferMatcher(byteBufferOf(0, 0))
+    )
+  }
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializerTest.kt
index e109746fc..dfaa87f44 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/ByteBufferSerializerTest.kt
@@ -22,6 +22,7 @@ import de.kuschku.libquassel.protocol.testutil.byteBufferOf
 import de.kuschku.libquassel.protocol.testutil.matchers.ByteBufferMatcher
 import de.kuschku.libquassel.protocol.testutil.qtSerializerTest
 import org.junit.jupiter.api.Test
+import java.nio.ByteBuffer
 
 class ByteBufferSerializerTest {
   @Test
@@ -39,4 +40,30 @@ class ByteBufferSerializerTest {
     byteBufferOf(0, 0, 0, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9),
     ::ByteBufferMatcher
   )
+
+  @Test
+  fun testEmpty() = qtSerializerTest(
+    ByteBufferSerializer,
+    ByteBuffer.allocate(0),
+    byteBufferOf(0, 0, 0, 0),
+    ::ByteBufferMatcher
+  )
+
+  @Test
+  fun testNull() {
+    qtSerializerTest(
+      ByteBufferSerializer,
+      null,
+      byteBufferOf(0, 0, 0, 0),
+      ::ByteBufferMatcher,
+      serializeFeatureSet = null
+    )
+
+    qtSerializerTest(
+      ByteBufferSerializer,
+      null,
+      byteBufferOf(-1, -1, -1, -1),
+      ::ByteBufferMatcher
+    )
+  }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/DateTimeSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/DateTimeSerializerTest.kt
index 1f2352a43..f2291dbf5 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/DateTimeSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/DateTimeSerializerTest.kt
@@ -90,7 +90,7 @@ class DateTimeSerializerTest {
     DateTimeSerializer,
     LocalDateTime
       .of(2019, Month.JANUARY, 15, 20, 25),
-    byteBufferOf(0x00u, 0x25u, 0x83u, 0x83u, 0x04u, 0x61u, 0x85u, 0x60u, 0x07u),
+    byteBufferOf(0x00u, 0x25u, 0x83u, 0x83u, 0x04u, 0x61u, 0x85u, 0x60u, 0xFFu),
     matcher = ::TemporalMatcher
   )
 
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/MessageSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/MessageSerializerTest.kt
index f07ad5f6c..0c5328670 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/MessageSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/MessageSerializerTest.kt
@@ -80,7 +80,8 @@ class MessageSerializerTest {
       // Content
       0xFFu, 0xFFu, 0xFFu, 0xFFu,
     ),
-    deserializeFeatureSet = FeatureSet.all()
+    deserializeFeatureSet = FeatureSet.all(),
+    serializeFeatureSet = null
   )
 
   @Test
@@ -105,7 +106,8 @@ class MessageSerializerTest {
       ""
     ),
     byteBufferOf(-1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
-    deserializeFeatureSet = FeatureSet.none()
+    deserializeFeatureSet = FeatureSet.none(),
+    serializeFeatureSet = FeatureSet.none()
   )
 
   @Test
@@ -130,7 +132,8 @@ class MessageSerializerTest {
       "äẞ\u0000\uFFFF"
     ),
     byteBufferOf(127, -1, -1, -1, 90, -33, -109, -106, 0, 7, -1, -1, -113, 127, -1, -1, -1, 127, -1, -1, -1, 0, 15, 127, -1, -1, -1, 0, 0, 0, 9, -61, -92, -31, -70, -98, 0, -17, -65, -65, 0, 0, 0, 9, -61, -92, -31, -70, -98, 0, -17, -65, -65, 0, 0, 0, 9, -61, -92, -31, -70, -98, 0, -17, -65, -65),
-    deserializeFeatureSet = FeatureSet.none()
+    deserializeFeatureSet = FeatureSet.none(),
+    serializeFeatureSet = FeatureSet.none()
   )
 
   @Test
@@ -157,5 +160,6 @@ class MessageSerializerTest {
     byteBufferOf(0x7Fu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0x00u, 0x00u, 0x13u, 0x87u, 0xFFu, 0xFFu, 0xD8u, 0xF0u, 0x00u, 0x07u, 0xFFu, 0xFFu, 0x8Fu, 0x7Fu, 0xFFu, 0xFFu, 0xFFu, 0x7Fu, 0xFFu, 0xFFu, 0xFFu, 0x00u, 0x0Fu, 0x7Fu, 0xFFu, 0xFFu, 0xFFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu, 0x00u, 0x00u, 0x00u, 0x09u, 0xC3u, 0xA4u, 0xE1u, 0xBAu, 0x9Eu, 0x00u, 0xEFu, 0xBFu, 0xBFu),
     featureSets = listOf(FeatureSet.all()),
     deserializeFeatureSet = FeatureSet.all(),
+    serializeFeatureSet = FeatureSet.all()
   )
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializerTest.kt
index c95da6fd0..411635ccf 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QCharSerializerTest.kt
@@ -44,7 +44,7 @@ class QCharSerializerTest {
   fun testBOM1() = qtSerializerTest(
     QCharSerializer,
     '\uFFFE',
-    byteBufferOf(-2, -1),
+    byteBufferOf(-1, -2),
     ::BomMatcherChar,
   )
 
@@ -52,7 +52,7 @@ class QCharSerializerTest {
   fun testBOM2() = qtSerializerTest(
     QCharSerializer,
     '\uFEFF',
-    byteBufferOf(-1, -2),
+    byteBufferOf(-2, -1),
     ::BomMatcherChar,
   )
 
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QVariantMapSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QVariantMapSerializerTest.kt
new file mode 100644
index 000000000..bc38705c6
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/QVariantMapSerializerTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2020 Janne Mareike Koschinski
+ * Copyright (c) 2020 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.kuschku.libquassel.protocol.serializers.primitive
+
+import de.kuschku.libquassel.protocol.testutil.byteBufferOf
+import de.kuschku.libquassel.protocol.testutil.matchers.MapMatcher
+import de.kuschku.libquassel.protocol.testutil.qtSerializerTest
+import de.kuschku.libquassel.protocol.variant.QtType
+import de.kuschku.libquassel.protocol.variant.qVariant
+import org.junit.jupiter.api.Test
+
+class QVariantMapSerializerTest {
+  @Test
+  fun testEmpty() = qtSerializerTest(
+    QVariantMapSerializer,
+    mapOf(),
+    byteBufferOf(0, 0, 0, 0)
+  )
+
+  @Test
+  fun testNormal() = qtSerializerTest(
+    QVariantMapSerializer,
+    mapOf(
+      "Username" to qVariant("AzureDiamond", QtType.QString),
+      "Password" to qVariant("hunter2", QtType.QString)
+    ),
+    byteBufferOf(0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x55, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x6E, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x41, 0x00, 0x7A, 0x00, 0x75, 0x00, 0x72, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x64, 0x00, 0x00, 0x00, 0x10, 0x00, 0x50, 0x00, 0x61, 0x00, 0x73, 0x00, 0x73, 0x00, 0x77, 0x00, 0x6F, 0x00, 0x72, 0x00, 0x64, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x68, 0x00, 0x75, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x32),
+    ::MapMatcher
+  )
+
+  @Test
+  fun testNullKey() = qtSerializerTest(
+    QVariantMapSerializer,
+    mapOf(
+      "" to qVariant<String?>(null, QtType.QString)
+    ),
+    byteBufferOf(0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x0Au, 0x00u, 0xFFu, 0xFFu, 0xFFu, 0xFFu),
+    ::MapMatcher
+  )
+}
+
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/StringSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/StringSerializerTest.kt
index fe16a2e22..01c8ace78 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/StringSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/serializers/primitive/StringSerializerTest.kt
@@ -24,6 +24,7 @@ import de.kuschku.libquassel.protocol.testutil.matchers.BomMatcherString
 import de.kuschku.libquassel.protocol.testutil.matchers.ByteBufferMatcher
 import de.kuschku.libquassel.protocol.testutil.testQtSerializerDirect
 import de.kuschku.libquassel.protocol.testutil.testQtSerializerVariant
+import org.hamcrest.MatcherAssert.assertThat
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
 
@@ -35,6 +36,17 @@ class StringSerializerTest {
       if (!it.startsWith('#')) {
         testQtSerializerDirect(StringSerializerUtf8, it, matcher = BomMatcherString(it))
         testQtSerializerDirect(StringSerializerUtf16, it, matcher = BomMatcherString(it))
+        testQtSerializerVariant(StringSerializerUtf16, it, matcher = BomMatcherString(it))
+
+        val bufferUtf8 = StringSerializerUtf8.serializeRaw(it)
+        testQtSerializerDirect(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
+        testQtSerializerVariant(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
+        assertEquals(it, StringSerializerUtf8.deserializeRaw(bufferUtf8.rewind()))
+
+        val bufferUtf16 = StringSerializerUtf16.serializeRaw(it)
+        testQtSerializerDirect(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
+        testQtSerializerVariant(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
+        assertThat(StringSerializerUtf16.deserializeRaw(bufferUtf16.rewind()), BomMatcherString(it))
       }
     }
   }
@@ -47,26 +59,38 @@ class StringSerializerTest {
       DAS KOMPUTERMASCHINE IST NICHT FÜR DER GEFINGERPOKEN UND MITTENGRABEN! ODERWISE IST EASY TO SCHNAPPEN DER SPRINGENWERK, BLOWENFUSEN UND POPPENCORKEN MIT SPITZENSPARKEN.
       IST NICHT FÜR GEWERKEN BEI DUMMKOPFEN. DER RUBBERNECKEN SIGHTSEEREN KEEPEN DAS COTTONPICKEN HÄNDER IN DAS POCKETS MUSS.
       ZO RELAXEN UND WATSCHEN DER BLINKENLICHTEN.
+      : ACHTUNG!
+      ALLES TURISTEN UND NONTEKNISCHEN LOOKENPEEPERS!
+      DAS KOMPUTERMASCHINE IST NICHT FÜR DER GEFINGERPOKEN UND MITTENGRABEN! ODERWISE IST EASY TO SCHNAPPEN DER SPRINGENWERK, BLOWENFUSEN UND POPPENCORKEN MIT SPITZENSPARKEN.
+      IST NICHT FÜR GEWERKEN BEI DUMMKOPFEN. DER RUBBERNECKEN SIGHTSEEREN KEEPEN DAS COTTONPICKEN HÄNDER IN DAS POCKETS MUSS.
+      ZO RELAXEN UND WATSCHEN DER BLINKENLICHTEN.
     """.trimIndent()
 
-    testQtSerializerDirect(StringSerializerAscii, data)
+    testQtSerializerDirect(StringSerializerAscii, data, matcher = BomMatcherString(data))
 
     val bufferAscii = StringSerializerAscii.serializeRaw(data)
     testQtSerializerDirect(ByteBufferSerializer, bufferAscii, matcher = ByteBufferMatcher(bufferAscii))
     testQtSerializerVariant(ByteBufferSerializer, bufferAscii, matcher = ByteBufferMatcher(bufferAscii))
+    assertEquals(data, StringSerializerAscii.deserializeRaw(bufferAscii.rewind()))
 
-    testQtSerializerDirect(StringSerializerUtf8, data)
-
-    val bufferUtf8 = StringSerializerUtf8.serializeRaw(data)
-    testQtSerializerDirect(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
-    testQtSerializerVariant(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
+    testUtf(data)
+  }
 
+  @Test
+  fun testRoundtripEnglishUtf16() {
+    val data = """
+      : ACHTUNG!
+      ALLES TURISTEN UND NONTEKNISCHEN LOOKENPEEPERS!
+      DAS KOMPUTERMASCHINE IST NICHT FÜR DER GEFINGERPOKEN UND MITTENGRABEN! ODERWISE IST EASY TO SCHNAPPEN DER SPRINGENWERK, BLOWENFUSEN UND POPPENCORKEN MIT SPITZENSPARKEN.
+      IST NICHT FÜR GEWERKEN BEI DUMMKOPFEN. DER RUBBERNECKEN SIGHTSEEREN KEEPEN DAS COTTONPICKEN HÄNDER IN DAS POCKETS MUSS.
+      ZO RELAXEN UND WATSCHEN DER BLINKENLICHTEN.
+      : ACHTUNG!
+      ALLES TURISTEN UND NONTEKNISCHEN LOOKENPEEPERS!
+      DAS KOMPUTERMASCHINE IST NICHT FÜR DER GEFINGERPOKEN UND MITTENGRABEN! ODERWISE IST EASY TO SCHNAPPEN DER SPRINGENWERK, BLOWENFUSEN UND POPPENCORKEN MIT SPITZENSPARKEN.
+      IST NICHT FÜR GEWERKEN BEI DUMMKOPFEN. DER RUBBERNECKEN SIGHTSEEREN KEEPEN DAS COTTONPICKEN HÄNDER IN DAS POCKETS MUSS.
+      ZO RELAXEN UND WATSCHEN DER BLINKENLICHTEN.
+    """.trimIndent()
     testQtSerializerDirect(StringSerializerUtf16, data)
-    testQtSerializerVariant(StringSerializerUtf16, data)
-
-    val bufferUtf16 = StringSerializerUtf16.serializeRaw(data)
-    testQtSerializerDirect(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
-    testQtSerializerVariant(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
   }
 
   @Test
@@ -97,4 +121,21 @@ class StringSerializerTest {
     testQtSerializerVariant(ByteBufferSerializer, bufferAscii, matcher = ByteBufferMatcher(bufferAscii))
     assertEquals(data, StringSerializerAscii.deserializeRaw(bufferAscii.rewind()))
   }
+
+  private fun testUtf(data: String) {
+    testQtSerializerDirect(StringSerializerUtf8, data, matcher = BomMatcherString(data))
+
+    val bufferUtf8 = StringSerializerUtf8.serializeRaw(data)
+    testQtSerializerDirect(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
+    testQtSerializerVariant(ByteBufferSerializer, bufferUtf8, matcher = ByteBufferMatcher(bufferUtf8))
+    assertEquals(data, StringSerializerUtf8.deserializeRaw(bufferUtf8.rewind()))
+
+    //testQtSerializerDirect(StringSerializerUtf16, data, matcher = BomMatcherString(data))
+    //testQtSerializerVariant(StringSerializerUtf16, data, matcher = BomMatcherString(data))
+
+    //val bufferUtf16 = StringSerializerUtf16.serializeRaw(data)
+    //testQtSerializerDirect(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
+    //testQtSerializerVariant(ByteBufferSerializer, bufferUtf16, matcher = ByteBufferMatcher(bufferUtf16))
+    //assertEquals(data, StringSerializerUtf16.deserializeRaw(bufferUtf16.rewind()))
+  }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/handshakeSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/handshakeSerializerTest.kt
index 65941fda6..2139c07a5 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/handshakeSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/handshakeSerializerTest.kt
@@ -29,17 +29,23 @@ fun <T> handshakeSerializerTest(
   encoded: ByteBuffer? = null,
   matcher: ((T) -> Matcher<T>)? = null,
   featureSets: List<FeatureSet> = listOf(FeatureSet.none(), FeatureSet.all()),
-  deserializeFeatureSet: FeatureSet = FeatureSet.all(),
+  deserializeFeatureSet: FeatureSet? = FeatureSet.all(),
+  serializeFeatureSet: FeatureSet? = FeatureSet.all(),
 ) {
+  if (encoded != null) {
+    if (deserializeFeatureSet != null) {
+      if (matcher != null) {
+        testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
+      } else {
+        testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
+      }
+    }
+    if (serializeFeatureSet != null) {
+      testSerialize(serializer, value, encoded.rewind(), serializeFeatureSet)
+    }
+  }
   for (featureSet in featureSets) {
     testHandshakeSerializerDirect(serializer, value)
     testHandshakeSerializerEncoded(serializer, value, featureSet)
   }
-  if (encoded != null) {
-    if (matcher != null) {
-      testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
-    } else {
-      testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
-    }
-  }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/BomMatcherChar.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/BomMatcherChar.kt
index 511c69c99..e128e8f68 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/BomMatcherChar.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/BomMatcherChar.kt
@@ -23,7 +23,7 @@ import org.hamcrest.Description
 
 class BomMatcherChar(private val expected: Char) : BaseMatcher<Char>() {
   private val malformed = charArrayOf(
-    '￾', ''
+    '\uFFFE', '\uFEFF', '\uFFFD', ''
   )
 
   override fun describeTo(description: Description?) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/ByteBufferMatcher.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/ByteBufferMatcher.kt
index c295170f8..69ab7faf8 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/ByteBufferMatcher.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/ByteBufferMatcher.kt
@@ -19,9 +19,11 @@
 package de.kuschku.libquassel.protocol.testutil.matchers
 
 import de.kuschku.libquassel.protocol.io.contentToString
+import de.kuschku.libquassel.protocol.io.isEmpty
 import org.hamcrest.BaseMatcher
 import org.hamcrest.Description
 import java.nio.ByteBuffer
+import kotlin.math.exp
 
 class ByteBufferMatcher(buffer: ByteBuffer?) : BaseMatcher<ByteBuffer>() {
   private val expected = buffer?.let { original ->
@@ -43,13 +45,12 @@ class ByteBufferMatcher(buffer: ByteBuffer?) : BaseMatcher<ByteBuffer>() {
   }
 
   override fun matches(item: Any?): Boolean {
-    return if (item is ByteBuffer && expected is ByteBuffer) {
-      val expected = expected.rewind().contentToString()
-      val actual = item.rewind().contentToString()
+    val actual = item as? ByteBuffer
 
-      expected == actual
-    } else {
-      false
+    if (actual.isEmpty() && expected.isEmpty()) {
+      return true
     }
+
+    return actual?.rewind()?.contentToString() == expected?.rewind()?.contentToString()
   }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/MapMatcher.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/MapMatcher.kt
new file mode 100644
index 000000000..4ca0dc72e
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/matchers/MapMatcher.kt
@@ -0,0 +1,62 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.testutil.matchers
+
+import org.hamcrest.BaseMatcher
+import org.hamcrest.Description
+
+class MapMatcher<K, V>(
+  private val expected: Map<K, V>
+) : BaseMatcher<Map<K, V>>() {
+  override fun describeTo(description: Description?) {
+    description?.appendText(expected.toString())
+  }
+
+  override fun describeMismatch(item: Any?, description: Description?) {
+    if (item is Map<*, *>) {
+      for (key in expected.keys) {
+        if (!item.containsKey(key)) {
+          description?.appendText(" did not have key $key")
+        }
+        if (expected[key] != item[key]) {
+          description?.appendText(" key $key was: ${item[key]} instead of ${expected[key]}")
+        }
+      }
+    } else {
+      description?.appendText("was: $item")
+    }
+  }
+
+  override fun matches(item: Any?): Boolean {
+    if (item is Map<*, *>) {
+      for (key in expected.keys) {
+        if (!item.containsKey(key)) {
+          return false
+        }
+        if (expected[key] != item[key]) {
+          return false
+        }
+      }
+      return true
+    } else {
+      return false
+    }
+  }
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/qtSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/qtSerializerTest.kt
index f830f90d9..80b11b526 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/qtSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/qtSerializerTest.kt
@@ -23,23 +23,29 @@ import de.kuschku.libquassel.protocol.serializers.primitive.QtSerializer
 import org.hamcrest.Matcher
 import java.nio.ByteBuffer
 
-fun <T> qtSerializerTest(
+fun <T : Any?> qtSerializerTest(
   serializer: QtSerializer<T>,
   value: T,
   encoded: ByteBuffer? = null,
   matcher: ((T) -> Matcher<T>)? = null,
   featureSets: List<FeatureSet> = listOf(FeatureSet.none(), FeatureSet.all()),
-  deserializeFeatureSet: FeatureSet = FeatureSet.all(),
+  deserializeFeatureSet: FeatureSet? = FeatureSet.all(),
+  serializeFeatureSet: FeatureSet? = FeatureSet.all(),
 ) {
+  if (encoded != null) {
+    if (deserializeFeatureSet != null) {
+      if (matcher != null) {
+        testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
+      } else {
+        testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
+      }
+    }
+    if (serializeFeatureSet != null) {
+      testSerialize(serializer, value, encoded.rewind(), serializeFeatureSet)
+    }
+  }
   for (featureSet in featureSets) {
     testQtSerializerDirect(serializer, value, featureSet, matcher?.invoke(value))
     testQtSerializerVariant(serializer, value, featureSet, matcher?.invoke(value))
   }
-  if (encoded != null) {
-    if (matcher != null) {
-      testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
-    } else {
-      testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
-    }
-  }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/quasselSerializerTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/quasselSerializerTest.kt
index 1d3654472..eae1a5751 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/quasselSerializerTest.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/quasselSerializerTest.kt
@@ -29,17 +29,23 @@ fun <T> quasselSerializerTest(
   encoded: ByteBuffer? = null,
   matcher: ((T) -> Matcher<T>)? = null,
   featureSets: List<FeatureSet> = listOf(FeatureSet.none(), FeatureSet.all()),
-  deserializeFeatureSet: FeatureSet = FeatureSet.all(),
+  deserializeFeatureSet: FeatureSet? = FeatureSet.all(),
+  serializeFeatureSet: FeatureSet? = FeatureSet.all(),
 ) {
+  if (encoded != null) {
+    if (deserializeFeatureSet != null) {
+      if (matcher != null) {
+        testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
+      } else {
+        testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
+      }
+    }
+    if (serializeFeatureSet != null) {
+      testSerialize(serializer, value, encoded.rewind(), serializeFeatureSet)
+    }
+  }
   for (featureSet in featureSets) {
     testQuasselSerializerDirect(serializer, value, featureSet, matcher?.invoke(value))
     testQuasselSerializerVariant(serializer, value, featureSet, matcher?.invoke(value))
   }
-  if (encoded != null) {
-    if (matcher != null) {
-      testDeserialize(serializer, matcher(value), encoded.rewind(), deserializeFeatureSet)
-    } else {
-      testDeserialize(serializer, value, encoded.rewind(), deserializeFeatureSet)
-    }
-  }
 }
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/serialize.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/serialize.kt
new file mode 100644
index 000000000..fca9603c9
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/serialize.kt
@@ -0,0 +1,61 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.kuschku.libquassel.protocol.testutil
+
+import de.kuschku.libquassel.protocol.features.FeatureSet
+import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
+import de.kuschku.libquassel.protocol.serializers.handshake.HandshakeSerializer
+import de.kuschku.libquassel.protocol.serializers.primitive.HandshakeMapSerializer
+import de.kuschku.libquassel.protocol.serializers.primitive.QtSerializer
+import de.kuschku.libquassel.protocol.testutil.matchers.ByteBufferMatcher
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.jupiter.api.Assertions.assertEquals
+import java.nio.ByteBuffer
+
+fun <T> serialize(
+  serializer: QtSerializer<T>,
+  data: T,
+  featureSet: FeatureSet = FeatureSet.all()
+): ByteBuffer {
+  val buffer = ChainedByteBuffer()
+  serializer.serialize(buffer, data, featureSet)
+  return buffer.toBuffer()
+}
+
+fun <T> testSerialize(
+  serializer: QtSerializer<T>,
+  data: T,
+  buffer: ByteBuffer,
+  featureSet: FeatureSet = FeatureSet.all()
+) {
+  val after = serialize(serializer, data, featureSet)
+  assertThat(after, ByteBufferMatcher(buffer))
+}
+
+fun <T> testSerialize(
+  serializer: HandshakeSerializer<T>,
+  data: T,
+  buffer: ByteBuffer,
+  featureSet: FeatureSet = FeatureSet.all()
+) {
+  val map = serializer.serialize(data)
+  val after = serialize(HandshakeMapSerializer, map, featureSet)
+  assertThat(after, ByteBufferMatcher(buffer))
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testHandshakeSerializerEncoded.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testHandshakeSerializerEncoded.kt
index 5b4699eb5..b511b7705 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testHandshakeSerializerEncoded.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testHandshakeSerializerEncoded.kt
@@ -20,7 +20,6 @@ package de.kuschku.libquassel.protocol.testutil
 
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
 import de.kuschku.libquassel.protocol.serializers.handshake.HandshakeSerializer
 import de.kuschku.libquassel.protocol.serializers.primitive.HandshakeMapSerializer
 import org.hamcrest.Matcher
@@ -33,10 +32,9 @@ fun <T> testHandshakeSerializerEncoded(
   featureSet: FeatureSet = FeatureSet.all(),
   matcher: Matcher<T>? = null
 ) {
-  val buffer = ChainedByteBuffer()
+  val buffer = ChainedByteBuffer(limit = 16384)
   HandshakeMapSerializer.serialize(buffer, serializer.serialize(data), featureSet)
   val result = buffer.toBuffer()
-  result.print()
   val after = serializer.deserialize(HandshakeMapSerializer.deserialize(result, featureSet))
   assertEquals(0, result.remaining())
   if (matcher != null) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerDirect.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerDirect.kt
index 6f6967dcb..a07f194fe 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerDirect.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerDirect.kt
@@ -20,7 +20,7 @@ package de.kuschku.libquassel.protocol.testutil
 
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
+import de.kuschku.libquassel.protocol.io.contentToString
 import de.kuschku.libquassel.protocol.serializers.primitive.QtSerializer
 import org.hamcrest.Matcher
 import org.hamcrest.MatcherAssert.assertThat
@@ -32,10 +32,10 @@ fun <T> testQtSerializerDirect(
   featureSet: FeatureSet = FeatureSet.all(),
   matcher: Matcher<T>? = null
 ) {
-  val buffer = ChainedByteBuffer()
+  val buffer = ChainedByteBuffer(limit = 16384)
   serializer.serialize(buffer, data, featureSet)
   val result = buffer.toBuffer()
-  result.print()
+  println(result.contentToString())
   val after = serializer.deserialize(result, featureSet)
   assertEquals(0, result.remaining())
   if (matcher != null) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerVariant.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerVariant.kt
index ea5bb24bc..9ddb5bfd6 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerVariant.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQtSerializerVariant.kt
@@ -20,7 +20,6 @@ package de.kuschku.libquassel.protocol.testutil
 
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
 import de.kuschku.libquassel.protocol.serializers.primitive.QVariantSerializer
 import de.kuschku.libquassel.protocol.serializers.primitive.QtSerializer
 import de.kuschku.libquassel.protocol.variant.QVariant
@@ -34,10 +33,9 @@ fun <T> testQtSerializerVariant(
   featureSet: FeatureSet = FeatureSet.all(),
   matcher: Matcher<in T>? = null
 ) {
-  val buffer = ChainedByteBuffer()
+  val buffer = ChainedByteBuffer(limit = 16384)
   QVariantSerializer.serialize(buffer, QVariant.of(data, serializer), featureSet)
   val result = buffer.toBuffer()
-  result.print()
   val after = QVariantSerializer.deserialize(result, featureSet)
   assertEquals(0, result.remaining())
   if (matcher != null) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerDirect.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerDirect.kt
index aa7e70834..18aa22057 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerDirect.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerDirect.kt
@@ -20,7 +20,6 @@ package de.kuschku.libquassel.protocol.testutil
 
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
 import de.kuschku.libquassel.protocol.serializers.primitive.QuasselSerializer
 import org.hamcrest.Matcher
 import org.hamcrest.MatcherAssert.assertThat
@@ -32,11 +31,9 @@ fun <T> testQuasselSerializerDirect(
   featureSet: FeatureSet = FeatureSet.all(),
   matcher: Matcher<T>? = null
 ) {
-  val buffer = ChainedByteBuffer()
+  val buffer = ChainedByteBuffer(limit = 16384)
   serializer.serialize(buffer, data, featureSet)
   val result = buffer.toBuffer()
-  println("direct")
-  result.print()
   val after = serializer.deserialize(result, featureSet)
   assertEquals(0, result.remaining())
   if (matcher != null) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerVariant.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerVariant.kt
index fff25c724..1cbff786c 100644
--- a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerVariant.kt
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/testutil/testQuasselSerializerVariant.kt
@@ -20,7 +20,6 @@ package de.kuschku.libquassel.protocol.testutil
 
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
-import de.kuschku.libquassel.protocol.io.print
 import de.kuschku.libquassel.protocol.serializers.primitive.QVariantSerializer
 import de.kuschku.libquassel.protocol.serializers.primitive.QuasselSerializer
 import de.kuschku.libquassel.protocol.variant.QVariant
@@ -34,10 +33,9 @@ fun <T> testQuasselSerializerVariant(
   featureSet: FeatureSet = FeatureSet.all(),
   matcher: Matcher<in T>? = null
 ) {
-  val buffer = ChainedByteBuffer()
+  val buffer = ChainedByteBuffer(limit = 16384)
   QVariantSerializer.serialize(buffer, QVariant.of(data, serializer), featureSet)
   val result = buffer.toBuffer()
-  result.print()
   val after = QVariantSerializer.deserialize(result, featureSet)
   assertEquals(0, result.remaining())
   if (matcher != null) {
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/types/SignedIdTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/types/SignedIdTest.kt
new file mode 100644
index 000000000..c3520885e
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/types/SignedIdTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.types
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+class SignedIdTest {
+  @Test
+  fun testNegativeOne() {
+    assertFalse(BufferId(-1).isValid())
+    assertFalse(IdentityId(-1).isValid())
+    assertFalse(MsgId(-1).isValid())
+    assertFalse(NetworkId(-1).isValid())
+  }
+
+  @Test
+  fun testZero() {
+    assertFalse(BufferId(0).isValid())
+    assertFalse(IdentityId(0).isValid())
+    assertFalse(MsgId(0).isValid())
+    assertFalse(NetworkId(0).isValid())
+  }
+
+  @Test
+  fun testMinimal() {
+    assertFalse(BufferId(Int.MIN_VALUE).isValid())
+    assertFalse(IdentityId(Int.MIN_VALUE).isValid())
+    assertFalse(MsgId(Long.MIN_VALUE).isValid())
+    assertFalse(NetworkId(Int.MIN_VALUE).isValid())
+  }
+
+  @Test
+  fun testMaximum() {
+    assertTrue(BufferId(Int.MAX_VALUE).isValid())
+    assertTrue(IdentityId(Int.MAX_VALUE).isValid())
+    assertTrue(MsgId(Long.MAX_VALUE).isValid())
+    assertTrue(NetworkId(Int.MAX_VALUE).isValid())
+  }
+
+  @Test
+  fun testSortOrder() {
+    assertEquals(
+      listOf(
+        BufferId(Int.MIN_VALUE),
+        BufferId(-1),
+        BufferId(0),
+        BufferId(Int.MAX_VALUE)
+      ),
+      listOf(
+        BufferId(Int.MAX_VALUE),
+        BufferId(Int.MIN_VALUE),
+        BufferId(0),
+        BufferId(-1)
+      ).sorted()
+    )
+
+    assertEquals(
+      listOf(
+        IdentityId(Int.MIN_VALUE),
+        IdentityId(-1),
+        IdentityId(0),
+        IdentityId(Int.MAX_VALUE)
+      ),
+      listOf(
+        IdentityId(Int.MAX_VALUE),
+        IdentityId(Int.MIN_VALUE),
+        IdentityId(0),
+        IdentityId(-1)
+      ).sorted()
+    )
+
+    assertEquals(
+      listOf(
+        MsgId(Long.MIN_VALUE),
+        MsgId(-1),
+        MsgId(0),
+        MsgId(Long.MAX_VALUE)
+      ),
+      listOf(
+        MsgId(Long.MAX_VALUE),
+        MsgId(Long.MIN_VALUE),
+        MsgId(0),
+        MsgId(-1)
+      ).sorted()
+    )
+
+    assertEquals(
+      listOf(
+        NetworkId(Int.MIN_VALUE),
+        NetworkId(-1),
+        NetworkId(0),
+        NetworkId(Int.MAX_VALUE)
+      ),
+      listOf(
+        NetworkId(Int.MAX_VALUE),
+        NetworkId(Int.MIN_VALUE),
+        NetworkId(0),
+        NetworkId(-1)
+      ).sorted()
+    )
+  }
+}
diff --git a/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/variant/QVariantTest.kt b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/variant/QVariantTest.kt
new file mode 100644
index 000000000..df0d34f82
--- /dev/null
+++ b/protocol/src/test/kotlin/de/kuschku/libquassel/protocol/variant/QVariantTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.libquassel.protocol.variant
+
+import de.kuschku.libquassel.protocol.testutil.byteBufferOf
+import de.kuschku.libquassel.protocol.variant.QtType
+import de.kuschku.libquassel.protocol.variant.qVariant
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+class QVariantTest {
+  @Test
+  fun testString() {
+    assertEquals(
+      "QVariant(ByteBufferSerializer, DEADBEEF)",
+      qVariant(
+        byteBufferOf(0xDEu, 0xADu, 0xBEu, 0xEFu),
+        QtType.QByteArray
+      ).toString()
+    )
+    assertEquals(
+      "QVariant(StringSerializerUtf16, DEADBEEF)",
+      qVariant(
+        "DEADBEEF",
+        QtType.QString
+      ).toString()
+    )
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e4af358e3..eef525f65 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,5 +23,6 @@ rootProject.buildFileName = "build.gradle.kts"
 include(
   ":app",
   ":bitflags",
-  ":protocol"
+  ":protocol",
+  ":coverage-annotations"
 )
-- 
GitLab