Use Notifications Master app ID

This commit is contained in:
ajp_anton
2026-08-16 01:57:44 +00:00
parent 572660a967
commit af3673c2e8
51 changed files with 142 additions and 142 deletions
@@ -0,0 +1,43 @@
package se.ajpanton.notificationsmaster.capture
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import se.ajpanton.notificationsmaster.model.NotificationAction
class GroupSummaryPolicyTest {
private fun snapshot(isGroupSummary: Boolean) = NotificationSnapshot(
key = "key",
packageName = "example.app",
textContents = null,
hasImage = false,
isGroupSummary = isGroupSummary,
isRoutine = false,
)
@Test fun `enabled group-summary logging retains summaries`() {
assertTrue(GroupSummaryPolicy.shouldLog(snapshot(isGroupSummary = true), logGroupSummaries = true))
}
@Test fun `disabled group summaries are skipped without affecting children`() {
assertFalse(GroupSummaryPolicy.shouldLog(snapshot(isGroupSummary = true), logGroupSummaries = false))
assertTrue(GroupSummaryPolicy.shouldLog(snapshot(isGroupSummary = false), logGroupSummaries = false))
}
@Test fun `group summary cancellation is hidden when group summaries are disabled`() {
assertFalse(
GroupSummaryPolicy.shouldLog(
snapshot(isGroupSummary = false),
NotificationAction.GROUP_SUMMARY_CANCELLED,
logGroupSummaries = false,
),
)
assertTrue(
GroupSummaryPolicy.shouldLog(
snapshot(isGroupSummary = false),
NotificationAction.GROUP_SUMMARY_CANCELLED,
logGroupSummaries = true,
),
)
}
}
@@ -0,0 +1,25 @@
package se.ajpanton.notificationsmaster.capture
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NotificationChangeClassifierTest {
private fun snapshot(text: String, routine: Boolean = false) = NotificationSnapshot(
key = "key", packageName = "example.app", textContents = text, hasImage = false,
isGroupSummary = false, isRoutine = routine,
)
@Test fun `text change is an edit`() {
assertTrue(NotificationChangeClassifier.isMeaningfulEdit(snapshot("old"), snapshot("new")))
}
@Test fun `identical repost is not an edit`() {
assertFalse(NotificationChangeClassifier.isMeaningfulEdit(snapshot("same"), snapshot("same")))
}
@Test fun `routine edit respects ignore setting`() {
assertTrue(NotificationChangeClassifier.shouldIgnoreEdit(snapshot("old"), snapshot("new", routine = true), true))
assertFalse(NotificationChangeClassifier.shouldIgnoreEdit(snapshot("old"), snapshot("new", routine = true), false))
}
}
@@ -0,0 +1,32 @@
package se.ajpanton.notificationsmaster.capture
import org.junit.Assert.assertEquals
import org.junit.Test
class NotificationContentsTest {
@Test fun `expanded text replaces the alternate collapsed rendering`() {
val contents = NotificationContents.genericContents(
title = "Sender",
text = "Subject • Email body",
bigText = "Subject\nEmail body",
subText = "account@example.com",
summaryText = null,
textLines = emptyList(),
)
assertEquals("Sender\nSubject\nEmail body\naccount@example.com", contents)
}
@Test fun `collapsed text remains the fallback without expanded text`() {
val contents = NotificationContents.genericContents(
title = "Sender",
text = "Subject • Email body",
bigText = null,
subText = "account@example.com",
summaryText = null,
textLines = emptyList(),
)
assertEquals("Sender\nSubject • Email body\naccount@example.com", contents)
}
}
@@ -0,0 +1,28 @@
package se.ajpanton.notificationsmaster.data
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import javax.crypto.KeyGenerator
class AesGcmCipherTest {
@Test
fun roundTripsDataWithoutLeavingItInCipherText() {
val key = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
val plainText = "private notification content".encodeToByteArray()
val encrypted = AesGcmCipher(key).encrypt(plainText)
assertFalse(encrypted.cipherText.decodeToString().contains("private notification content"))
assertTrue(AesGcmCipher(key).decrypt(encrypted).contentEquals(plainText))
}
@Test(expected = javax.crypto.AEADBadTagException::class)
fun rejectsTamperedCipherText() {
val key = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
val encrypted = AesGcmCipher(key).encrypt("message".encodeToByteArray())
encrypted.cipherText[0] = (encrypted.cipherText[0].toInt() xor 1).toByte()
AesGcmCipher(key).decrypt(encrypted)
}
}
@@ -0,0 +1,67 @@
package se.ajpanton.notificationsmaster.export
import org.junit.Assert.assertFalse
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import se.ajpanton.notificationsmaster.model.NotificationAction
import se.ajpanton.notificationsmaster.model.NotificationLogEntry
import se.ajpanton.notificationsmaster.settings.LogViewSettings
import se.ajpanton.notificationsmaster.settings.TimestampClockFormat
import se.ajpanton.notificationsmaster.settings.TimestampDateFormat
import se.ajpanton.notificationsmaster.settings.TimestampZone
class LogExporterTest {
private val imageEntry = NotificationLogEntry(
recordedAtEpochMillis = 0,
packageName = "example.app",
appName = "Example",
action = NotificationAction.APPEARED,
contents = "A notification [image]",
imageId = "123e4567-e89b-12d3-a456-426614174000",
)
@Test fun `text exports retain an image placeholder`() {
assertTrue(LogExporter.csv(listOf(imageEntry), LogViewSettings()).contains("[image]"))
assertTrue(LogExporter.formatted(listOf(imageEntry), LogViewSettings()).contains("[image]"))
}
@Test fun `html replaces an image placeholder only when a support path exists`() {
val withImage = LogExporter.html(listOf(imageEntry), LogViewSettings()) { "images/${it.imageId}.png" }
val withoutImage = LogExporter.html(listOf(imageEntry), LogViewSettings())
assertTrue(withImage.contains("<img src=\"images/123e4567-e89b-12d3-a456-426614174000.png\""))
assertFalse(withoutImage.contains("<img"))
assertTrue(withoutImage.contains("[image]"))
}
@Test fun `row writers produce the same export as the convenience methods`() {
val settings = LogViewSettings()
val csv = listOf(LogExporter.csvHeader(settings), LogExporter.csvRow(imageEntry, settings)).joinToString("\n")
val widths = LogExporter.headers(settings).map { it.length }.toMutableList()
LogExporter.values(imageEntry, settings).forEachIndexed { index, value ->
widths[index] = maxOf(widths[index], value.length)
}
val formatted = listOf(
LogExporter.formattedRow(LogExporter.headers(settings), widths),
LogExporter.formattedRow(LogExporter.values(imageEntry, settings), widths),
).joinToString("\n")
val html = LogExporter.htmlStart(settings) +
LogExporter.htmlRow(imageEntry, settings, "images/${imageEntry.imageId}.png") +
LogExporter.htmlEnd()
assertEquals(LogExporter.csv(listOf(imageEntry), settings), csv)
assertEquals(LogExporter.formatted(listOf(imageEntry), settings), formatted)
assertEquals(LogExporter.html(listOf(imageEntry), settings) { "images/${it.imageId}.png" }, html)
}
@Test fun `exports use the configured timestamp date and clock formats`() {
val settings = LogViewSettings(
timestampZone = TimestampZone.UTC,
timestampDateFormat = TimestampDateFormat.YEAR_MONTH_DAY,
timestampClockFormat = TimestampClockFormat.HOUR_24,
)
assertTrue(LogExporter.csv(listOf(imageEntry), settings).contains("1970-01-01 00:00:00"))
}
}
@@ -0,0 +1,75 @@
package se.ajpanton.notificationsmaster.settings
import org.junit.Assert.assertEquals
import org.junit.Test
class AppListOrderingTest {
private fun app(
label: String,
seen: Boolean = false,
selected: Boolean = false,
hasEventOverride: Boolean = false,
) = ListedApp(label, "pkg.$label", seen, selected, hasEventOverride)
@Test fun `seen apps are titled and repeated before the full list when requested`() {
val items = AppListOrdering.items(
listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)),
onlySeen = false,
seenAppsFirst = true,
)
assertEquals(listOf("[Seen apps]", "Alpha", "Beta", "|", "[All apps]", "Alpha", "Beta", "Zulu"), items.labels())
}
@Test fun `selected unseen apps are titled after seen apps in only-seen mode`() {
val items = AppListOrdering.items(
listOf(app("Seen", seen = true), app("Chosen", selected = true), app("Hidden")),
onlySeen = true,
seenAppsFirst = true,
)
assertEquals(listOf("[Seen apps]", "Seen", "|", "[Edited apps]", "Chosen"), items.labels())
}
@Test fun `only seen and checked without grouping is one alphabetical list`() {
val items = AppListOrdering.items(
listOf(app("Zulu", seen = true), app("Beta", selected = true), app("Hidden")),
onlySeen = true,
seenAppsFirst = false,
)
assertEquals(listOf("Beta", "Zulu"), items.labels())
}
@Test fun `unseen app with an event override is included as edited`() {
val items = AppListOrdering.items(
listOf(app("Seen", seen = true), app("Configured", hasEventOverride = true), app("Hidden")),
onlySeen = true,
seenAppsFirst = true,
)
assertEquals(listOf("[Seen apps]", "Seen", "|", "[Edited apps]", "Configured"), items.labels())
}
@Test fun `only seen apps omit section titles without unseen checked apps`() {
val items = AppListOrdering.items(
listOf(app("Zulu", seen = true), app("Alpha", seen = true), app("Hidden")),
onlySeen = true,
seenAppsFirst = true,
)
assertEquals(listOf("Alpha", "Zulu"), items.labels())
}
@Test fun `all apps without grouping is one alphabetical list`() {
val items = AppListOrdering.items(
listOf(app("Zulu", seen = true), app("Alpha"), app("Beta", selected = true)),
onlySeen = false,
seenAppsFirst = false,
)
assertEquals(listOf("Alpha", "Beta", "Zulu"), items.labels())
}
private fun List<AppListItem>.labels() = map {
when (it) {
is AppListItem.App -> it.value.label
is AppListItem.SectionTitle -> "[${it.value}]"
AppListItem.Separator -> "|"
}
}
}
@@ -0,0 +1,10 @@
package se.ajpanton.notificationsmaster.settings
import org.junit.Assert.assertTrue
import org.junit.Test
class LoggingRuleTest {
@Test fun `routine updates default to ignored`() {
assertTrue(LoggingRule().ignoreRoutineUpdates)
}
}
@@ -0,0 +1,32 @@
package se.ajpanton.notificationsmaster.settings
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NotificationListenerPolicyTest {
@Test fun `listener runs when any event logging type is enabled`() {
assertTrue(NotificationListenerPolicy.shouldRun(rules(LoggingType.EDITS to true)))
}
@Test fun `listener stays off when every logging type is disabled`() {
assertFalse(NotificationListenerPolicy.shouldRun(rules()))
}
@Test fun `content settings alone do not keep the listener active`() {
assertFalse(NotificationListenerPolicy.shouldRun(rules(LoggingType.TEXT_CONTENT to true, LoggingType.IMAGE_CONTENT to true)))
}
@Test fun `listener runs for an enabled per-app event override`() {
assertTrue(
NotificationListenerPolicy.shouldRun(
rules(),
hasEnabledEventOverride = true,
),
)
}
private fun rules(vararg enabled: Pair<LoggingType, Boolean>) = LoggingType.entries.associateWith { type ->
LoggingRule(enabled = enabled.firstOrNull { it.first == type }?.second ?: false)
}
}
@@ -0,0 +1,24 @@
package se.ajpanton.notificationsmaster.settings
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NotificationRuleEvaluatorTest {
@Test fun `default blacklist allows every app`() {
assertTrue(NotificationRuleEvaluator.allows(AppFilterSettings(), "example.app"))
}
@Test fun `blacklist excludes selected app`() {
val rule = AppFilterSettings(selectedPackages = setOf("example.app"))
assertFalse(NotificationRuleEvaluator.allows(rule, "example.app"))
assertTrue(NotificationRuleEvaluator.allows(rule, "other.app"))
}
@Test fun `whitelist accepts only selected app`() {
val rule = AppFilterSettings(mode = AppRuleMode.WHITELIST, selectedPackages = setOf("example.app"))
assertTrue(NotificationRuleEvaluator.allows(rule, "example.app"))
assertFalse(NotificationRuleEvaluator.allows(rule, "other.app"))
}
}