diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/rules/HighlightRule.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/rules/HighlightRule.kt
index 38f0a33ef68a2c7d927b13b5ba9bade7872b5f0f..4b6fadafb46331d60f093c0ee4889a73ecfb222d 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/rules/HighlightRule.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/rules/HighlightRule.kt
@@ -13,7 +13,7 @@ import de.justjanne.libquassel.protocol.util.expression.ExpressionMatch
 
 data class HighlightRule(
   val id: Int,
-  val contents: String,
+  val content: String,
   val isRegEx: Boolean = false,
   val isCaseSensitive: Boolean = false,
   val isEnabled: Boolean = true,
@@ -22,7 +22,7 @@ data class HighlightRule(
   val channel: String
 ) {
   val contentMatch = ExpressionMatch(
-    contents,
+    content,
     if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx
     else ExpressionMatch.MatchMode.MatchPhrase,
     isCaseSensitive
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt
index 394d9d26bdfd1996ef5cafc8c96d9e5b7c39a807..f32fdbb66960a8169b25d5f3dd85d7fd3d3131f1 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt
@@ -59,9 +59,6 @@ open class HighlightRuleManager(
     require(idList.size == channelList.size) {
       "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}"
     }
-    require(idList.size == channelList.size) {
-      "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}"
-    }
 
     state.update {
       copy(
@@ -93,7 +90,7 @@ open class HighlightRuleManager(
           QtType.QVariantList
         ),
         "name" to qVariant(
-          state().rules.map(HighlightRule::contents),
+          state().rules.map(HighlightRule::content),
           QtType.QStringList
         ),
         "isRegEx" to qVariant(
@@ -134,7 +131,7 @@ open class HighlightRuleManager(
   fun count() = state().count()
   fun removeAt(index: Int) {
     state.update {
-      copy(rules = rules.drop(index))
+      copy(rules = rules.take(index) + rules.drop(index + 1))
     }
   }
 
@@ -157,13 +154,13 @@ open class HighlightRuleManager(
 
   override fun addHighlightRule(
     id: Int,
-    name: String?,
+    content: String?,
     isRegEx: Boolean,
     isCaseSensitive: Boolean,
     isEnabled: Boolean,
     isInverse: Boolean,
     sender: String?,
-    chanName: String?
+    channel: String?
   ) {
     if (contains(id)) {
       return
@@ -173,18 +170,18 @@ open class HighlightRuleManager(
       copy(
         rules = rules + HighlightRule(
           id,
-          name ?: "",
+          content ?: "",
           isRegEx,
           isCaseSensitive,
           isEnabled,
           isInverse,
           sender ?: "",
-          chanName ?: ""
+          channel ?: ""
         )
       )
     }
 
-    super.addHighlightRule(id, name, isRegEx, isCaseSensitive, isEnabled, isInverse, sender, chanName)
+    super.addHighlightRule(id, content, isRegEx, isCaseSensitive, isEnabled, isInverse, sender, channel)
   }
 
   override fun setHighlightNick(highlightNick: Int) {
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/stubs/HighlightRuleManagerStub.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/stubs/HighlightRuleManagerStub.kt
index 118b56969e0240bfd7f8846b0bfd787f012c2336..9ebac411d98238ca8d086b77b5d9d6af4e165bc3 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/stubs/HighlightRuleManagerStub.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/stubs/HighlightRuleManagerStub.kt
@@ -58,50 +58,50 @@ interface HighlightRuleManagerStub : SyncableStub {
   @SyncedCall(target = ProtocolSide.CORE)
   fun requestAddHighlightRule(
     id: Int,
-    name: String?,
+    content: String?,
     isRegEx: Boolean,
     isCaseSensitive: Boolean,
     isEnabled: Boolean,
     isInverse: Boolean,
     sender: String?,
-    chanName: String?
+    channel: String?
   ) {
     sync(
       target = ProtocolSide.CORE,
       "requestToggleHighlightRule",
       qVariant(id, QtType.Int),
-      qVariant(name, QtType.QString),
+      qVariant(content, QtType.QString),
       qVariant(isRegEx, QtType.Bool),
       qVariant(isCaseSensitive, QtType.Bool),
       qVariant(isEnabled, QtType.Bool),
       qVariant(isInverse, QtType.Bool),
       qVariant(sender, QtType.QString),
-      qVariant(chanName, QtType.QString),
+      qVariant(channel, QtType.QString),
     )
   }
 
   @SyncedCall(target = ProtocolSide.CLIENT)
   fun addHighlightRule(
     id: Int,
-    name: String?,
+    content: String?,
     isRegEx: Boolean,
     isCaseSensitive: Boolean,
     isEnabled: Boolean,
     isInverse: Boolean,
     sender: String?,
-    chanName: String?
+    channel: String?
   ) {
     sync(
       target = ProtocolSide.CLIENT,
       "addHighlightRule",
       qVariant(id, QtType.Int),
-      qVariant(name, QtType.QString),
+      qVariant(content, QtType.QString),
       qVariant(isRegEx, QtType.Bool),
       qVariant(isCaseSensitive, QtType.Bool),
       qVariant(isEnabled, QtType.Bool),
       qVariant(isInverse, QtType.Bool),
       qVariant(sender, QtType.QString),
-      qVariant(chanName, QtType.QString),
+      qVariant(channel, QtType.QString),
     )
   }
 
diff --git a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManagerTest.kt b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManagerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9180494f3a40b749daf0699cac916d3a911709b1
--- /dev/null
+++ b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManagerTest.kt
@@ -0,0 +1,405 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
+
+import de.justjanne.libquassel.protocol.models.QStringList
+import de.justjanne.libquassel.protocol.models.rules.HighlightNickType
+import de.justjanne.libquassel.protocol.models.rules.HighlightRule
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.syncables.state.HighlightRuleManagerState
+import de.justjanne.libquassel.protocol.testutil.nextEnum
+import de.justjanne.libquassel.protocol.testutil.nextHighlightRule
+import de.justjanne.libquassel.protocol.variant.QVariantList
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.qVariant
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import kotlin.random.Random
+
+class HighlightRuleManagerTest {
+  @Test
+  fun testEmpty() {
+    val state = HighlightRuleManagerState()
+    val actual = HighlightRuleManager(state = state).apply {
+      update(emptyMap())
+    }.state()
+
+    assertEquals(state, actual)
+  }
+
+  @Test
+  fun testInvalidData() {
+    val state = HighlightRuleManagerState()
+    val actual = HighlightRuleManager(state = state).apply {
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "name" to qVariant<QStringList>(listOf(""), QtType.QStringList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "isRegEx" to qVariant<QVariantList>(listOf(
+                  qVariant(false, QtType.Bool)
+                ), QtType.QVariantList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "isCaseSensitive" to qVariant<QVariantList>(listOf(
+                  qVariant(false, QtType.Bool)
+                ), QtType.QVariantList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "isEnabled" to qVariant<QVariantList>(listOf(
+                  qVariant(false, QtType.Bool)
+                ), QtType.QVariantList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "isInverse" to qVariant<QVariantList>(listOf(
+                  qVariant(false, QtType.Bool)
+                ), QtType.QVariantList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "sender" to qVariant<QStringList>(listOf(""), QtType.QStringList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+      assertThrows<IllegalArgumentException> {
+        update(
+          mapOf(
+            "HighlightRuleList" to qVariant<QVariantMap>(
+              mapOf(
+                "id" to qVariant<QVariantList>(emptyList(), QtType.QVariantList),
+                "channel" to qVariant<QStringList>(listOf(""), QtType.QStringList),
+              ), QtType.QVariantMap
+            )
+          )
+        )
+      }
+    }.state()
+
+    assertEquals(state, actual)
+  }
+
+  @Test
+  fun testNulLData() {
+    val random = Random(1337)
+    val actual = HighlightRuleManager(
+      state = HighlightRuleManagerState()
+    ).apply {
+      update(mapOf(
+        "HighlightRuleList" to qVariant(mapOf(
+          "id" to qVariant(listOf(
+            qVariant(999, QtType.Int)
+          ), QtType.QVariantList),
+          "name" to qVariant(listOf(
+            null
+          ), QtType.QStringList),
+          "isRegEx" to qVariant(listOf(
+            qVariant(false, QtType.Bool)
+          ), QtType.QVariantList),
+          "isCaseSensitive" to qVariant(listOf(
+            qVariant(false, QtType.Bool)
+          ), QtType.QVariantList),
+          "isEnabled" to qVariant(listOf(
+            qVariant(false, QtType.Bool)
+          ), QtType.QVariantList),
+          "isInverse" to qVariant(listOf(
+            qVariant(false, QtType.Bool)
+          ), QtType.QVariantList),
+          "sender" to qVariant(listOf(
+            null
+          ), QtType.QStringList),
+          "channel" to qVariant(listOf(
+            null
+          ), QtType.QStringList)
+        ), QtType.QVariantMap)
+      ))
+    }.state()
+
+    assertEquals(HighlightRule(
+      id = 999,
+      content = "",
+      isRegEx = false,
+      isCaseSensitive = false,
+      isEnabled = false,
+      isInverse = false,
+      sender = "",
+      channel = ""
+    ), actual.rules.first())
+  }
+
+  @Test
+  fun testSerialization() {
+    val random = Random(1337)
+    val expected = HighlightRuleManagerState(
+      rules = List(random.nextInt(20)) {
+        random.nextHighlightRule(it)
+      },
+      highlightNickType = random.nextEnum(),
+      highlightNickCaseSensitive = random.nextBoolean()
+    )
+
+    val actual = HighlightRuleManager(
+      state = HighlightRuleManagerState()
+    ).apply {
+      update(HighlightRuleManager(state = expected).toVariantMap())
+    }.state()
+
+    assertEquals(expected, actual)
+  }
+
+  @Nested
+  inner class Setters {
+    @Test
+    fun testRemoveHighlightRule() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      val rule = value.state().rules.random(random)
+      assertTrue(value.contains(rule.id))
+      assertNotEquals(-1, value.indexOf(rule.id))
+      value.removeHighlightRule(rule.id)
+      assertFalse(value.contains(rule.id))
+      assertEquals(-1, value.indexOf(rule.id))
+    }
+
+    @Test
+    fun testRemoveAll() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      assertFalse(value.isEmpty())
+      for (rule in value.state().rules) {
+        value.removeHighlightRule(rule.id)
+      }
+      assertTrue(value.isEmpty())
+      assertEquals(0, value.count())
+    }
+
+    @Test
+    fun testToggleHighlightRule() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      val rule = value.state().rules.random(random)
+      assertTrue(value.contains(rule.id))
+      assertNotEquals(-1, value.indexOf(rule.id))
+      assertFalse(value.state().rules[value.indexOf(rule.id)].isEnabled)
+      value.toggleHighlightRule(rule.id)
+      assertTrue(value.contains(rule.id))
+      assertNotEquals(-1, value.indexOf(rule.id))
+      assertTrue(value.state().rules[value.indexOf(rule.id)].isEnabled)
+      value.toggleHighlightRule(rule.id)
+      assertTrue(value.contains(rule.id))
+      assertNotEquals(-1, value.indexOf(rule.id))
+      assertFalse(value.state().rules[value.indexOf(rule.id)].isEnabled)
+    }
+
+    @Test
+    fun testHighlightNick() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      assertEquals(HighlightNickType.AllNicks, value.state().highlightNickType)
+      value.setHighlightNick(HighlightNickType.CurrentNick.value)
+      assertEquals(HighlightNickType.CurrentNick, value.state().highlightNickType)
+      value.setHighlightNick(-2)
+      assertEquals(HighlightNickType.CurrentNick, value.state().highlightNickType)
+    }
+
+    @Test
+    fun testNicksCaseSensitive() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      value.setNicksCaseSensitive(false)
+      assertEquals(false, value.state().highlightNickCaseSensitive)
+      value.setNicksCaseSensitive(true)
+      assertEquals(true, value.state().highlightNickCaseSensitive)
+    }
+
+    @Test
+    fun testAddExisting() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      val rule = value.state().rules.random(random)
+      val sizeBefore = value.count()
+      value.addHighlightRule(
+        id = rule.id,
+        content = rule.content,
+        isRegEx = rule.isRegEx,
+        isCaseSensitive = rule.isCaseSensitive,
+        isEnabled = rule.isEnabled,
+        isInverse = rule.isInverse,
+        sender = rule.sender,
+        channel = rule.channel
+      )
+      assertEquals(sizeBefore, value.count())
+    }
+
+    @Test
+    fun testAddNew() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      val rule = random.nextHighlightRule(value.count())
+      val sizeBefore = value.count()
+      value.addHighlightRule(
+        id = rule.id,
+        content = rule.content,
+        isRegEx = rule.isRegEx,
+        isCaseSensitive = rule.isCaseSensitive,
+        isEnabled = rule.isEnabled,
+        isInverse = rule.isInverse,
+        sender = rule.sender,
+        channel = rule.channel
+      )
+      assertEquals(sizeBefore + 1, value.count())
+    }
+
+    @Test
+    fun testAddEdgeCase() {
+      val random = Random(1337)
+      val value = HighlightRuleManager(
+        state = HighlightRuleManagerState(
+          rules = List(random.nextInt(20)) {
+            random.nextHighlightRule(it)
+          },
+          highlightNickType = random.nextEnum(),
+          highlightNickCaseSensitive = random.nextBoolean()
+        )
+      )
+
+      val rule = random.nextHighlightRule(value.count())
+      val sizeBefore = value.count()
+      value.addHighlightRule(
+        id = rule.id,
+        content = null,
+        isRegEx = rule.isRegEx,
+        isCaseSensitive = rule.isCaseSensitive,
+        isEnabled = rule.isEnabled,
+        isInverse = rule.isInverse,
+        sender = null,
+        channel = null
+      )
+      assertEquals(sizeBefore + 1, value.count())
+    }
+  }
+}
diff --git a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt
index 80766aaab4263812a67af3d21e224e18b1fbc576..4575d537f3ed7f6e657039a15a10d0895a9620dc 100644
--- a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt
+++ b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/testutil/Random.kt
@@ -16,6 +16,8 @@ import de.justjanne.libquassel.protocol.models.ids.BufferId
 import de.justjanne.libquassel.protocol.models.ids.IdentityId
 import de.justjanne.libquassel.protocol.models.ids.NetworkId
 import de.justjanne.libquassel.protocol.models.network.NetworkServer
+import de.justjanne.libquassel.protocol.models.rules.HighlightRule
+import de.justjanne.libquassel.protocol.models.rules.IgnoreRule
 import de.justjanne.libquassel.protocol.syncables.BufferViewConfig
 import de.justjanne.libquassel.protocol.syncables.IrcChannel
 import de.justjanne.libquassel.protocol.syncables.IrcUser
@@ -184,3 +186,24 @@ fun Random.nextBufferViewManager() = BufferViewManagerState(
     BufferViewConfig(state = BufferViewConfigState(bufferViewId = nextInt()))
   }.associateBy(BufferViewConfig::bufferViewId)
 )
+
+fun Random.nextHighlightRule(id: Int) = HighlightRule(
+  id = id,
+  content = nextString(),
+  isRegEx = nextBoolean(),
+  isCaseSensitive = nextBoolean(),
+  isEnabled = nextBoolean(),
+  isInverse = nextBoolean(),
+  sender = nextString(),
+  channel = nextString()
+)
+
+fun Random.nextIgnoreRule() = IgnoreRule(
+  type = nextEnum(),
+  ignoreRule = nextString(),
+  isRegEx = nextBoolean(),
+  strictness = nextEnum(),
+  isEnabled = nextBoolean(),
+  scope = nextEnum(),
+  scopeRule = nextString()
+)