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
1 change: 1 addition & 0 deletions build-plugin/src/main/kotlin/ThunderbirdProjectConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ object ThunderbirdProjectConfig {
object Compiler {
val javaCompatibility = JavaVersion.VERSION_11
val jvmTarget = JvmTarget.JVM_11
val javaVersion = JavaVersion.VERSION_11
}
}
9 changes: 9 additions & 0 deletions feature/notification/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ kotlin {
implementation(projects.core.ui.compose.designsystem)
implementation(projects.core.ui.compose.theme2.common)
}
jvmTest.dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.bundles.shared.jvm.test)
}
}

sourceSets.all {
Expand All @@ -32,6 +36,11 @@ android {
namespace = "net.thunderbird.feature.notification.api"
}

java {
sourceCompatibility = ThunderbirdProjectConfig.Compiler.javaVersion
targetCompatibility = ThunderbirdProjectConfig.Compiler.javaVersion
}

compose.resources {
publicResClass = false
packageOfResClass = "net.thunderbird.feature.notification.resources.api"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package net.thunderbird.feature.notification.api.sender.compat

import androidx.annotation.Discouraged
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.sender.NotificationSender

/**
* A compatibility layer for sending notifications from Java code.
*
* This class wraps [NotificationSender] and provides a Java-friendly API for sending notifications
* and receiving results via a callback interface.
*
* It is marked as [Discouraged] because it is intended only for use within Java classes.
* Kotlin code should use [NotificationSender] directly.
*
* @property notificationSender The underlying [NotificationSender] instance.
* @property mainImmediateDispatcher The [CoroutineDispatcher] used for launching coroutines.
*/
@Discouraged("Only for usage within a Java class. Use NotificationSender instead.")
class NotificationSenderCompat @JvmOverloads constructor(
private val notificationSender: NotificationSender,
mainImmediateDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
) : DisposableHandle {
private val scope = CoroutineScope(SupervisorJob() + mainImmediateDispatcher)

fun send(notification: Notification, onResultListener: OnResultListener) {
notificationSender.send(notification)
.onEach { outcome -> onResultListener.onResult(outcome) }
.launchIn(scope)
}

override fun dispose() {
scope.cancel()
}

fun interface OnResultListener {
fun onResult(outcome: Outcome<Success<Notification>, Failure<Notification>>)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.thunderbird.feature.notification.api.sender.compat

import assertk.assertThat
import assertk.assertions.containsExactlyInAnyOrder
import dev.mokkery.matcher.any
import dev.mokkery.spy
import dev.mokkery.verify
import dev.mokkery.verify.VerifyMode.Companion.exactly
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.NotificationCommandException
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.testing.fake.FakeNotification
import net.thunderbird.feature.notification.testing.fake.command.FakeInAppNotificationCommand
import net.thunderbird.feature.notification.testing.fake.command.FakeSystemNotificationCommand
import net.thunderbird.feature.notification.testing.fake.sender.FakeNotificationSender

class NotificationSenderCompatTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `send should call listener callback whenever a result is received`() {
// Arrange
val expectedResults = listOf<Outcome<Success<Notification>, Failure<Notification>>>(
Outcome.success(Success(FakeInAppNotificationCommand())),
Outcome.success(Success(FakeSystemNotificationCommand())),
Outcome.failure(
error = Failure(
command = FakeSystemNotificationCommand(),
throwable = NotificationCommandException("What an issue?"),
),
),
)
val sender = FakeNotificationSender(results = expectedResults)
val actualResults = mutableListOf<Outcome<Success<Notification>, Failure<Notification>>>()
val listener = spy(
NotificationSenderCompat.OnResultListener { outcome ->
actualResults += outcome
},
)
val testSubject = NotificationSenderCompat(
notificationSender = sender,
mainImmediateDispatcher = UnconfinedTestDispatcher(),
)

// Act
testSubject.send(notification = FakeNotification(), listener)

// Assert
verify(exactly(expectedResults.size)) {
listener.onResult(outcome = any())
}
assertThat(actualResults).containsExactlyInAnyOrder(elements = expectedResults.toTypedArray())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package net.thunderbird.feature.notification.api.sender.compat;


import java.util.ArrayList;
import java.util.List;

import kotlinx.coroutines.Dispatchers;
import kotlinx.coroutines.test.TestCoroutineDispatchersKt;
import kotlinx.coroutines.test.TestDispatcher;
import kotlinx.coroutines.test.TestDispatchers;
import net.thunderbird.core.outcome.Outcome;
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure;
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success;
import net.thunderbird.feature.notification.api.command.NotificationCommandException;
import net.thunderbird.feature.notification.api.content.Notification;
import net.thunderbird.feature.notification.testing.fake.FakeNotification;
import net.thunderbird.feature.notification.testing.fake.command.FakeInAppNotificationCommand;
import net.thunderbird.feature.notification.testing.fake.command.FakeSystemNotificationCommand;
import net.thunderbird.feature.notification.testing.fake.sender.FakeNotificationSender;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;


public class NotificationSenderCompatJavaTest {
private TestDispatcher testDispatcher;

@Before
public void setUp() {
testDispatcher = TestCoroutineDispatchersKt.UnconfinedTestDispatcher(null, null);
TestDispatchers.setMain(Dispatchers.INSTANCE, testDispatcher);
}

@After
public void tearDown() {
// restore original Main
TestDispatchers.resetMain(Dispatchers.INSTANCE);
}

@Test
public void send_shouldCallListenerCallback_wheneverAResultIsReceived() {
// Arrange
@SuppressWarnings("unchecked") final List<
Outcome<
? extends @NotNull Success<? extends @NotNull Notification>,
? extends @NotNull Failure<? extends @NotNull Notification>
>
> expectedResults = List.of(
Outcome.Companion.success(new Success<>(new FakeInAppNotificationCommand())),
Outcome.Companion.success(new Success<>(new FakeSystemNotificationCommand())),
Outcome.Companion.failure(
new Failure<>(
new FakeSystemNotificationCommand(),
new NotificationCommandException("What an issue?")
)
)
);


final FakeNotificationSender sender = new FakeNotificationSender(expectedResults);

final ResultListener listener = new ResultListener();
final NotificationSenderCompat.OnResultListener spyListener = spy(listener);

final NotificationSenderCompat testSubject = new NotificationSenderCompat(sender, testDispatcher);

// Act
testSubject.send(new FakeNotification(), spyListener);

// Assert
verify(spyListener, times(expectedResults.size())).onResult(any());
assertEquals(expectedResults, listener.actualResults);
}

private static class ResultListener implements NotificationSenderCompat.OnResultListener {
final ArrayList<
Outcome<
? extends @NotNull Success<? extends @NotNull Notification>,
? extends @NotNull Failure<? extends @NotNull Notification>
>
> actualResults = new ArrayList<>();

@Override
public void onResult(
@NotNull Outcome<? extends @NotNull Success<? extends @NotNull Notification>, ? extends @NotNull Failure<? extends @NotNull Notification>> outcome) {
actualResults.add(outcome);
}
}
}
1 change: 1 addition & 0 deletions feature/notification/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ android {
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.outcome)
api(projects.feature.notification.api)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.thunderbird.feature.notification.testing.fake.command

import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.InAppNotification
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
import net.thunderbird.feature.notification.testing.fake.FakeInAppOnlyNotification
import net.thunderbird.feature.notification.testing.fake.receiver.FakeInAppNotificationNotifier

class FakeInAppNotificationCommand(
notification: InAppNotification = FakeInAppOnlyNotification(),
notifier: NotificationNotifier<InAppNotification> = FakeInAppNotificationNotifier(),
) : NotificationCommand<InAppNotification>(notification, notifier) {
override suspend fun execute(): Outcome<Success<InAppNotification>, Failure<InAppNotification>> =
error("not implemented")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.thunderbird.feature.notification.testing.fake.command

import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.SystemNotification
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
import net.thunderbird.feature.notification.testing.fake.FakeSystemOnlyNotification
import net.thunderbird.feature.notification.testing.fake.receiver.FakeSystemNotificationNotifier

class FakeSystemNotificationCommand(
notification: SystemNotification = FakeSystemOnlyNotification(),
notifier: NotificationNotifier<SystemNotification> = FakeSystemNotificationNotifier(),
) : NotificationCommand<SystemNotification>(notification, notifier) {
override suspend fun execute(): Outcome<Success<SystemNotification>, Failure<SystemNotification>> =
error("not implemented")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.thunderbird.feature.notification.testing.fake.receiver

import net.thunderbird.feature.notification.api.NotificationId
import net.thunderbird.feature.notification.api.content.SystemNotification
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier

class FakeSystemNotificationNotifier : NotificationNotifier<SystemNotification> {
override suspend fun show(
id: NotificationId,
notification: SystemNotification,
) = Unit

override fun dispose() = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.thunderbird.feature.notification.testing.fake.sender

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.sender.NotificationSender

class FakeNotificationSender(
private val results: List<Outcome<Success<Notification>, Failure<Notification>>>,
) : NotificationSender {
override fun send(notification: Notification): Flow<Outcome<Success<Notification>, Failure<Notification>>> =
results.asFlow()
}