briar-headless: Add more controller tests

Current controller line coverage: 100%
parent 159fd34c
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All in briar-headless" type="AndroidJUnit" factoryName="Android JUnit" nameIsGenerated="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<module name="briar-headless" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
</option>
<envs />
<patterns />
<method />
</configuration>
</component>
\ No newline at end of file
......@@ -24,6 +24,7 @@
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in bramble-android" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in bramble-java" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in briar-core" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All in briar-headless" run_configuration_type="AndroidJUnit" />
</method>
</configuration>
</component>
</component>
\ No newline at end of file
plugins {
id 'java'
id 'idea'
id 'org.jetbrains.kotlin.jvm' version '1.2.61'
id "org.jetbrains.kotlin.kapt" version "1.2.61"
id 'org.jetbrains.kotlin.jvm' version '1.2.70'
id 'org.jetbrains.kotlin.kapt' version '1.2.70'
id 'witness'
}
apply from: 'witness.gradle'
......@@ -14,7 +14,7 @@ dependencies {
implementation project(path: ':briar-core', configuration: 'default')
implementation project(path: ':bramble-java', configuration: 'default')
implementation 'io.javalin:javalin:2.1.0'
implementation 'io.javalin:javalin:2.2.0'
implementation 'org.slf4j:slf4j-simple:1.7.25'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6'
implementation 'com.github.ajalt:clikt:1.5.0'
......@@ -29,6 +29,7 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "io.mockk:mockk:1.8.6"
testImplementation "org.skyscreamer:jsonassert:1.5.0"
}
jar {
......
......@@ -4,6 +4,8 @@ import io.javalin.BadRequestResponse
import io.javalin.Context
import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.util.StringUtils
import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_BODY_LENGTH
import org.briarproject.briar.api.blog.BlogManager
import org.briarproject.briar.api.blog.BlogPostFactory
import javax.annotation.concurrent.Immutable
......@@ -33,7 +35,9 @@ constructor(
fun createPost(ctx: Context): Context {
val text = ctx.formParam("text")
if (text == null || text.isEmpty())
throw BadRequestResponse("Expecting Blog text")
throw BadRequestResponse("Expecting blog post text")
if (StringUtils.toUtf8(text).size > MAX_BLOG_POST_BODY_LENGTH)
throw BadRequestResponse("Too long blog post text")
val author = identityManager.localAuthor
val blog = blogManager.getPersonalBlog(author)
......
......@@ -2,6 +2,8 @@ package org.briarproject.briar.headless.forums
import io.javalin.BadRequestResponse
import io.javalin.Context
import org.briarproject.bramble.util.StringUtils
import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH
import org.briarproject.briar.api.forum.ForumManager
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
......@@ -18,8 +20,10 @@ constructor(private val forumManager: ForumManager) {
fun create(ctx: Context): Context {
val name = ctx.formParam("name")
if (name == null || name.isEmpty())
if (name == null || name.isNullOrEmpty())
throw BadRequestResponse("Expecting Forum Name")
if (StringUtils.toUtf8(name).size > MAX_FORUM_NAME_LENGTH)
throw BadRequestResponse("Forum name is too long")
return ctx.json(forumManager.addForum(name).output())
}
......
......@@ -4,7 +4,7 @@ import org.briarproject.briar.api.forum.Forum
import javax.annotation.concurrent.Immutable
@Immutable
internal class OutputForum(
internal data class OutputForum(
val name: String,
val id: ByteArray
) {
......
......@@ -14,10 +14,10 @@ import org.briarproject.briar.api.sharing.InvitationResponse
internal fun PrivateMessageHeader.output(
contactId: ContactId,
body: String?
) = OutputPrivateMessage(this, contactId, body)
) = OutputPrivateMessageHeader(this, contactId, body)
internal fun PrivateMessage.output(contactId: ContactId, body: String) =
OutputPrivateMessage(this, contactId, body)
OutputPrivateMessageHeader(this, contactId, body)
internal fun PrivateMessageReceivedEvent<*>.output(body: String) =
messageHeader.output(contactId, body)
......
......@@ -20,7 +20,7 @@ import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
private const val EVENT_PRIVATE_MESSAGE =
internal const val EVENT_PRIVATE_MESSAGE =
"org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent"
@Immutable
......@@ -79,7 +79,11 @@ constructor(
private fun getContact(ctx: Context): Contact {
val contactString = ctx.pathParam("contactId")
val contactInt = Integer.parseInt(contactString)
val contactInt = try {
Integer.parseInt(contactString)
} catch (e: NumberFormatException) {
throw NotFoundResponse()
}
val contactId = ContactId(contactInt)
return try {
contactManager.getContact(contactId)
......
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package org.briarproject.briar.headless.messaging
import org.briarproject.bramble.api.contact.ContactId
......@@ -8,47 +6,45 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader
import javax.annotation.concurrent.Immutable
@Immutable
internal open class OutputPrivateMessage(
val body: String?,
val timestamp: Long,
val read: Boolean,
val seen: Boolean,
val sent: Boolean,
val local: Boolean,
val id: ByteArray,
val groupId: ByteArray,
val contactId: Int
internal abstract class OutputPrivateMessage(
protected open val iHeader: PrivateMessageHeader,
protected open val iContactId: ContactId,
open val body: String?
) {
open val type = "org.briarproject.briar.api.messaging.PrivateMessageHeader"
internal constructor(
header: PrivateMessageHeader,
contactId: ContactId,
body: String?
) : this(
body = body,
timestamp = header.timestamp,
read = header.isRead,
seen = header.isSeen,
sent = header.isSent,
local = header.isLocal,
id = header.id.bytes,
groupId = header.groupId.bytes,
contactId = contactId.int
)
open val type: String get() = throw NotImplementedError()
val contactId: Int get() = iContactId.int
val timestamp: Long get() = iHeader.timestamp
val read: Boolean get() = iHeader.isRead
val seen: Boolean get() = iHeader.isSeen
val sent: Boolean get() = iHeader.isSent
val local: Boolean get() = iHeader.isLocal
val id: ByteArray get() = iHeader.id.bytes
val groupId: ByteArray get() = iHeader.groupId.bytes
}
@Immutable
internal data class OutputPrivateMessageHeader(
override val iHeader: PrivateMessageHeader,
override val iContactId: ContactId,
override val body: String?
) : OutputPrivateMessage(iHeader, iContactId, body) {
override val type = "org.briarproject.briar.api.messaging.PrivateMessageHeader"
/**
* Only meant for own [PrivateMessage]s directly after creation.
*/
internal constructor(m: PrivateMessage, contactId: ContactId, body: String) : this(
body = body,
timestamp = m.message.timestamp,
read = true,
seen = true,
sent = true,
local = true,
id = m.message.id.bytes,
groupId = m.message.groupId.bytes,
contactId = contactId.int
PrivateMessageHeader(
m.message.id,
m.message.groupId,
m.message.timestamp,
true,
true,
true,
true
), contactId, body
)
}
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
@file:Suppress("unused")
package org.briarproject.briar.headless.messaging
......@@ -21,22 +21,28 @@ internal abstract class OutputPrivateRequest(header: PrivateRequest<*>, contactI
}
@Immutable
internal class OutputIntroductionRequest(header: IntroductionRequest, contactId: ContactId) :
OutputPrivateRequest(header, contactId) {
internal data class OutputIntroductionRequest(
override val iHeader: IntroductionRequest,
override val iContactId: ContactId
) : OutputPrivateRequest(iHeader, iContactId) {
override val type = "org.briarproject.briar.api.introduction.IntroductionRequest"
val alreadyContact = header.isContact
val alreadyContact get() = iHeader.isContact
}
@Immutable
internal class OutputInvitationRequest(header: InvitationRequest<*>, contactId: ContactId) :
OutputPrivateRequest(header, contactId) {
internal data class OutputInvitationRequest(
override val iHeader: InvitationRequest<*>,
override val iContactId: ContactId
) : OutputPrivateRequest(iHeader, iContactId) {
override val type = when (header) {
override val type = when (iHeader) {
is ForumInvitationRequest -> "org.briarproject.briar.api.forum.ForumInvitationRequest"
is BlogInvitationRequest -> "org.briarproject.briar.api.blog.BlogInvitationRequest"
is GroupInvitationRequest -> "org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest"
else -> throw AssertionError("Unknown InvitationRequest")
}
val canBeOpened = header.canBeOpened()
val canBeOpened get() = iHeader.canBeOpened()
}
......@@ -21,23 +21,27 @@ internal abstract class OutputPrivateResponse(header: PrivateResponse, contactId
}
@Immutable
internal class OutputIntroductionResponse(header: IntroductionResponse, contactId: ContactId) :
OutputPrivateResponse(header, contactId) {
internal data class OutputIntroductionResponse(
override val iHeader: IntroductionResponse,
override val iContactId: ContactId
) : OutputPrivateResponse(iHeader, iContactId) {
override val type = "org.briarproject.briar.api.introduction.IntroductionResponse"
val introducedAuthor = header.introducedAuthor.output()
val introducer = header.isIntroducer
val introducedAuthor get() = iHeader.introducedAuthor.output()
val introducer get() = iHeader.isIntroducer
}
@Immutable
internal class OutputInvitationResponse(header: InvitationResponse, contactId: ContactId) :
OutputPrivateResponse(header, contactId) {
internal data class OutputInvitationResponse(
override val iHeader: InvitationResponse,
override val iContactId: ContactId
) : OutputPrivateResponse(iHeader, iContactId) {
override val type = when (header) {
override val type = when (iHeader) {
is ForumInvitationResponse -> "org.briarproject.briar.api.forum.ForumInvitationResponse"
is BlogInvitationResponse -> "org.briarproject.briar.api.blog.BlogInvitationResponse"
is GroupInvitationResponse -> "org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse"
else -> throw AssertionError("Unknown InvitationResponse")
}
val shareableId: ByteArray = header.shareableId.bytes
val shareableId: ByteArray get() = iHeader.shareableId.bytes
}
package org.briarproject.briar.headless
import io.javalin.Context
import io.javalin.core.util.ContextUtil
import io.mockk.mockk
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.identity.Author
import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.identity.LocalAuthor
import org.briarproject.bramble.api.sync.Group
import org.briarproject.bramble.api.sync.Message
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.test.TestUtils.*
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.skyscreamer.jsonassert.JSONAssert.assertEquals
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
abstract class ControllerTest {
protected val contactManager = mockk<ContactManager>()
protected val identityManager = mockk<IdentityManager>()
protected val clock = mockk<Clock>()
protected val ctx = mockk<Context>()
private val request = mockk<HttpServletRequest>(relaxed = true)
private val response = mockk<HttpServletResponse>(relaxed = true)
private val outputCtx = ContextUtil.init(request, response)
protected val group: Group = getGroup(getClientId(), 0)
protected val author: Author = getAuthor()
protected val localAuthor: LocalAuthor = getLocalAuthor()
protected val contact = Contact(ContactId(1), author, localAuthor.id, true, true)
protected val message: Message = getMessage(group.id)
protected val body: String = getRandomString(5)
protected val timestamp = 42L
protected fun assertJsonEquals(json: String, obj: Any) {
assertEquals(json, outputCtx.json(obj).resultString(), false)
}
}
package org.briarproject.briar.headless.blogs
import io.javalin.Context
import io.javalin.BadRequestResponse
import io.javalin.json.JavalinJson.toJson
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verifySequence
import org.briarproject.bramble.api.identity.Author.Status.OURSELVES
import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.test.TestUtils.*
import org.briarproject.bramble.api.sync.MessageId
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.blog.Blog
import org.briarproject.briar.api.blog.BlogManager
import org.briarproject.briar.api.blog.BlogPostFactory
import org.briarproject.briar.api.blog.BlogPostHeader
import org.briarproject.briar.api.blog.*
import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_BODY_LENGTH
import org.briarproject.briar.api.blog.MessageType.POST
import org.junit.jupiter.api.Assertions.assertEquals
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.output
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
class BlogControllerTest {
internal class BlogControllerTest : ControllerTest() {
private val blogManager = mockk<BlogManager>()
private val blogPostFactory = mockk<BlogPostFactory>()
private val identityManager = mockk<IdentityManager>()
private val clock = mockk<Clock>()
private val ctx = mockk<Context>()
private val blogController =
BlogController(blogManager, blogPostFactory, identityManager, clock)
private val controller =
BlogController(blogManager, blogPostFactory, identityManager, clock)
private val group = getGroup(getClientId(), 0)
private val author = getAuthor()
private val blog = Blog(group, author, false)
private val message = getMessage(group.id)
private val body = getRandomString(5)
private val parentId: MessageId? = null
private val rssFeed = false
private val read = true
private val header = BlogPostHeader(
POST,
group.id,
message.id,
parentId,
message.timestamp,
timestamp,
author,
OURSELVES,
rssFeed,
read
)
@Test
fun testCreate() {
val post = BlogPost(message, null, localAuthor)
every { ctx.formParam("text") } returns body
every { identityManager.localAuthor } returns localAuthor
every { blogManager.getPersonalBlog(localAuthor) } returns blog
every { clock.currentTimeMillis() } returns message.timestamp
every {
blogPostFactory.createBlogPost(
message.groupId,
message.timestamp,
parentId,
localAuthor,
body
)
} returns post
every { blogManager.addLocalPost(post) } just Runs
every { blogManager.getPostHeader(post.message.groupId, post.message.id) } returns header
every { ctx.json(header.output(body)) } returns ctx
controller.createPost(ctx)
}
@Test
fun testCreateNoText() {
every { ctx.formParam("text") } returns null
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testCreateEmptyText() {
every { ctx.formParam("text") } returns ""
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testCreateTooLongText() {
every { ctx.formParam("text") } returns getRandomString(MAX_BLOG_POST_BODY_LENGTH + 1)
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testList() {
val header = BlogPostHeader(POST, group.id, message.id, null, 0, 0, author, OURSELVES, true,
true)
val slot = slot<List<OutputBlogPost>>()
every { blogManager.blogs } returns listOf(blog)
every { blogManager.getPostHeaders(group.id) } returns listOf(header)
every { blogManager.getPostBody(message.id) } returns body
every { ctx.json(listOf(header.output(body))) } returns ctx
controller.listPosts(ctx)
}
@Test
fun testEmptyList() {
every { blogManager.blogs } returns listOf(blog)
every { blogManager.getPostHeaders(any()) } returns listOf(header)
every { blogManager.getPostBody(any()) } returns body
every { ctx.json(capture(slot)) } returns ctx
blogController.listPosts(ctx)
assertEquals(1, slot.captured.size)
assertEquals(header.id.bytes, slot.captured[0].id)
assertEquals(body, slot.captured[0].body)
verifySequence {
blogManager.blogs
blogManager.getPostHeaders(group.id)
blogManager.getPostBody(message.id)
ctx.json(slot.captured)
}
every { blogManager.getPostHeaders(group.id) } returns emptyList()
every { ctx.json(emptyList<OutputBlogPost>()) } returns ctx
controller.listPosts(ctx)
}
@Test
fun testOutputBlogPost() {
val json = """
{
"body": "$body",
"author": ${toJson(author.output())},
"authorStatus": "ourselves",
"type": "post",
"id": ${toJson(header.id.bytes)},
"parentId": $parentId,
"read": $read,
"rssFeed": $rssFeed,
"timestamp": ${message.timestamp},
"timestampReceived": $timestamp
}
"""
assertJsonEquals(json, header.output(body))
}
}
package org.briarproject.briar.headless.contact
import io.javalin.json.JavalinJson.toJson
import io.mockk.every
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.output
import org.junit.jupiter.api.Test
internal class ContactControllerTest : ControllerTest() {
private val controller = ContactController(contactManager)
@Test
fun testEmptyContactList() {
every { contactManager.activeContacts } returns emptyList<Contact>()
every { ctx.json(emptyList<OutputContact>()) } returns ctx
controller.list(ctx)
}
@Test
fun testList() {
every { contactManager.activeContacts } returns listOf(contact)
every { ctx.json(listOf(contact.output())) } returns ctx
controller.list(ctx)
}
@Test
fun testOutputContact() {
val json = """
{
"id": ${contact.id.int},
"author": ${toJson(author.output())},
"verified": ${contact.isVerified}
}
"""
assertJsonEquals(json, contact.output())
}
@Test
fun testOutputAuthor() {
val json = """
{
"id": ${toJson(author.id.bytes)},
"name": "${author.name}",
"publicKey": ${toJson(author.publicKey)}
}
"""
assertJsonEquals(json, author.output())
}
}
package org.briarproject.briar.headless.forums
import io.javalin.BadRequestResponse
import io.mockk.every
import io.mockk.mockk
import org.briarproject.bramble.test.TestUtils.getRandomBytes
import org.briarproject.bramble.util.StringUtils.getRandomString