Files
honeyDueKMP/composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/notifications/FcmServiceTest.kt
Trey T 0d50726490 P4 Stream N: FCM service + NotificationChannels matching iOS categories
FcmService + NotificationPayload + 4 NotificationChannels (task_reminder,
task_overdue, residence_invite, subscription) parity with iOS
NotificationCategories.swift. Deep-link routing from payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:45:37 -05:00

184 lines
5.7 KiB
Kotlin

package com.tt.honeyDue.notifications
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.google.firebase.messaging.RemoteMessage
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* Tests for [FcmService] — verify channel routing and deep-link handling
* when data messages arrive.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
class FcmServiceTest {
private lateinit var context: Context
private lateinit var manager: NotificationManager
private lateinit var service: FcmService
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notificationChannels.forEach { manager.deleteNotificationChannel(it.id) }
manager.cancelAll()
service = Robolectric.setupService(FcmService::class.java)
}
private fun remoteMessage(id: String, data: Map<String, String>): RemoteMessage {
val b = RemoteMessage.Builder("test@fcm")
b.setMessageId(id)
data.forEach { (k, v) -> b.addData(k, v) }
return b.build()
}
@Test
fun onMessageReceived_routesTaskReminder_toCorrectChannel() {
val msg = remoteMessage(
id = "m-1",
data = mapOf(
"type" to "task_reminder",
"task_id" to "7",
"title" to "Reminder",
"body" to "Do it"
)
)
service.onMessageReceived(msg)
// Channel must have been created and a notification posted on it.
val channel = manager.getNotificationChannel(NotificationChannels.TASK_REMINDER)
assertNotNull("task_reminder channel should be created", channel)
val posted = manager.activeNotifications
assertTrue("one notification should be posted", posted.isNotEmpty())
val n = posted.first()
assertEquals(NotificationChannels.TASK_REMINDER, n.notification.channelId)
}
@Test
fun onMessageReceived_routesTaskOverdue_toHighChannel() {
val msg = remoteMessage(
id = "m-2",
data = mapOf(
"type" to "task_overdue",
"task_id" to "8",
"title" to "Overdue",
"body" to "Late"
)
)
service.onMessageReceived(msg)
val posted = manager.activeNotifications
assertTrue(posted.isNotEmpty())
assertEquals(NotificationChannels.TASK_OVERDUE, posted.first().notification.channelId)
}
@Test
fun onMessageReceived_routesResidenceInvite() {
val msg = remoteMessage(
id = "m-3",
data = mapOf(
"type" to "residence_invite",
"residence_id" to "42",
"title" to "Invite",
"body" to "Join us"
)
)
service.onMessageReceived(msg)
val posted = manager.activeNotifications
assertTrue(posted.isNotEmpty())
assertEquals(NotificationChannels.RESIDENCE_INVITE, posted.first().notification.channelId)
}
@Test
fun onMessageReceived_routesSubscription_toLowChannel() {
val msg = remoteMessage(
id = "m-4",
data = mapOf(
"type" to "subscription",
"title" to "Sub",
"body" to "Changed"
)
)
service.onMessageReceived(msg)
val posted = manager.activeNotifications
assertTrue(posted.isNotEmpty())
assertEquals(NotificationChannels.SUBSCRIPTION, posted.first().notification.channelId)
}
@Test
fun onMessageReceived_withTaskId_sets_deep_link() {
val msg = remoteMessage(
id = "m-5",
data = mapOf(
"type" to "task_reminder",
"task_id" to "123",
"title" to "T",
"body" to "B",
"deep_link" to "honeydue://task/123"
)
)
service.onMessageReceived(msg)
val posted = manager.activeNotifications
assertTrue(posted.isNotEmpty())
val contentIntent = posted.first().notification.contentIntent
assertNotNull("content intent should be attached for deep-link tap", contentIntent)
}
@Test
fun onMessageReceived_malformedPayload_postsNothing() {
// Missing type → payload parse returns null → nothing posted.
val msg = remoteMessage(
id = "m-6",
data = mapOf(
"title" to "x",
"body" to "y"
)
)
service.onMessageReceived(msg)
assertTrue(
"no notification should be posted for malformed payload",
manager.activeNotifications.isEmpty()
)
}
@Test
fun onMessageReceived_distinctMessageIds_produceDistinctNotifications() {
service.onMessageReceived(
remoteMessage(
"id-A",
mapOf("type" to "task_reminder", "task_id" to "1", "title" to "A", "body" to "a")
)
)
service.onMessageReceived(
remoteMessage(
"id-B",
mapOf("type" to "task_reminder", "task_id" to "2", "title" to "B", "body" to "b")
)
)
assertEquals(2, manager.activeNotifications.size)
}
}