Use Notifications Master app ID
This commit is contained in:
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+25
@@ -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)
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
+24
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user