Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package net.thunderbird.core.architecture.model

import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import kotlin.test.Test
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

private class TestTag

@OptIn(ExperimentalUuidApi::class)
private object TestIdFactory : BaseIdFactory<TestTag>()

@OptIn(ExperimentalUuidApi::class)
class BaseIdFactoryTest {

@Test
fun `given raw UUID when of is called then returns Id wrapping parsed UUID`() {
// Arrange
val raw = "123e4567-e89b-12d3-a456-426655440000"

// Act
val id = TestIdFactory.of(raw)

// Assert
assertThat(id.value).isEqualTo(Uuid.parse(raw))
assertThat(id.asRaw()).isEqualTo(raw)
}

@Test
fun `given create is called twice then returns different Ids`() {
// Arrange + Act
val id1 = TestIdFactory.create()
val id2 = TestIdFactory.create()

// Assert
assertThat(id1).isNotEqualTo(id2)
assertThat(id1.asRaw()).isNotEqualTo(id2.asRaw())
}

@Test
fun `given Id created when of is called with its raw then same Id is returned`() {
// Arrange
val original = TestIdFactory.create()
val raw = original.asRaw()

// Act
val parsed = TestIdFactory.of(raw)

// Assert
assertThat(parsed).isEqualTo(Id<TestTag>(Uuid.parse(raw)))
assertThat(parsed).isEqualTo(original)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.thunderbird.core.architecture.model

import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import kotlin.test.Test
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

class IdTestTag

@OptIn(ExperimentalUuidApi::class)
class IdTest {

@Test
fun `given raw UUID when creating Id then asRaw returns same string and value matches`() {
// Arrange
val raw = "123e4567-e89b-12d3-a456-426655440000"
val uuid = Uuid.parse(raw)

// Act
val id = Id<IdTestTag>(uuid)

// Assert
assertThat(id.value).isEqualTo(uuid)
assertThat(id.asRaw()).isEqualTo(raw)
}

@Test
fun `given two Ids with same UUID then they are equal and have same hashCode`() {
// Arrange
val uuid = Uuid.parse("123e4567-e89b-12d3-a456-426655440000")

// Act
val id1 = Id<IdTestTag>(uuid)
val id2 = Id<IdTestTag>(uuid)

// Assert
assertThat(id1).isEqualTo(id2)
assertThat(id1.hashCode()).isEqualTo(id2.hashCode())
}

@Test
fun `given two Ids with different UUIDs then they are not equal`() {
// Arrange
val id1 = Id<IdTestTag>(Uuid.parse("123e4567-e89b-12d3-a456-426655440000"))
val id2 = Id<IdTestTag>(Uuid.parse("123e4567-e89b-12d3-a456-426655440001"))

// Assert
assertThat(id1).isNotEqualTo(id2)
assertThat(id1.asRaw()).isNotEqualTo(id2.asRaw())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,22 @@ class FeatureFlagResultTest {
)
assertThat(unavailableResult).isEqualTo("Feature is OFF")
}

@Test
fun `isEnabled, isDisabled, isUnavailable, isDisabledOrUnavailable should return correct values`() {
assertThat(FeatureFlagResult.Enabled.isEnabled()).isEqualTo(true)
assertThat(FeatureFlagResult.Enabled.isDisabled()).isEqualTo(false)
assertThat(FeatureFlagResult.Enabled.isUnavailable()).isEqualTo(false)
assertThat(FeatureFlagResult.Enabled.isDisabledOrUnavailable()).isEqualTo(false)

assertThat(FeatureFlagResult.Disabled.isEnabled()).isEqualTo(false)
assertThat(FeatureFlagResult.Disabled.isDisabled()).isEqualTo(true)
assertThat(FeatureFlagResult.Disabled.isUnavailable()).isEqualTo(false)
assertThat(FeatureFlagResult.Disabled.isDisabledOrUnavailable()).isEqualTo(true)

assertThat(FeatureFlagResult.Unavailable.isEnabled()).isEqualTo(false)
assertThat(FeatureFlagResult.Unavailable.isDisabled()).isEqualTo(false)
assertThat(FeatureFlagResult.Unavailable.isUnavailable()).isEqualTo(true)
assertThat(FeatureFlagResult.Unavailable.isDisabledOrUnavailable()).isEqualTo(true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package net.thunderbird.core.outcome

import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import assertk.assertions.isTrue
import kotlin.test.Test
import kotlinx.coroutines.test.runTest

class OutcomeTest {

@Test
fun `given Success when checking then has correct flags and data`() {
// Arrange
val outcome: Outcome<Int, String> = Outcome.success(42)

// Act + Assert
assertThat(outcome.isSuccess).isTrue()
assertThat(outcome.isFailure).isFalse()
val data = (outcome as Outcome.Success).data
assertThat(data).isEqualTo(42)
}

@Test
fun `given Failure when checking then has correct flags and error`() {
// Arrange
val outcome: Outcome<Int, String> = Outcome.failure("error")

// Act + Assert
assertThat(outcome.isFailure).isTrue()
assertThat(outcome.isSuccess).isFalse()
val error = (outcome as Outcome.Failure).error
assertThat(error).isEqualTo("error")
}

@Test
fun `given Success when map is called then transforms success value`() {
// Arrange
val outcome: Outcome<Int, String> = Outcome.Success(7)

// Act
val mapped = outcome.map(
transformSuccess = { it * 2 },
transformFailure = { err, _ -> "$err!" },
)

// Assert
val data = (mapped as Outcome.Success).data
assertThat(data).isEqualTo(14)
}

@Test
fun `given Failure with cause when map is called then transforms failure value and provides cause`() {
// Arrange
val cause = IllegalStateException("cause")
val outcome: Outcome<Int, String> = Outcome.Failure("error", cause)

// Act
val mapped = outcome.map(
transformSuccess = { it * 2 },
transformFailure = { err, receivedCause ->
assertThat(receivedCause).isEqualTo(cause)
"$err-transformed"
},
)

// Assert
val failure = (mapped as Outcome.Failure)
assertThat(failure.error).isEqualTo("error-transformed")
}

@Test
fun `given Success and Failure when mapSuccess is called then only success is transformed`() {
// Arrange & Act
val success = Outcome.Success(3).mapSuccess { it + 1 }
val failure = Outcome.Failure("failure").mapSuccess { 999 }

// Assert
assertThat((success as Outcome.Success).data).isEqualTo(4)
// Failure must be unchanged
assertThat((failure as Outcome.Failure).error).isEqualTo("failure")
}

@Test
fun `given Success and Failure when flatMapSuccess is called then success is flat-mapped and failure passes through`() {
// Arrange & Act
val onSuccess = Outcome.Success(10).flatMapSuccess { value ->
if (value > 5) Outcome.Success("success") else Outcome.Failure("failure")
}
val onFailure: Outcome<String, String> =
Outcome.Failure("failure").flatMapSuccess { Outcome.Success("won't happen") }

// Assert
assertThat((onSuccess as Outcome.Success).data).isEqualTo("success")
assertThat((onFailure as Outcome.Failure).error).isEqualTo("failure")
}

@Test
fun `given Success and Failure when mapFailure is called then only failure is transformed and cause provided`() {
// Arrange & Act
val cause = RuntimeException("cause")
val success = Outcome.Success("success").mapFailure { e: String, _ -> "$e?" }
val failure = Outcome.Failure("fail", cause).mapFailure { e, c ->
assertThat(c).isEqualTo(cause)
999
}

// Assert
assertThat((success as Outcome.Success).data).isEqualTo("success")
assertThat((failure as Outcome.Failure).error).isEqualTo(999)
}

@Test
fun `given Outcome when handle is invoked then calls only the matching callback`() {
// Arrange
var successCalledWith: Int? = null
var failureCalledWith: String? = null

// Act
Outcome.Success(5).handle(
onSuccess = { successCalledWith = it },
onFailure = { failureCalledWith = it },
)
// Assert
assertThat(successCalledWith).isEqualTo(5)
assertThat(failureCalledWith).isNull()

// Arrange
successCalledWith = null
val failureOutcome: Outcome<Int, String> = Outcome.Failure("failure")
// Act
failureOutcome.handle(
onSuccess = { successCalledWith = it },
onFailure = { failureCalledWith = it },
)
// Assert
assertThat(successCalledWith).isNull()
assertThat(failureCalledWith).isEqualTo("failure")
}

@Test
fun `given Outcome when handleAsync is invoked then calls only the matching suspending callback`() = runTest {
// Arrange
var successCalledWith: Int? = null
var failureCalledWith: String? = null

// Act
Outcome.Success(1).handleAsync(
onSuccess = { successCalledWith = it },
onFailure = { failureCalledWith = it },
)
// Assert
assertThat(successCalledWith).isEqualTo(1)
assertThat(failureCalledWith).isNull()

// Arrange
successCalledWith = null
val failureOutcome: Outcome<Int, String> = Outcome.Failure("failure")
// Act
failureOutcome.handleAsync(
onSuccess = { successCalledWith = it },
onFailure = { failureCalledWith = it },
)
// Assert
assertThat(successCalledWith).isNull()
assertThat(failureCalledWith).isEqualTo("failure")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package net.thunderbird.core.testing.coroutines

import assertk.assertThat
import assertk.assertions.isFalse
import assertk.assertions.isNotSameInstanceAs
import assertk.assertions.isTrue
import kotlin.coroutines.ContinuationInterceptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Rule
import org.junit.Test
import org.junit.runner.Description
import org.junit.runners.model.Statement

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRuleTest {

@get:Rule
val rule = MainDispatcherRule(StandardTestDispatcher())

@Test
fun `given rule with StandardTestDispatcher when posting to Main then it is controlled by provided scheduler`() =
runTest(rule.testDispatcher) {
var executed = false

launch(Dispatchers.Main) {
delay(1000)
executed = true
}

repeat(9) {
// wait a bit for the coroutine to start this delays by 900ms total
if (executed) return@repeat
delay(100)
}

// Should not run until time advances on the provided scheduler
assertThat(executed).isFalse()

rule.testDispatcher.scheduler.advanceTimeBy(1000)
rule.testDispatcher.scheduler.advanceUntilIdle()

assertThat(executed).isTrue()
}

@Test
fun `given rule when applied around statement then sets Main and resets after evaluation`() {
// Arrange
val testDispatcher = StandardTestDispatcher()
val localRule = MainDispatcherRule(testDispatcher)

val inner = object : Statement() {
override fun evaluate() {
runTest(testDispatcher) {
withContext(Dispatchers.Main) { /* no-op */ }
}
}
}

// Act
val wrapped = localRule.apply(inner, Description.EMPTY)
wrapped.evaluate()

// Assert
val outcome = runCatching {
var after: CoroutineDispatcher? = null
runTest {
withContext(Dispatchers.Main) {
after = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
}
}
after
}
outcome.onSuccess { after ->
assertThat(after).isNotSameInstanceAs(testDispatcher)
}
}
}
Loading
Loading