test(reporting): ReportUpload testability seams + push IdleService E2E

Coverage lane 4 (#249) flagged reporting/push classes as unreachable by tests.
Add minimal, behaviour-preserving seams and the tests they unblock (issue #257):

- ReportUploadScheduler: inject Provider<WorkManager> (mirroring SyncScheduler)
  instead of calling the WorkManager.getInstance static that MockK can't stub on
  the abstract WorkManager (AbstractMethodError). New ReportUploadSchedulerTest
  pins the per-report unique-work name + REPLACE policy.
- ReportUploadWorker: take the ingest endpoint via a new @DebugReportEndpoint
  qualifier (provided from BuildConfig.DEBUG_REPORT_ENDPOINT in ReportingModule)
  rather than reading the BuildConfig static inline. New ReportUploadWorkerHttpTest
  drives the transmit path against an in-process JDK HttpServer on loopback and
  covers 2xx success + delete, 4xx failure, 5xx retry/attempt-cap, and network
  error. Production value is unchanged (empty by default).
- IdleService: extract the foreground-notification channel + push-mode-to-text
  logic into PushStatusNotification. New PushStatusNotificationInstrumentedTest
  asserts channel importance and the IDLE/POLLING notification text with a real
  application Context (never a mocked Context).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 23:58:00 -05:00
co-authored by Claude Opus 4.8
parent 165699d950
commit 0b44276909
10 changed files with 382 additions and 44 deletions
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.push
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.libremail.R
import org.libremail.data.sync.PushMode
/**
* On-device coverage of [PushStatusNotification] — the foreground-notification logic [IdleService]
* delegates to (issue #257). `NotificationChannel`/`NotificationCompat` are no-op stubs in the
* unit-test `android.jar`, so this is the only place the real channel importance and the push-mode →
* text branch can be asserted. It uses the real application `Context` (a `ContextWrapper`, never a
* mocked `Context`), mirroring `BatteryOptimizationManagerIntentTest`, and touches no service
* lifecycle, Hilt graph, or network — so it is deterministic and side-effect-free beyond registering a
* low-importance notification channel.
*/
@RunWith(AndroidJUnit4::class)
class PushStatusNotificationInstrumentedTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
@Test
fun ensureChannel_registersALowImportancePushChannel() {
PushStatusNotification.ensureChannel(context)
val channel = requireNotNull(
NotificationManagerCompat.from(context).getNotificationChannel(PushStatusNotification.CHANNEL_ID),
) { "push status channel must be registered" }
assertEquals(NotificationManager.IMPORTANCE_LOW, channel.importance)
}
@Test
fun build_inIdleMode_saysConnectedForInstantDeliveryAndIsAnOngoingServiceNotification() {
val notification = PushStatusNotification.build(context, PushMode.IDLE)
assertEquals(
context.getString(R.string.notif_push_status_title),
notification.extras.getCharSequence(Notification.EXTRA_TITLE).toString(),
)
assertEquals(
context.getString(R.string.notif_push_status_text),
notification.extras.getCharSequence(Notification.EXTRA_TEXT).toString(),
)
assertTrue(
"status notification must be ongoing",
(notification.flags and Notification.FLAG_ONGOING_EVENT) != 0,
)
assertEquals(Notification.CATEGORY_SERVICE, notification.category)
}
@Test
fun build_inPollingMode_saysLowBatteryFallback() {
val notification = PushStatusNotification.build(context, PushMode.POLLING)
assertEquals(
context.getString(R.string.notif_push_status_text_low_battery),
notification.extras.getCharSequence(Notification.EXTRA_TEXT).toString(),
)
}
}