diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/ConnectedClientTest.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/ConnectedClientTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..868bda7c0d6a098738dc52c9916fa722b7c7614a
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/ConnectedClientTest.kt
@@ -0,0 +1,76 @@
+/*
+ * libquassel
+ * Copyright (c) 2024 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.client
+
+import de.justjanne.bitflags.of
+import de.justjanne.libquassel.connection.ChannelConnection
+import de.justjanne.libquassel.connection.ClientHandshakeHandler
+import de.justjanne.libquassel.connection.ClientSessionHandler
+import de.justjanne.libquassel.connection.MagicHandler
+import de.justjanne.libquassel.di.DaggerConnectedClientComponent
+import de.justjanne.libquassel.protocol.connection.ClientHeader
+import de.justjanne.libquassel.protocol.connection.ProtocolFeature
+import de.justjanne.libquassel.protocol.connection.ProtocolMeta
+import de.justjanne.libquassel.protocol.connection.ProtocolVersion
+import de.justjanne.libquassel.protocol.features.FeatureSet
+import de.justjanne.libquassel.protocol.io.CoroutineChannel
+import de.justjanne.libquassel.protocol.models.HandshakeMessage
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.jupiter.api.Test
+import java.net.InetSocketAddress
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+class ConnectedClientTest {
+  @Test
+  fun testConnection() = runBlocking {
+    val channel = CoroutineChannel()
+    withTimeout(500.milliseconds) {
+      channel.connect(InetSocketAddress("decentralised.chat", 4242), keepAlive = true)
+    }
+    val connection = ChannelConnection(channel)
+    val connectionSetup = withTimeout(500.milliseconds) {
+      MagicHandler(
+        ClientHeader(
+          features = ProtocolFeature.of(ProtocolFeature.Compression, ProtocolFeature.TLS),
+          versions = listOf(ProtocolMeta(ProtocolVersion.Datastream, 0u))
+        ),
+      ).handle(connection)
+    }.getOrThrow()
+    val handshake = withTimeout(500.milliseconds) {
+      ClientHandshakeHandler(
+        HandshakeMessage.ClientInit(clientVersion = "", buildDate = "", featureSet = FeatureSet.none()),
+        HandshakeMessage.ClientLogin(Const.user, Const.pass),
+      ).handle(connection)
+    }.getOrThrow()
+    val di = DaggerConnectedClientComponent.builder()
+      .connection(connection)
+      .featureSet(handshake.clientInitAck.featureSet)
+      .build()
+    val sessionHandler = ClientSessionHandler(handshake, di.sync(), di.rpc(), di.db().buffer())
+    launch {
+      sessionHandler.handle(connection)
+    }
+    withTimeout(20.seconds) {
+      sessionHandler.toInit.first { it != null && it.isEmpty() }
+    }
+    val bufferInfo = handshake.sessionInit.bufferInfos.first { it.bufferName == "#quassel-test" }
+    di.api().rpc.sendInput(bufferInfo, "/PRINT Test from libquassel?")
+    delay(1.seconds)
+    channel.close()
+    println(di.db().alias().getAll())
+    println(di.db().buffer().getAll())
+    println(di.db().message().getAll())
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/DatabaseTest.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/DatabaseTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b3e97035764dcba661776c5e9b92bd34a5d7add8
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/DatabaseTest.kt
@@ -0,0 +1,56 @@
+/*
+ * libquassel
+ * Copyright (c) 2024 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.client
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import de.justjanne.libquassel.persistence.AliasEntity
+import de.justjanne.libquassel.persistence.AppDatabase
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.Test
+import kotlin.time.Duration.Companion.milliseconds
+
+class DatabaseTest {
+  @Test
+  fun testDb() = runBlocking {
+    val db = Room.inMemoryDatabaseBuilder<AppDatabase>()
+      .setDriver(BundledSQLiteDriver())
+      .setQueryCoroutineContext(Dispatchers.IO)
+      .build()
+
+    val aliasRepo = db.alias()
+    val job = CoroutineScope(Dispatchers.Default).launch {
+      aliasRepo.collectAll().collectLatest {
+        println("alias list changed: $it")
+      }
+    }
+    delay(10.milliseconds)
+    aliasRepo.insert(
+      AliasEntity(0, "foo", "bar"),
+    )
+    delay(10.milliseconds)
+    aliasRepo.insert(
+      AliasEntity(1, "bar", "baz"),
+    )
+    delay(10.milliseconds)
+    aliasRepo.insert(
+      AliasEntity(0, "foo", "foo"),
+      AliasEntity(2, "baz", "foo"),
+    )
+    delay(10.milliseconds)
+    job.cancelAndJoin()
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/OfflineClientTest.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/OfflineClientTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..755ffb12f4ecf1fff3fd44a63e8dd3ef2a19049b
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/OfflineClientTest.kt
@@ -0,0 +1,56 @@
+/*
+ * libquassel
+ * Copyright (c) 2024 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.client
+
+import de.justjanne.libquassel.di.DaggerOfflineClientComponent
+import de.justjanne.libquassel.protocol.api.ObjectName
+import de.justjanne.libquassel.protocol.exceptions.RpcInvocationFailedException
+import de.justjanne.libquassel.protocol.models.ids.BufferId
+import de.justjanne.libquassel.protocol.models.ids.MsgId
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.variant.QVariant_
+import de.justjanne.libquassel.protocol.variant.qVariant
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.Test
+import kotlin.test.assertFails
+import kotlin.test.assertFailsWith
+
+class OfflineClientTest {
+  @Test
+  fun test() = runBlocking {
+    val client = DaggerOfflineClientComponent.builder().build()
+    client.sync().invoke(
+      "AliasManager",
+      ObjectName(""),
+      "update",
+      listOf(qVariant(emptyMap<String, QVariant_>(), QtType.QVariantMap))
+    )
+    assertFailsWith<RpcInvocationFailedException.UnknownMethodException> {
+      client.sync().invoke(
+        "AliasManager",
+        ObjectName(""),
+        "unknown",
+        listOf(qVariant(emptyMap<String, QVariant_>(), QtType.QVariantMap))
+      )
+    }
+    assertFails {
+      client.sync().invoke("AliasManager", ObjectName(""), "update", emptyList())
+    }
+    client.api().aliasManager.requestUpdate(emptyMap())
+    client.api().aliasManager.addAlias("name", "expansion")
+    client.api().backlogManager.requestBacklog(BufferId(-1))
+    client.api().bufferSyncer.requestSetLastSeenMsg(BufferId(1), MsgId(1337))
+    client.api().bufferViewConfig.requestSetBufferViewName(ObjectName("1"), "test")
+    client.api().bufferViewManager.requestDeleteBufferView(1)
+    client.api().certManager.requestUpdate(ObjectName(""), emptyMap())
+    client.api().highlightRuleManager.requestRemoveHighlightRule(5)
+    client.api().identity.requestUpdate(ObjectName(""), emptyMap())
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/QuasselApiTest.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/QuasselApiTest.kt
deleted file mode 100644
index f8650bb512d99947fc25c62d67c9bff07c086e97..0000000000000000000000000000000000000000
--- a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/client/QuasselApiTest.kt
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * libquassel
- * Copyright (c) 2024 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.client
-
-import androidx.room.Room
-import androidx.sqlite.driver.bundled.BundledSQLiteDriver
-import dagger.Binds
-import dagger.BindsInstance
-import dagger.Component
-import dagger.Module
-import dagger.Provides
-import de.justjanne.bitflags.of
-import de.justjanne.libquassel.backend.AliasManagerPersister
-import de.justjanne.libquassel.backend.BacklogManagerPersister
-import de.justjanne.libquassel.backend.BufferSyncerPersister
-import de.justjanne.libquassel.backend.BufferViewConfigPersister
-import de.justjanne.libquassel.backend.BufferViewManagerPersister
-import de.justjanne.libquassel.backend.CertManagerPersister
-import de.justjanne.libquassel.backend.CoreInfoPersister
-import de.justjanne.libquassel.backend.HighlightRuleManagerPersister
-import de.justjanne.libquassel.backend.IdentityPersister
-import de.justjanne.libquassel.backend.IgnoreListManagerPersister
-import de.justjanne.libquassel.backend.IrcChannelPersister
-import de.justjanne.libquassel.backend.IrcListHelperPersister
-import de.justjanne.libquassel.backend.IrcUserPersister
-import de.justjanne.libquassel.backend.NetworkConfigPersister
-import de.justjanne.libquassel.backend.NetworkPersister
-import de.justjanne.libquassel.backend.RpcPersister
-import de.justjanne.libquassel.connection.ChannelConnection
-import de.justjanne.libquassel.connection.ClientHandshakeHandler
-import de.justjanne.libquassel.connection.ClientSessionHandler
-import de.justjanne.libquassel.connection.Connection
-import de.justjanne.libquassel.connection.MagicHandler
-import de.justjanne.libquassel.persistence.AliasEntity
-import de.justjanne.libquassel.persistence.AppDatabase
-import de.justjanne.libquassel.protocol.api.ObjectName
-import de.justjanne.libquassel.protocol.api.client.AliasManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.BacklogManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.BufferSyncerClientApi
-import de.justjanne.libquassel.protocol.api.client.BufferViewConfigClientApi
-import de.justjanne.libquassel.protocol.api.client.BufferViewManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.CertManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.CoreInfoClientApi
-import de.justjanne.libquassel.protocol.api.client.HighlightRuleManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.IdentityClientApi
-import de.justjanne.libquassel.protocol.api.client.IgnoreListManagerClientApi
-import de.justjanne.libquassel.protocol.api.client.IrcChannelClientApi
-import de.justjanne.libquassel.protocol.api.client.IrcListHelperClientApi
-import de.justjanne.libquassel.protocol.api.client.IrcUserClientApi
-import de.justjanne.libquassel.protocol.api.client.NetworkClientApi
-import de.justjanne.libquassel.protocol.api.client.NetworkConfigClientApi
-import de.justjanne.libquassel.protocol.api.client.RpcClientApi
-import de.justjanne.libquassel.protocol.api.dispatcher.ClientDispatcherModule
-import de.justjanne.libquassel.protocol.api.dispatcher.RpcDispatcher
-import de.justjanne.libquassel.protocol.api.dispatcher.SyncHandler
-import de.justjanne.libquassel.protocol.api.proxy.ClientProxyModule
-import de.justjanne.libquassel.protocol.api.proxy.Proxy
-import de.justjanne.libquassel.protocol.api.server.AliasManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.BacklogManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.BufferSyncerServerApi
-import de.justjanne.libquassel.protocol.api.server.BufferViewConfigServerApi
-import de.justjanne.libquassel.protocol.api.server.BufferViewManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.CertManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.HighlightRuleManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.IdentityServerApi
-import de.justjanne.libquassel.protocol.api.server.IgnoreListManagerServerApi
-import de.justjanne.libquassel.protocol.api.server.IrcListHelperServerApi
-import de.justjanne.libquassel.protocol.api.server.NetworkConfigServerApi
-import de.justjanne.libquassel.protocol.api.server.NetworkServerApi
-import de.justjanne.libquassel.protocol.api.server.RpcServerApi
-import de.justjanne.libquassel.protocol.connection.ClientHeader
-import de.justjanne.libquassel.protocol.connection.ProtocolFeature
-import de.justjanne.libquassel.protocol.connection.ProtocolMeta
-import de.justjanne.libquassel.protocol.connection.ProtocolVersion
-import de.justjanne.libquassel.protocol.exceptions.RpcInvocationFailedException
-import de.justjanne.libquassel.protocol.features.FeatureSet
-import de.justjanne.libquassel.protocol.io.CoroutineChannel
-import de.justjanne.libquassel.protocol.models.HandshakeMessage
-import de.justjanne.libquassel.protocol.models.SignalProxyMessage
-import de.justjanne.libquassel.protocol.models.ids.BufferId
-import de.justjanne.libquassel.protocol.models.ids.MsgId
-import de.justjanne.libquassel.protocol.models.types.QtType
-import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer
-import de.justjanne.libquassel.protocol.variant.QVariantList
-import de.justjanne.libquassel.protocol.variant.QVariant_
-import de.justjanne.libquassel.protocol.variant.qVariant
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeout
-import org.junit.jupiter.api.Test
-import java.net.InetSocketAddress
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.test.assertFails
-import kotlin.test.assertFailsWith
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-
-@Singleton
-class ProxyImpl @Inject constructor(
-  private val connection: Connection,
-  private val featureSet: FeatureSet,
-) : Proxy {
-  override suspend fun sync(className: String, objectName: ObjectName, function: String, params: QVariantList) {
-    val message = SignalProxyMessage.Sync(className, objectName.objectName, function, params)
-    println("Send: $message")
-    connection.send(true) {
-      SignalProxyMessageSerializer.serialize(it, message, featureSet)
-    }
-    println("Sent: $message")
-  }
-
-  override suspend fun rpc(function: String, params: QVariantList) {
-    val message = SignalProxyMessage.Rpc(function, params)
-    println("Send: $message")
-    connection.send(true) {
-      SignalProxyMessageSerializer.serialize(it, message, featureSet)
-    }
-    println("Sent: $message")
-  }
-}
-
-@Singleton
-class OfflineProxyImpl @Inject constructor() : Proxy {
-  override suspend fun sync(className: String, objectName: ObjectName, function: String, params: QVariantList) {
-    val message = SignalProxyMessage.Sync(className, objectName.objectName, function, params)
-    println("Send: $message")
-  }
-
-  override suspend fun rpc(function: String, params: QVariantList) {
-    val message = SignalProxyMessage.Rpc(function, params)
-    println("Send: $message")
-  }
-}
-
-@Module
-interface ClientModule {
-  @Binds fun bindAliasManagerPersister(impl: AliasManagerPersister): AliasManagerClientApi
-  @Binds fun bindBacklogManagerPersister(impl: BacklogManagerPersister): BacklogManagerClientApi
-  @Binds fun bindBufferSyncerPersister(impl: BufferSyncerPersister): BufferSyncerClientApi
-  @Binds fun bindBufferViewConfigPersister(impl: BufferViewConfigPersister): BufferViewConfigClientApi
-  @Binds fun bindBufferViewManagerPersister(impl: BufferViewManagerPersister): BufferViewManagerClientApi
-  @Binds fun bindCertManagerPersister(impl: CertManagerPersister): CertManagerClientApi
-  @Binds fun bindCoreInfoPersister(impl: CoreInfoPersister): CoreInfoClientApi
-  @Binds fun bindHighlightRuleManagerPersister(impl: HighlightRuleManagerPersister): HighlightRuleManagerClientApi
-  @Binds fun bindIdentityPersister(impl: IdentityPersister): IdentityClientApi
-  @Binds fun bindIgnoreListManagerPersister(impl: IgnoreListManagerPersister): IgnoreListManagerClientApi
-  @Binds fun bindIrcChannelPersister(impl: IrcChannelPersister): IrcChannelClientApi
-  @Binds fun bindIrcListHelperPersister(impl: IrcListHelperPersister): IrcListHelperClientApi
-  @Binds fun bindIrcUserPersister(impl: IrcUserPersister): IrcUserClientApi
-  @Binds fun bindNetworkConfigPersister(impl: NetworkConfigPersister): NetworkConfigClientApi
-  @Binds fun bindNetworkPersister(impl: NetworkPersister): NetworkClientApi
-  @Binds fun bindRpcPersister(impl: RpcPersister): RpcClientApi
-}
-
-@Module
-interface OnlineClientModule {
-  @Binds fun bindProxy(impl: ProxyImpl): Proxy
-}
-
-@Module
-interface OfflineClientModule {
-  @Binds fun bindProxy(impl: OfflineProxyImpl): Proxy
-}
-
-@Module
-class DatabaseModule {
-  @Singleton
-  @Provides
-  fun database(): AppDatabase =
-    Room.inMemoryDatabaseBuilder<AppDatabase>()
-      .setDriver(BundledSQLiteDriver())
-      .setQueryCoroutineContext(Dispatchers.IO)
-      .build()
-
-  @Provides
-  fun provideAliasRepository(database: AppDatabase) = database.alias()
-  @Provides
-  fun provideBufferRepository(database: AppDatabase) = database.buffer()
-  @Provides
-  fun provideMessageRepository(database: AppDatabase) = database.message()
-}
-
-@Singleton
-class QuasselApiClient @Inject constructor(
-  val aliasManager: AliasManagerServerApi,
-  val backlogManager: BacklogManagerServerApi,
-  val bufferSyncer: BufferSyncerServerApi,
-  val bufferViewConfig: BufferViewConfigServerApi,
-  val bufferViewManager: BufferViewManagerServerApi,
-  val certManager: CertManagerServerApi,
-  val highlightRuleManager: HighlightRuleManagerServerApi,
-  val identity: IdentityServerApi,
-  val ignoreListManager: IgnoreListManagerServerApi,
-  val ircListHelper: IrcListHelperServerApi,
-  val network: NetworkServerApi,
-  val networkConfig: NetworkConfigServerApi,
-  val rpc: RpcServerApi,
-)
-
-@Singleton
-@Component(modules = [ClientModule::class, OnlineClientModule::class, DatabaseModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
-interface ClientComponent {
-  fun db(): AppDatabase
-  fun sync(): SyncHandler
-  fun rpc(): RpcDispatcher
-  fun api(): QuasselApiClient
-
-  @Component.Builder
-  interface Builder {
-    fun build(): ClientComponent
-    @BindsInstance fun connection(connection: Connection): Builder
-    @BindsInstance fun featureSet(featureSet: FeatureSet): Builder
-  }
-}
-
-@Singleton
-@Component(modules = [ClientModule::class, OfflineClientModule::class, DatabaseModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
-interface OfflineClientComponent {
-  fun db(): AppDatabase
-  fun sync(): SyncHandler
-  fun rpc(): RpcDispatcher
-  fun api(): QuasselApiClient
-}
-
-class QuasselApiTest {
-  @Test
-  fun test() = runBlocking {
-    val client = DaggerOfflineClientComponent.builder().build()
-    client.sync().invoke(
-      "AliasManager",
-      ObjectName(""),
-      "update",
-      listOf(qVariant(emptyMap<String, QVariant_>(), QtType.QVariantMap))
-    )
-    assertFailsWith<RpcInvocationFailedException.UnknownMethodException> {
-      client.sync().invoke(
-        "AliasManager",
-        ObjectName(""),
-        "unknown",
-        listOf(qVariant(emptyMap<String, QVariant_>(), QtType.QVariantMap))
-      )
-    }
-    assertFails {
-      client.sync().invoke("AliasManager", ObjectName(""), "update", emptyList())
-    }
-    client.api().aliasManager.requestUpdate(emptyMap())
-    client.api().aliasManager.addAlias("name", "expansion")
-    client.api().backlogManager.requestBacklog(BufferId(-1))
-    client.api().bufferSyncer.requestSetLastSeenMsg(BufferId(1), MsgId(1337))
-    client.api().bufferViewConfig.requestSetBufferViewName(ObjectName("1"), "test")
-    client.api().bufferViewManager.requestDeleteBufferView(1)
-    client.api().certManager.requestUpdate(ObjectName(""), emptyMap())
-    client.api().highlightRuleManager.requestRemoveHighlightRule(5)
-    client.api().identity.requestUpdate(ObjectName(""), emptyMap())
-  }
-
-  @Test
-  fun testDb() = runBlocking {
-    val db = Room.inMemoryDatabaseBuilder<AppDatabase>()
-      .setDriver(BundledSQLiteDriver())
-      .setQueryCoroutineContext(Dispatchers.IO)
-      .build()
-
-    val aliasRepo = db.alias()
-    val job = CoroutineScope(Dispatchers.Default).launch {
-      aliasRepo.collectAll().collectLatest {
-        println("alias list changed: $it")
-      }
-    }
-    delay(10.milliseconds)
-    aliasRepo.insert(
-      AliasEntity(0, "foo", "bar"),
-    )
-    delay(10.milliseconds)
-    aliasRepo.insert(
-      AliasEntity(1, "bar", "baz"),
-    )
-    delay(10.milliseconds)
-    aliasRepo.insert(
-      AliasEntity(0, "foo", "foo"),
-      AliasEntity(2, "baz", "foo"),
-    )
-    delay(10.milliseconds)
-    job.cancelAndJoin()
-  }
-
-  @Test
-  fun testConnection() = runBlocking {
-    val channel = CoroutineChannel()
-    withTimeout(500.milliseconds) {
-      channel.connect(InetSocketAddress("decentralised.chat", 4242), keepAlive = true)
-    }
-    val connection = ChannelConnection(channel)
-    val connectionSetup = withTimeout(500.milliseconds) {
-      MagicHandler(
-        ClientHeader(
-          features = ProtocolFeature.of(ProtocolFeature.Compression, ProtocolFeature.TLS),
-          versions = listOf(ProtocolMeta(ProtocolVersion.Datastream, 0u))
-        ),
-      ).handle(connection)
-    }.getOrThrow()
-    val handshake = withTimeout(500.milliseconds) {
-      ClientHandshakeHandler(
-        HandshakeMessage.ClientInit(clientVersion = "", buildDate = "", featureSet = FeatureSet.none()),
-        HandshakeMessage.ClientLogin(Const.user, Const.pass),
-      ).handle(connection)
-    }.getOrThrow()
-    val di = DaggerClientComponent.builder()
-      .connection(connection)
-      .featureSet(handshake.clientInitAck.featureSet)
-      .build()
-    val sessionHandler = ClientSessionHandler(handshake, di.sync(), di.rpc(), di.db().buffer())
-    launch {
-      sessionHandler.handle(connection)
-    }
-    withTimeout(20.seconds) {
-      sessionHandler.toInit.first { it != null && it.isEmpty() }
-    }
-    val bufferInfo = handshake.sessionInit.bufferInfos.first { it.bufferName == "#quassel-test" }
-    di.api().rpc.sendInput(bufferInfo, "/PRINT Test from libquassel?")
-    delay(1.seconds)
-    channel.close()
-    println(di.db().alias().getAll())
-    println(di.db().buffer().getAll())
-    println(di.db().message().getAll())
-    Unit
-  }
-}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ClientModule.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ClientModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b5f8e599ca483f66625f763aebb90843a6edff4
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ClientModule.kt
@@ -0,0 +1,81 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import dagger.Binds
+import dagger.Module
+import de.justjanne.libquassel.backend.AliasManagerPersister
+import de.justjanne.libquassel.backend.BacklogManagerPersister
+import de.justjanne.libquassel.backend.BufferSyncerPersister
+import de.justjanne.libquassel.backend.BufferViewConfigPersister
+import de.justjanne.libquassel.backend.BufferViewManagerPersister
+import de.justjanne.libquassel.backend.CertManagerPersister
+import de.justjanne.libquassel.backend.CoreInfoPersister
+import de.justjanne.libquassel.backend.HighlightRuleManagerPersister
+import de.justjanne.libquassel.backend.IdentityPersister
+import de.justjanne.libquassel.backend.IgnoreListManagerPersister
+import de.justjanne.libquassel.backend.IrcChannelPersister
+import de.justjanne.libquassel.backend.IrcListHelperPersister
+import de.justjanne.libquassel.backend.IrcUserPersister
+import de.justjanne.libquassel.backend.NetworkConfigPersister
+import de.justjanne.libquassel.backend.NetworkPersister
+import de.justjanne.libquassel.backend.RpcPersister
+import de.justjanne.libquassel.protocol.api.client.AliasManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.BacklogManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.BufferSyncerClientApi
+import de.justjanne.libquassel.protocol.api.client.BufferViewConfigClientApi
+import de.justjanne.libquassel.protocol.api.client.BufferViewManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.CertManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.CoreInfoClientApi
+import de.justjanne.libquassel.protocol.api.client.HighlightRuleManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.IdentityClientApi
+import de.justjanne.libquassel.protocol.api.client.IgnoreListManagerClientApi
+import de.justjanne.libquassel.protocol.api.client.IrcChannelClientApi
+import de.justjanne.libquassel.protocol.api.client.IrcListHelperClientApi
+import de.justjanne.libquassel.protocol.api.client.IrcUserClientApi
+import de.justjanne.libquassel.protocol.api.client.NetworkClientApi
+import de.justjanne.libquassel.protocol.api.client.NetworkConfigClientApi
+import de.justjanne.libquassel.protocol.api.client.RpcClientApi
+
+@Module
+interface ClientModule {
+  @Binds
+  fun bindAliasManagerPersister(impl: AliasManagerPersister): AliasManagerClientApi
+  @Binds
+  fun bindBacklogManagerPersister(impl: BacklogManagerPersister): BacklogManagerClientApi
+  @Binds
+  fun bindBufferSyncerPersister(impl: BufferSyncerPersister): BufferSyncerClientApi
+  @Binds
+  fun bindBufferViewConfigPersister(impl: BufferViewConfigPersister): BufferViewConfigClientApi
+  @Binds
+  fun bindBufferViewManagerPersister(impl: BufferViewManagerPersister): BufferViewManagerClientApi
+  @Binds
+  fun bindCertManagerPersister(impl: CertManagerPersister): CertManagerClientApi
+  @Binds
+  fun bindCoreInfoPersister(impl: CoreInfoPersister): CoreInfoClientApi
+  @Binds
+  fun bindHighlightRuleManagerPersister(impl: HighlightRuleManagerPersister): HighlightRuleManagerClientApi
+  @Binds
+  fun bindIdentityPersister(impl: IdentityPersister): IdentityClientApi
+  @Binds
+  fun bindIgnoreListManagerPersister(impl: IgnoreListManagerPersister): IgnoreListManagerClientApi
+  @Binds
+  fun bindIrcChannelPersister(impl: IrcChannelPersister): IrcChannelClientApi
+  @Binds
+  fun bindIrcListHelperPersister(impl: IrcListHelperPersister): IrcListHelperClientApi
+  @Binds
+  fun bindIrcUserPersister(impl: IrcUserPersister): IrcUserClientApi
+  @Binds
+  fun bindNetworkConfigPersister(impl: NetworkConfigPersister): NetworkConfigClientApi
+  @Binds
+  fun bindNetworkPersister(impl: NetworkPersister): NetworkClientApi
+  @Binds
+  fun bindRpcPersister(impl: RpcPersister): RpcClientApi
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientComponent.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientComponent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..17dd48f71ef898e079338e5632fa3c74a920161c
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientComponent.kt
@@ -0,0 +1,39 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import dagger.BindsInstance
+import dagger.Component
+import de.justjanne.libquassel.connection.Connection
+import de.justjanne.libquassel.persistence.AppDatabase
+import de.justjanne.libquassel.protocol.api.dispatcher.ClientDispatcherModule
+import de.justjanne.libquassel.protocol.api.dispatcher.RpcDispatcher
+import de.justjanne.libquassel.protocol.api.dispatcher.SyncHandler
+import de.justjanne.libquassel.protocol.api.proxy.ClientProxyModule
+import de.justjanne.libquassel.protocol.features.FeatureSet
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [ClientModule::class, ConnectedClientModule::class, DatabaseModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
+interface ConnectedClientComponent {
+  fun db(): AppDatabase
+  fun sync(): SyncHandler
+  fun rpc(): RpcDispatcher
+  fun api(): QuasselApiClient
+
+  @Component.Builder
+  interface Builder {
+    fun build(): ConnectedClientComponent
+    @BindsInstance
+    fun connection(connection: Connection): Builder
+    @BindsInstance
+    fun featureSet(featureSet: FeatureSet): Builder
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientModule.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..506ecf046ffd3e7af7c6c4e00488c47a9d55e064
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedClientModule.kt
@@ -0,0 +1,20 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import dagger.Binds
+import dagger.Module
+import de.justjanne.libquassel.protocol.api.proxy.Proxy
+
+@Module
+interface ConnectedClientModule {
+  @Binds
+  fun bindProxy(impl: ConnectedProxyImpl): Proxy
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedProxyImpl.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedProxyImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7205ee95e153139dd05ae874ce6741e2f02ab37f
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/ConnectedProxyImpl.kt
@@ -0,0 +1,44 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import de.justjanne.libquassel.connection.Connection
+import de.justjanne.libquassel.protocol.api.ObjectName
+import de.justjanne.libquassel.protocol.api.proxy.Proxy
+import de.justjanne.libquassel.protocol.features.FeatureSet
+import de.justjanne.libquassel.protocol.models.SignalProxyMessage
+import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer
+import de.justjanne.libquassel.protocol.variant.QVariantList
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ConnectedProxyImpl @Inject constructor(
+  private val connection: Connection,
+  private val featureSet: FeatureSet,
+) : Proxy {
+  override suspend fun sync(className: String, objectName: ObjectName, function: String, params: QVariantList) {
+    val message = SignalProxyMessage.Sync(className, objectName.objectName, function, params)
+    println("Send: $message")
+    connection.send(true) {
+      SignalProxyMessageSerializer.serialize(it, message, featureSet)
+    }
+    println("Sent: $message")
+  }
+
+  override suspend fun rpc(function: String, params: QVariantList) {
+    val message = SignalProxyMessage.Rpc(function, params)
+    println("Send: $message")
+    connection.send(true) {
+      SignalProxyMessageSerializer.serialize(it, message, featureSet)
+    }
+    println("Sent: $message")
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/DatabaseModule.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/DatabaseModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..acc77c487e1df387cdb10e7cee5e3c400b18de2c
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/DatabaseModule.kt
@@ -0,0 +1,36 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import dagger.Module
+import dagger.Provides
+import de.justjanne.libquassel.persistence.AppDatabase
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Singleton
+
+@Module
+class DatabaseModule {
+  @Singleton
+  @Provides
+  fun database(): AppDatabase =
+    Room.inMemoryDatabaseBuilder<AppDatabase>()
+      .setDriver(BundledSQLiteDriver())
+      .setQueryCoroutineContext(Dispatchers.IO)
+      .build()
+
+  @Provides
+  fun provideAliasRepository(database: AppDatabase) = database.alias()
+  @Provides
+  fun provideBufferRepository(database: AppDatabase) = database.buffer()
+  @Provides
+  fun provideMessageRepository(database: AppDatabase) = database.message()
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientComponent.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientComponent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7f97123b3ac3aa0b68ad42ec474ce6616304d967
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientComponent.kt
@@ -0,0 +1,27 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import dagger.Component
+import de.justjanne.libquassel.persistence.AppDatabase
+import de.justjanne.libquassel.protocol.api.dispatcher.ClientDispatcherModule
+import de.justjanne.libquassel.protocol.api.dispatcher.RpcDispatcher
+import de.justjanne.libquassel.protocol.api.dispatcher.SyncHandler
+import de.justjanne.libquassel.protocol.api.proxy.ClientProxyModule
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [ClientModule::class, OfflineClientModule::class, DatabaseModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
+interface OfflineClientComponent {
+  fun db(): AppDatabase
+  fun sync(): SyncHandler
+  fun rpc(): RpcDispatcher
+  fun api(): QuasselApiClient
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientModule.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..425a1f1d2b3c36c5171d7ab4620a6fc275d8a20e
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineClientModule.kt
@@ -0,0 +1,20 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import dagger.Binds
+import dagger.Module
+import de.justjanne.libquassel.protocol.api.proxy.Proxy
+
+@Module
+interface OfflineClientModule {
+  @Binds
+  fun bindProxy(impl: OfflineProxyImpl): Proxy
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineProxyImpl.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineProxyImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5f8f9916db6551e66a5a1ad256b3b24efdebabb0
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/OfflineProxyImpl.kt
@@ -0,0 +1,30 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import de.justjanne.libquassel.protocol.api.ObjectName
+import de.justjanne.libquassel.protocol.api.proxy.Proxy
+import de.justjanne.libquassel.protocol.models.SignalProxyMessage
+import de.justjanne.libquassel.protocol.variant.QVariantList
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class OfflineProxyImpl @Inject constructor() : Proxy {
+  override suspend fun sync(className: String, objectName: ObjectName, function: String, params: QVariantList) {
+    val message = SignalProxyMessage.Sync(className, objectName.objectName, function, params)
+    println("Send: $message")
+  }
+
+  override suspend fun rpc(function: String, params: QVariantList) {
+    val message = SignalProxyMessage.Rpc(function, params)
+    println("Send: $message")
+  }
+}
diff --git a/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/QuasselApiClient.kt b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/QuasselApiClient.kt
new file mode 100644
index 0000000000000000000000000000000000000000..493b6d2c76e3ab17e380c805e9af841061a55e13
--- /dev/null
+++ b/libquassel-client/src/test/kotlin/de/justjanne/libquassel/di/QuasselApiClient.kt
@@ -0,0 +1,43 @@
+/*
+ * libquassel
+ * Copyright (c) 2025 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.di
+
+import de.justjanne.libquassel.protocol.api.server.AliasManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.BacklogManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.BufferSyncerServerApi
+import de.justjanne.libquassel.protocol.api.server.BufferViewConfigServerApi
+import de.justjanne.libquassel.protocol.api.server.BufferViewManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.CertManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.HighlightRuleManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.IdentityServerApi
+import de.justjanne.libquassel.protocol.api.server.IgnoreListManagerServerApi
+import de.justjanne.libquassel.protocol.api.server.IrcListHelperServerApi
+import de.justjanne.libquassel.protocol.api.server.NetworkConfigServerApi
+import de.justjanne.libquassel.protocol.api.server.NetworkServerApi
+import de.justjanne.libquassel.protocol.api.server.RpcServerApi
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class QuasselApiClient @Inject constructor(
+  val aliasManager: AliasManagerServerApi,
+  val backlogManager: BacklogManagerServerApi,
+  val bufferSyncer: BufferSyncerServerApi,
+  val bufferViewConfig: BufferViewConfigServerApi,
+  val bufferViewManager: BufferViewManagerServerApi,
+  val certManager: CertManagerServerApi,
+  val highlightRuleManager: HighlightRuleManagerServerApi,
+  val identity: IdentityServerApi,
+  val ignoreListManager: IgnoreListManagerServerApi,
+  val ircListHelper: IrcListHelperServerApi,
+  val network: NetworkServerApi,
+  val networkConfig: NetworkConfigServerApi,
+  val rpc: RpcServerApi,
+)