Fix Android notification service code review issues

- Fix invalid notificationManager extension property (line 60)
- Remove invalid getChannelId() calls (line 124)
- Fix NotificationCompat.Builder to use method chaining (line 140)
- Remove undefined newIntent() call (line 154)
- Add unit tests for NotificationService, NotificationManager, NotificationPreferences

All fixes address issues identified in code review for FRE-536.
This commit is contained in:
2026-03-31 06:24:19 -04:00
parent f8d696a440
commit 9ce750bed6
3 changed files with 486 additions and 14 deletions

View File

@@ -0,0 +1,188 @@
package com.rssuper.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PermissionGrantState
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
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.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
class NotificationServiceTest {
private lateinit var context: Context
private lateinit var notificationManager: NotificationManager
private lateinit var notificationService: NotificationService
@Before
fun setup() {
context = mock()
notificationManager = mock()
notificationService = NotificationService(context)
}
@Test
fun testCreateNotificationChannels_OreoPlus() = runTest {
whenever(context.getSystemService(Context.NOTIFICATION_SERVICE))
.doReturn(notificationManager)
notificationService.createNotificationChannels()
verify(notificationManager).createNotificationChannel(any<NotificationChannel>())
}
@Test
fun testHasNotificationPermission_belowTiramisu() = runTest {
whenever(context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS))
.doReturn(PackageManager.PERMISSION_GRANTED)
val hasPermission = notificationService.hasNotificationPermission()
assertTrue(hasPermission)
}
@Test
fun testHasNotificationPermission_granted() = runTest {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
whenever(context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS))
.doReturn(PackageManager.PERMISSION_GRANTED)
val hasPermission = notificationService.hasNotificationPermission()
assertTrue(hasPermission)
}
}
@Test
fun testHasNotificationPermission_denied() = runTest {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
whenever(context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS))
.doReturn(PackageManager.PERMISSION_DENIED)
val hasPermission = notificationService.hasNotificationPermission()
assertFalse(hasPermission)
}
}
@Test
fun testShowNotification_permissionGranted() = runTest {
val mockNotificationManager = mock<NotificationManagerCompat>()
whenever(NotificationManagerCompat.from(context)).doReturn(mockNotificationManager)
whenever(mockNotificationManager.notify(any<Int>(), any<Notification>())).doReturn(true)
val result = notificationService.showNotification("Test Title", "Test Body")
assertTrue(result)
}
@Test
fun testShowNotification_permissionDenied() = runTest {
val mockNotificationManager = mock<NotificationManagerCompat>()
whenever(NotificationManagerCompat.from(context)).doReturn(mockNotificationManager)
whenever(mockNotificationManager.notify(any<Int>(), any<Notification>())).doReturn(false)
val result = notificationService.showNotification("Test Title", "Test Body")
assertFalse(result)
}
@Test
fun testShowLocalNotification() = runTest {
val mockNotificationManager = mock<NotificationManagerCompat>()
whenever(NotificationManagerCompat.from(context)).doReturn(mockNotificationManager)
whenever(mockNotificationManager.notify(any<Int>(), any<Notification>())).doReturn(true)
val result = notificationService.showLocalNotification("Test Title", "Test Body")
assertTrue(result)
}
@Test
fun testShowPushNotification() = runTest {
val mockNotificationManager = mock<NotificationManagerCompat>()
whenever(NotificationManagerCompat.from(context)).doReturn(mockNotificationManager)
whenever(mockNotificationManager.notify(any<Int>(), any<Notification>())).doReturn(true)
val result = notificationService.showPushNotification("Test Title", "Test Body")
assertTrue(result)
}
@Test
fun testShowNotificationWithAction() = runTest {
val mockNotificationManager = mock<NotificationManagerCompat>()
whenever(NotificationManagerCompat.from(context)).doReturn(mockNotificationManager)
whenever(mockNotificationManager.notify(any<Int>(), any<Notification>())).doReturn(true)
val actionIntent: Intent = mock()
val result = notificationService.showNotificationWithAction(
"Test Title",
"Test Body",
"Action Label",
PendingIntent.getActivity(context, 0, actionIntent, PendingIntent.FLAG_IMMUTABLE)
)
assertTrue(result)
}
@Test
fun testUpdateBadgeCount() = runTest {
notificationService.updateBadgeCount(5)
}
@Test
fun testClearAllNotifications() = runTest {
val mockNotificationManager = mock<NotificationManager>()
whenever(context.getSystemService(Context.NOTIFICATION_SERVICE))
.doReturn(mockNotificationManager)
notificationService.clearAllNotifications()
verify(mockNotificationManager).cancelAll()
}
@Test
fun testGetPreferences() = runTest {
val preferences = notificationService.getPreferences()
assertNotNull(preferences)
assertEquals(true, preferences.newArticles)
assertEquals(true, preferences.episodeReleases)
}
@Test
fun testSavePreferences() = runTest {
val preferences = NotificationPreferences(
newArticles = true,
episodeReleases = false
)
notificationService.savePreferences(preferences)
val retrieved = notificationService.getPreferences()
assertEquals(true, retrieved.newArticles)
assertEquals(false, retrieved.episodeReleases)
}
}

View File

@@ -57,7 +57,7 @@ class NotificationService : Service() {
* Create notification channels * Create notification channels
*/ */
private fun createNotificationChannels() { private fun createNotificationChannels() {
val notificationManager = context?.notificationManager val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
// Critical notifications channel // Critical notifications channel
val criticalChannel = NotificationChannel( val criticalChannel = NotificationChannel(
@@ -119,9 +119,9 @@ class NotificationService : Service() {
) { ) {
val notificationManager = notificationManager ?: return val notificationManager = notificationManager ?: return
// Get appropriate notification channel val channelId = when (urgency) {
val channel: NotificationChannel? = when (urgency) { NotificationUrgency.CRITICAL -> NOTIFICATION_CHANNEL_ID_CRITICAL
NotificationUrgency.CRITICAL -> { notificationManager.getChannelId(NOTIFICATION_CHANNEL_ID_CRITICAL) } else -> { notificationManager.getChannelId(NOTIFICATION_CHANNEL_ID) } else -> NOTIFICATION_CHANNEL_ID
} }
// Create notification intent // Create notification intent
@@ -137,21 +137,18 @@ class NotificationService : Service() {
) )
// Create notification builder // Create notification builder
val builder = NotificationCompat.Builder(this, channel) { val builder = NotificationCompat.Builder(this, channelId)
setSmallIcon(icon) .setSmallIcon(icon)
setAutoCancel(true) .setAutoCancel(true)
setPriority(when (urgency) { .setPriority(when (urgency) {
NotificationUrgency.CRITICAL -> NotificationCompat.PRIORITY_HIGH NotificationUrgency.CRITICAL -> NotificationCompat.PRIORITY_HIGH
NotificationUrgency.LOW -> NotificationCompat.PRIORITY_LOW NotificationUrgency.LOW -> NotificationCompat.PRIORITY_LOW
else -> NotificationCompat.PRIORITY_DEFAULT else -> NotificationCompat.PRIORITY_DEFAULT
}) })
setContentTitle(title) .setContentTitle(title)
setContentText(text) .setContentText(text)
setStyle(NotificationCompat.BigTextStyle().bigText(text)) .setStyle(NotificationCompat.BigTextStyle().bigText(text))
}
// Add extra data
builder.setExtras(newIntent())
builder.setCategory(NotificationCompat.CATEGORY_MESSAGE) builder.setCategory(NotificationCompat.CATEGORY_MESSAGE)
builder.setSound(null) builder.setSound(null)

View File

@@ -0,0 +1,287 @@
package com.rssuper
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ApplicationTestCase
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.*
/**
* NotificationServiceTests - Unit tests for NotificationService
*/
class NotificationServiceTests : ApplicationTestCase<Context>() {
private lateinit var context: Context
private lateinit var notificationService: NotificationService
override val packageName: String get() = "com.rssuper"
@Before
fun setUp() {
context = getTargetContext()
notificationService = NotificationService()
notificationService.initialize(context)
}
@Test
fun testNotificationService_initialization() {
assertNotNull("NotificationService should be initialized", notificationService)
assertNotNull("Context should be set", notificationService.getContext())
}
@Test
fun testNotificationService_getInstance() {
val instance = notificationService.getInstance()
assertNotNull("Instance should not be null", instance)
assertEquals("Instance should be the same object", notificationService, instance)
}
@Test
fun testNotificationService_getNotificationId() {
assertEquals("Notification ID should be 1001", 1001, notificationService.getNotificationId())
}
@Test
fun testNotificationService_getService() {
val service = notificationService.getService()
assertNotNull("Service should not be null", service)
}
@Test
fun testNotificationUrgency_values() {
assertEquals("CRITICAL should be 0", 0, NotificationUrgency.CRITICAL.ordinal)
assertEquals("LOW should be 1", 1, NotificationUrgency.LOW.ordinal)
assertEquals("NORMAL should be 2", 2, NotificationUrgency.NORMAL.ordinal)
}
@Test
fun testNotificationUrgency_critical() {
assertEquals("Critical urgency should be CRITICAL", NotificationUrgency.CRITICAL, NotificationUrgency.CRITICAL)
}
@Test
fun testNotificationUrgency_low() {
assertEquals("Low urgency should be LOW", NotificationUrgency.LOW, NotificationUrgency.LOW)
}
@Test
fun testNotificationUrgency_normal() {
assertEquals("Normal urgency should be NORMAL", NotificationUrgency.NORMAL, NotificationUrgency.NORMAL)
}
@Test
fun testNotificationService_showCriticalNotification() {
// Test that showCriticalNotification calls showNotification with CRITICAL urgency
// Note: This is a basic test - actual notification display would require Android environment
val service = NotificationService()
service.initialize(context)
// Verify the method exists and can be called
assertDoesNotThrow {
service.showCriticalNotification("Test Title", "Test Text", 0)
}
}
@Test
fun testNotificationService_showLowNotification() {
val service = NotificationService()
service.initialize(context)
assertDoesNotThrow {
service.showLowNotification("Test Title", "Test Text", 0)
}
}
@Test
fun testNotificationService_showNormalNotification() {
val service = NotificationService()
service.initialize(context)
assertDoesNotThrow {
service.showNormalNotification("Test Title", "Test Text", 0)
}
}
}
/**
* NotificationManagerTests - Unit tests for NotificationManager
*/
class NotificationManagerTests : ApplicationTestCase<Context>() {
private lateinit var context: Context
private lateinit var notificationManager: NotificationManager
override val packageName: String get() = "com.rssuper"
@Before
fun setUp() {
context = getTargetContext()
notificationManager = NotificationManager(context)
}
@Test
fun testNotificationManager_initialization() {
assertNotNull("NotificationManager should be initialized", notificationManager)
assertNotNull("Context should be set", notificationManager.getContext())
}
@Test
fun testNotificationManager_getPreferences_defaultValues() {
val prefs = notificationManager.getPreferences()
assertTrue("newArticles should default to true", prefs.newArticles)
assertTrue("episodeReleases should default to true", prefs.episodeReleases)
assertTrue("customAlerts should default to true", prefs.customAlerts)
assertTrue("badgeCount should default to true", prefs.badgeCount)
assertTrue("sound should default to true", prefs.sound)
assertTrue("vibration should default to true", prefs.vibration)
}
@Test
fun testNotificationManager_setPreferences() {
val preferences = NotificationPreferences(
newArticles = false,
episodeReleases = false,
customAlerts = false,
badgeCount = false,
sound = false,
vibration = false
)
assertDoesNotThrow {
notificationManager.setPreferences(preferences)
}
val loadedPrefs = notificationManager.getPreferences()
assertEquals("newArticles should match", preferences.newArticles, loadedPrefs.newArticles)
assertEquals("episodeReleases should match", preferences.episodeReleases, loadedPrefs.episodeReleases)
assertEquals("customAlerts should match", preferences.customAlerts, loadedPrefs.customAlerts)
assertEquals("badgeCount should match", preferences.badgeCount, loadedPrefs.badgeCount)
assertEquals("sound should match", preferences.sound, loadedPrefs.sound)
assertEquals("vibration should match", preferences.vibration, loadedPrefs.vibration)
}
@Test
fun testNotificationManager_getNotificationService() {
val service = notificationManager.getNotificationService()
assertNotNull("NotificationService should not be null", service)
}
@Test
fun testNotificationManager_getNotificationManager() {
val mgr = notificationManager.getNotificationManager()
assertNotNull("NotificationManager should not be null", mgr)
}
@Test
fun testNotificationManager_getAppIntent() {
val intent = notificationManager.getAppIntent()
assertNotNull("Intent should not be null", intent)
}
@Test
fun testNotificationManager_getPrefsName() {
assertEquals("Prefs name should be notification_prefs", "notification_prefs", notificationManager.getPrefsName())
}
}
/**
* NotificationPreferencesTests - Unit tests for NotificationPreferences data class
*/
class NotificationPreferencesTests : ApplicationTestCase<Context>() {
private lateinit var context: Context
override val packageName: String get() = "com.rssuper"
@Before
fun setUp() {
context = getTargetContext()
}
@Test
fun testNotificationPreferences_defaultValues() {
val prefs = NotificationPreferences()
assertTrue("newArticles should default to true", prefs.newArticles)
assertTrue("episodeReleases should default to true", prefs.episodeReleases)
assertTrue("customAlerts should default to true", prefs.customAlerts)
assertTrue("badgeCount should default to true", prefs.badgeCount)
assertTrue("sound should default to true", prefs.sound)
assertTrue("vibration should default to true", prefs.vibration)
}
@Test
fun testNotificationPreferences_customValues() {
val prefs = NotificationPreferences(
newArticles = false,
episodeReleases = false,
customAlerts = false,
badgeCount = false,
sound = false,
vibration = false
)
assertFalse("newArticles should be false", prefs.newArticles)
assertFalse("episodeReleases should be false", prefs.episodeReleases)
assertFalse("customAlerts should be false", prefs.customAlerts)
assertFalse("badgeCount should be false", prefs.badgeCount)
assertFalse("sound should be false", prefs.sound)
assertFalse("vibration should be false", prefs.vibration)
}
@Test
fun testNotificationPreferences_partialValues() {
val prefs = NotificationPreferences(newArticles = false, sound = false)
assertFalse("newArticles should be false", prefs.newArticles)
assertTrue("episodeReleases should default to true", prefs.episodeReleases)
assertTrue("customAlerts should default to true", prefs.customAlerts)
assertTrue("badgeCount should default to true", prefs.badgeCount)
assertFalse("sound should be false", prefs.sound)
assertTrue("vibration should default to true", prefs.vibration)
}
@Test
fun testNotificationPreferences_equality() {
val prefs1 = NotificationPreferences(
newArticles = true,
episodeReleases = false,
customAlerts = true,
badgeCount = false,
sound = true,
vibration = false
)
val prefs2 = NotificationPreferences(
newArticles = true,
episodeReleases = false,
customAlerts = true,
badgeCount = false,
sound = true,
vibration = false
)
assertEquals("Preferences with same values should be equal", prefs1, prefs2)
}
@Test
fun testNotificationPreferences_hashCode() {
val prefs1 = NotificationPreferences()
val prefs2 = NotificationPreferences()
assertEquals("Equal objects should have equal hash codes", prefs1.hashCode(), prefs2.hashCode())
}
@Test
fun testNotificationPreferences_copy() {
val prefs1 = NotificationPreferences(newArticles = false)
val prefs2 = prefs1.copy(newArticles = true)
assertFalse("prefs1 newArticles should be false", prefs1.newArticles)
assertTrue("prefs2 newArticles should be true", prefs2.newArticles)
assertEquals("prefs2 should have same other values", prefs1.episodeReleases, prefs2.episodeReleases)
}
}