diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
index 1982b8b503842cbce8db6c2d8ed4d2befd41bc07..455bdff772511e233b603a562bd2ae7674aa0d6f 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
@@ -85,7 +85,7 @@ class AuthManager @Inject constructor(
 
 sealed class MailboxPrincipal : Principal {
     object OwnerPrincipal : MailboxPrincipal()
-    class ContactPrincipal(val contact: Contact) : MailboxPrincipal()
+    data class ContactPrincipal(val contact: Contact) : MailboxPrincipal()
 }
 
 class AuthException : IllegalStateException()
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
index 2a9abd05bca1b569c945e5ae0cc888f6bef197ec..a21277876b3f7b760a9ec9bac1397a11bd6fad11 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
@@ -4,6 +4,7 @@ import dagger.Component
 import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.settings.SettingsManager
+import org.briarproject.mailbox.core.system.RandomIdManager
 import javax.inject.Singleton
 
 @Singleton
@@ -17,4 +18,5 @@ interface TestComponent {
     fun getLifecycleManager(): LifecycleManager
     fun getSettingsManager(): SettingsManager
     fun getDatabase(): Database
+    fun getRandomIdManager(): RandomIdManager
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
index 546e5ce16d0328a75371b8ad252e02d1dc543c55..1a3d6f5b0992b952674f1771900e817c070f5935 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
@@ -1,5 +1,173 @@
 package org.briarproject.mailbox.core.server
 
+import io.mockk.every
+import io.mockk.mockk
+import org.briarproject.mailbox.core.TestUtils.everyTransactionWithResult
+import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
+import org.briarproject.mailbox.core.TestUtils.getNewRandomId
+import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal
+import org.briarproject.mailbox.core.settings.Settings
+import org.briarproject.mailbox.core.settings.SettingsManager
+import org.briarproject.mailbox.core.system.InvalidIdException
+import org.briarproject.mailbox.core.system.RandomIdManager
+import org.briarproject.mailbox.core.system.toHex
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import kotlin.random.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
 class AuthManagerTest {
-    // TODO write unit tests
+
+    private val db: Database = mockk()
+    private val settingsManager: SettingsManager = mockk()
+    private val randomIdManager = RandomIdManager()
+
+    private val authManager = AuthManager(db, settingsManager, randomIdManager)
+
+    private val id = getNewRandomId()
+    private val otherId = getNewRandomId()
+    private val invalidId = Random.nextBytes(Random.nextInt(0, 32)).toHex()
+    private val contact = getNewRandomContact()
+    private val contactPrincipal = MailboxPrincipal.ContactPrincipal(contact)
+
+    @Test
+    fun `rejects invalid token for getPrincipal()`() {
+        assertThrows<InvalidIdException> {
+            authManager.getPrincipal(invalidId)
+        }
+    }
+
+    @Test
+    fun `getPrincipal() returns authenticated contact`() {
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContactWithToken(txn, id) } returns contactPrincipal.contact
+        }
+        assertEquals(contactPrincipal, authManager.getPrincipal(id))
+    }
+
+    @Test
+    fun `getPrincipal() returns authenticated owner`() {
+        val settings = Settings().apply {
+            put(SETTINGS_OWNER_TOKEN, id)
+        }
+
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContactWithToken(txn, id) } returns null
+            every { settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) } returns settings
+        }
+
+        assertEquals(OwnerPrincipal, authManager.getPrincipal(id))
+    }
+
+    @Test
+    fun `getPrincipal() returns null when unauthenticated`() {
+        val settings = Settings().apply {
+            put(SETTINGS_OWNER_TOKEN, otherId)
+        }
+
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContactWithToken(txn, id) } returns null
+            every { settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) } returns settings
+        }
+
+        assertNull(authManager.getPrincipal(id))
+    }
+
+    @Test
+    fun `assertCanDownloadFromFolder() throws for null MailboxPrincipal`() {
+        assertThrows<AuthException> {
+            authManager.assertCanDownloadFromFolder(null, id)
+        }
+    }
+
+    @Test
+    fun `assertCanDownloadFromFolder() throws if owner wants non-existent folder`() {
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContacts(txn) } returns emptyList()
+        }
+
+        assertThrows<AuthException> {
+            authManager.assertCanDownloadFromFolder(OwnerPrincipal, id)
+        }
+    }
+
+    @Test
+    fun `throws if contact wants to download from folder that is not their inbox`() {
+        assertThrows<AuthException> {
+            authManager.assertCanDownloadFromFolder(contactPrincipal, id)
+        }
+        assertThrows<AuthException> {
+            authManager.assertCanDownloadFromFolder(contactPrincipal, contact.outboxId)
+        }
+    }
+
+    @Test
+    fun `assertCanDownloadFromFolder() lets owner access contact's outbox folder`() {
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContacts(txn) } returns listOf(contact, getNewRandomContact())
+        }
+
+        authManager.assertCanDownloadFromFolder(OwnerPrincipal, contact.outboxId)
+    }
+
+    @Test
+    fun `assertCanDownloadFromFolder() lets contact access their inbox folder`() {
+        authManager.assertCanDownloadFromFolder(contactPrincipal, contact.inboxId)
+    }
+
+    @Test
+    fun `assertCanPostToFolder() throws for null MailboxPrincipal`() {
+        assertThrows<AuthException> {
+            authManager.assertCanPostToFolder(null, id)
+        }
+    }
+
+    @Test
+    fun `assertCanPostToFolder() throws if owner wants non-existent folder`() {
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContacts(txn) } returns emptyList()
+        }
+
+        assertThrows<AuthException> {
+            authManager.assertCanPostToFolder(OwnerPrincipal, id)
+        }
+    }
+
+    @Test
+    fun `throws if contact wants to post to folder that is not their outbox`() {
+        assertThrows<AuthException> {
+            authManager.assertCanPostToFolder(contactPrincipal, id)
+        }
+        assertThrows<AuthException> {
+            authManager.assertCanPostToFolder(contactPrincipal, contact.inboxId)
+        }
+    }
+
+    @Test
+    fun `assertCanPostToFolder() lets owner access contact's inbox folder`() {
+        everyTransactionWithResult(db, true) { txn ->
+            every { db.getContacts(txn) } returns listOf(contact, getNewRandomContact())
+        }
+
+        authManager.assertCanPostToFolder(OwnerPrincipal, contact.inboxId)
+    }
+
+    @Test
+    fun `assertCanPostToFolder() lets contact access their outbox folder`() {
+        authManager.assertCanPostToFolder(contactPrincipal, contact.outboxId)
+    }
+
+    @Test
+    fun `assertIsOwner() throws for non-owners`() {
+        assertThrows<AuthException> { authManager.assertIsOwner(null) }
+        assertThrows<AuthException> { authManager.assertIsOwner(contactPrincipal) }
+    }
+
+    @Test
+    fun `assertIsOwner() passes for owner`() {
+        authManager.assertIsOwner(OwnerPrincipal)
+    }
+
 }