@@ -0,0 +1,875 @@
import Testing
import Foundation
@ testable import Rosetta
// MARK: - P u s h N o t i f i c a t i o n B a d g e M a n a g e m e n t T e s t s
// / T e s t s f o r A p p G r o u p b a d g e c o u n t ( N S E w r i t e s , A p p D e l e g a t e r e a d s / d e c r e m e n t s ) .
// / B a d g e k e y : ` a p p _ b a d g e _ c o u n t ` i n ` g r o u p . c o m . r o s e t t a . d e v ` .
@ MainActor
struct PushNotificationBadgeTests {
private static let appGroupID = " group.com.rosetta.dev "
private static let badgeKey = " app_badge_count "
private var shared : UserDefaults ? { UserDefaults ( suiteName : Self . appGroupID ) }
private func resetBadge ( ) {
shared ? . set ( 0 , forKey : Self . badgeKey )
}
// MARK: - B a d g e I n c r e m e n t ( N S E b e h a v i o r s i m u l a t i o n )
@ Test ( " Badge increments from 0 to 1 on first message " )
func badgeIncrementFromZero ( ) {
resetBadge ( )
let current = shared ? . integer ( forKey : Self . badgeKey ) ? ? 0
let newBadge = current + 1
shared ? . set ( newBadge , forKey : Self . badgeKey )
# expect ( shared ? . integer ( forKey : Self . badgeKey ) = = 1 )
}
@ Test ( " Badge increments cumulatively for multiple messages " )
func badgeIncrementMultiple ( ) {
resetBadge ( )
for i in 1. . . 5 {
let current = shared ? . integer ( forKey : Self . badgeKey ) ? ? 0
shared ? . set ( current + 1 , forKey : Self . badgeKey )
# expect ( shared ? . integer ( forKey : Self . badgeKey ) = = i )
}
}
@ Test ( " Badge decrement on read push never goes below 0 " )
func badgeDecrementFloor ( ) {
resetBadge ( )
shared ? . set ( 2 , forKey : Self . badgeKey )
// S i m u l a t e c l e a r i n g 5 n o t i f i c a t i o n s ( m o r e t h a n b a d g e c o u n t )
let current = shared ? . integer ( forKey : Self . badgeKey ) ? ? 0
let newBadge = max ( current - 5 , 0 )
shared ? . set ( newBadge , forKey : Self . badgeKey )
# expect ( shared ? . integer ( forKey : Self . badgeKey ) = = 0 )
}
@ Test ( " Badge decrement from 3 by 2 cleared notifications = 1 " )
func badgeDecrementPartial ( ) {
resetBadge ( )
shared ? . set ( 3 , forKey : Self . badgeKey )
let current = shared ? . integer ( forKey : Self . badgeKey ) ? ? 0
let newBadge = max ( current - 2 , 0 )
shared ? . set ( newBadge , forKey : Self . badgeKey )
# expect ( shared ? . integer ( forKey : Self . badgeKey ) = = 1 )
}
@ Test ( " Badge not incremented for muted chats " )
func badgeNotIncrementedForMuted ( ) {
resetBadge ( )
let mutedKeys = [ " 02muted_sender " ]
shared ? . set ( mutedKeys , forKey : " muted_chats_keys " )
let senderKey = " 02muted_sender "
let isMuted = mutedKeys . contains ( senderKey )
# expect ( isMuted = = true )
// N S E s k i p s b a d g e i n c r e m e n t f o r m u t e d — b a d g e s t a y s 0
if ! isMuted {
let current = shared ? . integer ( forKey : Self . badgeKey ) ? ? 0
shared ? . set ( current + 1 , forKey : Self . badgeKey )
}
# expect ( shared ? . integer ( forKey : Self . badgeKey ) = = 0 )
// C l e a n u p
shared ? . removeObject ( forKey : " muted_chats_keys " )
}
}
// MARK: - N S E S e n d e r D e d u p W i n d o w T e s t s
// / T e s t s f o r 1 0 - s e c o n d s e n d e r d e d u p w i n d o w ( A n d r o i d p a r i t y ) .
// / U s e s i n - m e m o r y d i c t i o n a r i e s t o a v o i d U s e r D e f a u l t s p a r a l l e l t e s t i n t e r f e r e n c e .
struct PushNotificationSenderDedupTests {
private static let dedupWindow : TimeInterval = 10
@ Test ( " First notification from sender is NOT a duplicate " )
func firstNotificationNotDuplicate ( ) {
let senderKey = " 02first_sender "
let timestamps : [ String : Double ] = [ : ]
let isDuplicate = timestamps [ senderKey ] . map { Date ( ) . timeIntervalSince1970 - $0 < Self . dedupWindow } ? ? false
# expect ( isDuplicate = = false )
}
@ Test ( " Second notification within 10s IS a duplicate " )
func secondWithinWindowIsDuplicate ( ) {
let senderKey = " 02dup_sender "
let now = Date ( ) . timeIntervalSince1970
// S i m u l a t e f i r s t n o t i f i c a t i o n r e c o r d e d
let timestamps : [ String : Double ] = [ senderKey : now ]
// C h e c k s e c o n d n o t i f i c a t i o n ( s a m e s e n d e r , w i t h i n w i n d o w )
let isDuplicate = timestamps [ senderKey ] . map { now - $0 < Self . dedupWindow } ? ? false
# expect ( isDuplicate = = true )
}
@ Test ( " Notification after 10s is NOT a duplicate " )
func afterWindowNotDuplicate ( ) {
let senderKey = " 02old_sender "
let now = Date ( ) . timeIntervalSince1970
// F i r s t n o t i f i c a t i o n 1 1 s e c o n d s a g o
let timestamps : [ String : Double ] = [ senderKey : now - 11 ]
let isDuplicate = timestamps [ senderKey ] . map { now - $0 < Self . dedupWindow } ? ? false
# expect ( isDuplicate = = false )
}
@ Test ( " Different senders are independent (no cross-dedup) " )
func differentSendersIndependent ( ) {
let now = Date ( ) . timeIntervalSince1970
let timestamps : [ String : Double ] = [ " 02sender_a " : now ]
let isDupA = timestamps [ " 02sender_a " ] . map { now - $0 < Self . dedupWindow } ? ? false
let isDupB = timestamps [ " 02sender_b " ] . map { now - $0 < Self . dedupWindow } ? ? false
# expect ( isDupA = = true )
# expect ( isDupB = = false )
}
@ Test ( " Empty sender key uses __no_sender__ dedup key " )
func emptySenderKeyDedup ( ) {
let now = Date ( ) . timeIntervalSince1970
let dedupKey = " __no_sender__ "
let timestamps : [ String : Double ] = [ dedupKey : now ]
let isDuplicate = timestamps [ dedupKey ] . map { now - $0 < Self . dedupWindow } ? ? false
# expect ( isDuplicate = = true )
}
@ Test ( " Stale entries (>120s) are evicted on write " )
func staleEntriesEvicted ( ) {
let now = Date ( ) . timeIntervalSince1970
var timestamps : [ String : Double ] = [
" 02stale " : now - 200 , // > 1 2 0 s — s h o u l d b e e v i c t e d
" 02recent " : now - 5 // < 1 2 0 s — s h o u l d b e k e p t
]
// S i m u l a t e N S E e v i c t i o n l o g i c
timestamps = timestamps . filter { now - $0 . value < 120 }
# expect ( timestamps [ " 02stale " ] = = nil )
# expect ( timestamps [ " 02recent " ] != nil )
}
}
// MARK: - N S E M e s s a g e I D D e d u p T e s t s
// / T e s t s f o r m e s s a g e I d - b a s e d d e d u p ( u n i q u e m e s s a g e d e l i v e r y t r a c k i n g ) .
// / U s e s i n - m e m o r y a r r a y s t o s i m u l a t e N S E d e d u p l o g i c .
struct PushNotificationMessageIdDedupTests {
private static let maxProcessedIds = 100
@ Test ( " New messageId is NOT a duplicate " )
func newMessageIdNotDuplicate ( ) {
let processedIds : [ String ] = [ ]
# expect ( processedIds . contains ( " msg_new_123 " ) = = false )
}
@ Test ( " Already-processed messageId IS a duplicate " )
func processedMessageIdIsDuplicate ( ) {
let processedIds = [ " msg_abc " , " msg_def " ]
# expect ( processedIds . contains ( " msg_abc " ) = = true )
# expect ( processedIds . contains ( " msg_def " ) = = true )
# expect ( processedIds . contains ( " msg_xyz " ) = = false )
}
@ Test ( " Processed IDs capped at 100 (oldest evicted) " )
func processedIdsCapped ( ) {
var ids = ( 0. . < 105 ) . map { " msg_ \( $0 ) " }
// S i m u l a t e N S E e v i c t i o n : k e e p o n l y l a s t 1 0 0
if ids . count > Self . maxProcessedIds {
ids = Array ( ids . suffix ( Self . maxProcessedIds ) )
}
# expect ( ids . count = = 100 )
// O l d e s t 5 s h o u l d b e e v i c t e d
# expect ( ids . contains ( " msg_0 " ) = = false )
# expect ( ids . contains ( " msg_4 " ) = = false )
// N e w e s t s h o u l d r e m a i n
# expect ( ids . contains ( " msg_104 " ) = = true )
# expect ( ids . contains ( " msg_5 " ) = = true )
}
@ Test ( " Duplicate messageId does NOT increment badge " )
func duplicateMessageIdNoBadgeIncrement ( ) {
let processedIds = [ " msg_dup_1 " ]
var badgeCount = 0
let isMessageIdDuplicate = processedIds . contains ( " msg_dup_1 " )
# expect ( isMessageIdDuplicate = = true )
// N S E : i f d u p l i c a t e , b a d g e s t a y s u n c h a n g e d
if ! isMessageIdDuplicate {
badgeCount += 1
}
# expect ( badgeCount = = 0 )
}
}
// MARK: - D e s k t o p - A c t i v e S u p p r e s s i o n T e s t s
// / T e s t s f o r 3 0 - s e c o n d D e s k t o p - a c t i v e s u p p r e s s i o n w i n d o w .
// / W h e n D e s k t o p r e a d s a d i a l o g , i O S N S E s u p p r e s s e s m e s s a g e p u s h e s f o r 3 0 s .
// / U s e s i n - m e m o r y d i c t i o n a r i e s t o a v o i d p a r a l l e l t e s t i n t e r f e r e n c e .
struct PushNotificationDesktopSuppressionTests {
private static let recentlyReadWindow : TimeInterval = 30
@ Test ( " No recent read — message NOT suppressed " )
func noRecentReadNotSuppressed ( ) {
let senderKey = " 02alice "
let recentlyRead : [ String : Double ] = [ : ]
let shouldSuppress : Bool
if let lastReadTime = recentlyRead [ senderKey ] {
shouldSuppress = Date ( ) . timeIntervalSince1970 - lastReadTime < Self . recentlyReadWindow
} else {
shouldSuppress = false
}
# expect ( shouldSuppress = = false )
}
@ Test ( " Desktop read 5s ago — message IS suppressed " )
func recentDesktopReadSuppresses ( ) {
let senderKey = " 02alice "
let now = Date ( ) . timeIntervalSince1970
let recentlyRead : [ String : Double ] = [ senderKey : now - 5 ]
let shouldSuppress : Bool
if let lastReadTime = recentlyRead [ senderKey ] {
shouldSuppress = now - lastReadTime < Self . recentlyReadWindow
} else {
shouldSuppress = false
}
# expect ( shouldSuppress = = true )
}
@ Test ( " Desktop read 31s ago — message NOT suppressed (window expired) " )
func expiredDesktopReadNotSuppressed ( ) {
let senderKey = " 02alice "
let now = Date ( ) . timeIntervalSince1970
let recentlyRead : [ String : Double ] = [ senderKey : now - 31 ]
let shouldSuppress : Bool
if let lastReadTime = recentlyRead [ senderKey ] {
shouldSuppress = now - lastReadTime < Self . recentlyReadWindow
} else {
shouldSuppress = false
}
# expect ( shouldSuppress = = false )
}
@ Test ( " Desktop read for dialog A does NOT suppress dialog B " )
func suppressionPerDialog ( ) {
let now = Date ( ) . timeIntervalSince1970
let recentlyRead : [ String : Double ] = [ " 02alice " : now - 5 ]
let suppressAlice = recentlyRead [ " 02alice " ] . map { now - $0 < Self . recentlyReadWindow } ? ? false
let suppressBob = recentlyRead [ " 02bob " ] . map { now - $0 < Self . recentlyReadWindow } ? ? false
# expect ( suppressAlice = = true )
# expect ( suppressBob = = false )
}
@ Test ( " Stale entries (>60s) evicted on read push " )
func staleEntriesEvicted ( ) {
let now = Date ( ) . timeIntervalSince1970
var recentlyRead : [ String : Double ] = [
" 02stale_dialog " : now - 90 , // > 6 0 s — s h o u l d b e e v i c t e d
" 02recent_dialog " : now - 10 // < 6 0 s — s h o u l d b e k e p t
]
// S i m u l a t e N S E e v i c t i o n ( r u n s o n e a c h R E A D p u s h )
recentlyRead = recentlyRead . filter { now - $0 . value < 60 }
# expect ( recentlyRead [ " 02stale_dialog " ] = = nil )
# expect ( recentlyRead [ " 02recent_dialog " ] != nil )
}
@ Test ( " Read push records dialog in recently-read map " )
func readPushRecordsDialog ( ) {
let dialogKey = " 02alice "
let now = Date ( ) . timeIntervalSince1970
var recentlyRead : [ String : Double ] = [ : ]
recentlyRead [ dialogKey ] = now
# expect ( recentlyRead [ dialogKey ] != nil )
# expect ( abs ( recentlyRead [ dialogKey ] ! - now ) < 1 )
}
@ Test ( " Desktop read at exact boundary (30s) — NOT suppressed " )
func exactBoundaryNotSuppressed ( ) {
let senderKey = " 02alice "
let now = Date ( ) . timeIntervalSince1970
let recentlyRead : [ String : Double ] = [ senderKey : now - 30 ]
let shouldSuppress = recentlyRead [ senderKey ] . map { now - $0 < Self . recentlyReadWindow } ? ? false
# expect ( shouldSuppress = = false )
}
}
// MARK: - R e a d P u s h G r o u p K e y N o r m a l i z a t i o n T e s t s
// / S e r v e r s e n d s ` d i a l o g ` f i e l d w i t h ` # g r o u p : ` p r e f i x f o r g r o u p r e a d s .
// / N S E m u s t s t r i p t h e p r e f i x b e f o r e m a t c h i n g n o t i f i c a t i o n s .
@ MainActor
struct PushNotificationReadGroupKeyTests {
@ Test ( " Read push dialog with #group: prefix is stripped " )
func groupPrefixStripped ( ) {
let dialogKey = " #group:abc123 "
var normalized = dialogKey
if normalized . hasPrefix ( " #group: " ) {
normalized = String ( normalized . dropFirst ( " #group: " . count ) )
}
# expect ( normalized = = " abc123 " )
}
@ Test ( " Read push dialog without prefix is unchanged " )
func personalDialogKeyUnchanged ( ) {
let dialogKey = " 02abc123def456 "
var normalized = dialogKey
if normalized . hasPrefix ( " #group: " ) {
normalized = String ( normalized . dropFirst ( " #group: " . count ) )
}
# expect ( normalized = = " 02abc123def456 " )
}
@ Test ( " Empty dialog key stays empty " )
func emptyDialogKeyStaysEmpty ( ) {
let dialogKey = " "
var normalized = dialogKey
if normalized . hasPrefix ( " #group: " ) {
normalized = String ( normalized . dropFirst ( " #group: " . count ) )
}
# expect ( normalized = = " " )
}
@ Test ( " #group: prefix only → empty after strip " )
func prefixOnlyBecomesEmpty ( ) {
let dialogKey = " #group: "
var normalized = dialogKey
if normalized . hasPrefix ( " #group: " ) {
normalized = String ( normalized . dropFirst ( " #group: " . count ) )
}
# expect ( normalized = = " " )
}
}
// MARK: - C r o s s - P l a t f o r m P a y l o a d P a r i t y T e s t s
// / S e r v e r s e n d s s p e c i f i c p a y l o a d f o r m a t s f o r e a c h p u s h t y p e .
// / i O S m u s t c o r r e c t l y p a r s e t h e m . V a l i d a t e s p a r i t y w i t h S e r v e r F C M . j a v a .
@ MainActor
struct PushNotificationPayloadParityTests {
// MARK: - P e r s o n a l M e s s a g e P a y l o a d
@ Test ( " personal_message payload extracts sender from 'dialog' field " )
func personalMessagePayload ( ) {
let serverPayload : [ AnyHashable : Any ] = [
" type " : " personal_message " ,
" dialog " : " 02abc123def456789 " ,
" title " : " Alice "
]
let senderKey = AppDelegate . extractSenderKey ( from : serverPayload )
# expect ( senderKey = = " 02abc123def456789 " )
}
@ Test ( " personal_message with missing dialog falls back to sender_public_key " )
func personalMessageFallback ( ) {
let serverPayload : [ AnyHashable : Any ] = [
" type " : " personal_message " ,
" sender_public_key " : " 02fallback_key " ,
" title " : " Bob "
]
let senderKey = AppDelegate . extractSenderKey ( from : serverPayload )
# expect ( senderKey = = " 02fallback_key " )
}
// MARK: - G r o u p M e s s a g e P a y l o a d
@ Test ( " group_message payload has dialog = group ID (no #group: prefix) " )
func groupMessagePayload ( ) {
// S e r v e r s e n d s g r o u p I D w i t h o u t # g r o u p : p r e f i x i n p u s h p a y l o a d
let serverPayload : [ AnyHashable : Any ] = [
" type " : " group_message " ,
" dialog " : " groupIdABC123 "
]
let senderKey = AppDelegate . extractSenderKey ( from : serverPayload )
# expect ( senderKey = = " groupIdABC123 " )
}
// MARK: - R e a d P a y l o a d
@ Test ( " read payload with personal dialog " )
func readPayloadPersonal ( ) {
let serverPayload : [ AnyHashable : Any ] = [
" type " : " read " ,
" dialog " : " 02opponent_key "
]
let dialogKey = serverPayload [ " dialog " ] as ? String ? ? " "
# expect ( dialogKey = = " 02opponent_key " )
}
@ Test ( " read payload with group dialog (#group: prefixed) " )
func readPayloadGroup ( ) {
let serverPayload : [ AnyHashable : Any ] = [
" type " : " read " ,
" dialog " : " #group:groupIdXYZ "
]
var dialogKey = serverPayload [ " dialog " ] as ? String ? ? " "
if dialogKey . hasPrefix ( " #group: " ) {
dialogKey = String ( dialogKey . dropFirst ( " #group: " . count ) )
}
# expect ( dialogKey = = " groupIdXYZ " )
}
// MARK: - C a l l P a y l o a d
@ Test ( " call payload has callId and joinToken " )
func callPayload ( ) {
let serverPayload : [ AnyHashable : Any ] = [
" type " : " call " ,
" dialog " : " 02caller_key " ,
" callId " : " 550e8400-e29b-41d4-a716-446655440000 " ,
" joinToken " : " 6ba7b810-9dad-11d1-80b4-00c04fd430c8 "
]
let callerKey = serverPayload [ " dialog " ] as ? String ? ? " "
let callId = serverPayload [ " callId " ] as ? String ? ? " "
let joinToken = serverPayload [ " joinToken " ] as ? String ? ? " "
# expect ( callerKey = = " 02caller_key " )
# expect ( ! callId . isEmpty )
# expect ( ! joinToken . isEmpty )
}
// MARK: - T y p e F i e l d R o u t i n g
@ Test ( " Push type correctly identified from payload " )
func pushTypeRouting ( ) {
let types : [ ( String , String ) ] = [
( " personal_message " , " personal_message " ) ,
( " group_message " , " group_message " ) ,
( " read " , " read " ) ,
( " call " , " call " )
]
for ( input , expected ) in types {
let payload : [ AnyHashable : Any ] = [ " type " : input ]
let pushType = payload [ " type " ] as ? String ? ? " "
# expect ( pushType = = expected )
}
}
@ Test ( " Missing type field defaults to empty string " )
func missingTypeFieldDefault ( ) {
let payload : [ AnyHashable : Any ] = [ " dialog " : " 02abc " ]
let pushType = payload [ " type " ] as ? String ? ? " "
# expect ( pushType = = " " )
}
}
// MARK: - M u t e C h e c k w i t h G r o u p K e y V a r i a n t s
// / T e s t s m u t e s u p p r e s s i o n w i t h d i f f e r e n t g r o u p k e y f o r m a t s .
// / S e r v e r s e n d s g r o u p I D w i t h o u t # g r o u p : p r e f i x i n p u s h ` d i a l o g ` f i e l d .
// / M u t e l i s t m a y s t o r e w i t h o r w i t h o u t p r e f i x .
@ MainActor
struct PushNotificationMuteVariantsTests {
private static let appGroupID = " group.com.rosetta.dev "
private var shared : UserDefaults ? { UserDefaults ( suiteName : Self . appGroupID ) }
private func clearMuteList ( ) {
shared ? . removeObject ( forKey : " muted_chats_keys " )
}
@ Test ( " Personal chat muted by exact key match " )
func personalChatMuted ( ) {
clearMuteList ( )
shared ? . set ( [ " 02muted_user " ] , forKey : " muted_chats_keys " )
let senderKey = " 02muted_user "
let mutedKeys = shared ? . stringArray ( forKey : " muted_chats_keys " ) ? ? [ ]
# expect ( mutedKeys . contains ( senderKey ) = = true )
clearMuteList ( )
}
@ Test ( " Non-muted personal chat is NOT suppressed " )
func nonMutedPersonalChat ( ) {
clearMuteList ( )
shared ? . set ( [ " 02other_user " ] , forKey : " muted_chats_keys " )
let senderKey = " 02not_muted "
let mutedKeys = shared ? . stringArray ( forKey : " muted_chats_keys " ) ? ? [ ]
# expect ( mutedKeys . contains ( senderKey ) = = false )
clearMuteList ( )
}
@ Test ( " Group chat muted — server sends raw ID, mute list has raw ID " )
func groupMutedRawId ( ) {
clearMuteList ( )
shared ? . set ( [ " groupABC " ] , forKey : " muted_chats_keys " )
// S e r v e r s e n d s ` d i a l o g : " g r o u p A B C " ` ( n o p r e f i x )
let senderKey = " groupABC "
let mutedKeys = shared ? . stringArray ( forKey : " muted_chats_keys " ) ? ? [ ]
# expect ( mutedKeys . contains ( senderKey ) = = true )
clearMuteList ( )
}
@ Test ( " Group chat muted with #group: prefix in mute list " )
func groupMutedWithPrefix ( ) {
clearMuteList ( )
// i O S s t o r e s w i t h # g r o u p : p r e f i x
shared ? . set ( [ " #group:groupABC " , " groupABC " ] , forKey : " muted_chats_keys " )
// S e r v e r s e n d s r a w I D
let senderKey = " groupABC "
let mutedKeys = shared ? . stringArray ( forKey : " muted_chats_keys " ) ? ? [ ]
// A t l e a s t o n e v a r i a n t s h o u l d m a t c h
let isMuted = mutedKeys . contains ( senderKey ) || mutedKeys . contains ( " #group: \( senderKey ) " )
# expect ( isMuted = = true )
clearMuteList ( )
}
@ Test ( " Empty mute list — nothing suppressed " )
func emptyMuteList ( ) {
clearMuteList ( )
let senderKey = " 02any_user "
let mutedKeys = shared ? . stringArray ( forKey : " muted_chats_keys " ) ? ? [ ]
# expect ( mutedKeys . contains ( senderKey ) = = false )
}
}
// MARK: - S e n d e r K e y E x t r a c t i o n E x t e n d e d T e s t s
// / E x t e n d e d t e s t s f o r ` A p p D e l e g a t e . e x t r a c t S e n d e r K e y ( f r o m : ) ` .
// / C o v e r s e d g e c a s e s n o t i n F o r e g r o u n d N o t i f i c a t i o n T e s t s .
@ MainActor
struct PushSenderKeyExtractionExtendedTests {
@ Test ( " Whitespace-only dialog field falls back to next key " )
func whitespaceDialogFallback ( ) {
let payload : [ AnyHashable : Any ] = [
" dialog " : " " ,
" sender_public_key " : " 02real_key "
]
let key = AppDelegate . extractSenderKey ( from : payload )
// S h o u l d s k i p w h i t e s p a c e - o n l y d i a l o g a n d f a l l b a c k
# expect ( key = = " 02real_key " )
}
@ Test ( " All keys present — dialog wins (priority order) " )
func dialogWinsPriority ( ) {
let payload : [ AnyHashable : Any ] = [
" dialog " : " 02dialog_key " ,
" sender_public_key " : " 02sender_key " ,
" from_public_key " : " 02from_key " ,
" fromPublicKey " : " 02fromPK_key " ,
" public_key " : " 02pk_key " ,
" publicKey " : " 02PK_key "
]
let key = AppDelegate . extractSenderKey ( from : payload )
# expect ( key = = " 02dialog_key " )
}
@ Test ( " Only publicKey present (last fallback) " )
func lastFallbackPublicKey ( ) {
let payload : [ AnyHashable : Any ] = [
" publicKey " : " 02last_resort "
]
let key = AppDelegate . extractSenderKey ( from : payload )
# expect ( key = = " 02last_resort " )
}
@ Test ( " Non-string dialog value returns empty " )
func nonStringDialogReturnsEmpty ( ) {
let payload : [ AnyHashable : Any ] = [
" dialog " : 12345 // I n t , n o t S t r i n g
]
let key = AppDelegate . extractSenderKey ( from : payload )
# expect ( key = = " " )
}
@ Test ( " Unicode sender key preserved " )
func unicodeSenderKeyPreserved ( ) {
let payload : [ AnyHashable : Any ] = [
" dialog " : " 02а б вг д_те с т_🔑 "
]
let key = AppDelegate . extractSenderKey ( from : payload )
# expect ( key = = " 02а б вг д_те с т_🔑 " )
}
@ Test ( " Very long sender key handled " )
func longSenderKeyHandled ( ) {
let longKey = String ( repeating : " a " , count : 1000 )
let payload : [ AnyHashable : Any ] = [ " dialog " : longKey ]
let key = AppDelegate . extractSenderKey ( from : payload )
# expect ( key = = longKey )
# expect ( key . count = = 1000 )
}
}
// MARK: - I n - A p p B a n n e r S u p p r e s s i o n E x t e n d e d T e s t s
// / E x t e n d e d t e s t s f o r ` I n A p p N o t i f i c a t i o n M a n a g e r . s h o u l d S u p p r e s s ( s e n d e r K e y : ) ` .
// / C o v e r s m u t e + a c t i v e d i a l o g c o m b i n a t i o n s .
@ MainActor
struct PushInAppBannerSuppressionExtendedTests {
private static let appGroupID = " group.com.rosetta.dev "
private var shared : UserDefaults ? { UserDefaults ( suiteName : Self . appGroupID ) }
private func clearState ( ) {
for key in MessageRepository . shared . activeDialogKeys {
MessageRepository . shared . setDialogActive ( key , isActive : false )
}
shared ? . removeObject ( forKey : " muted_chats_keys " )
}
@ Test ( " Both muted AND active — suppressed (double reason) " )
func mutedAndActiveSuppressed ( ) {
clearState ( )
MessageRepository . shared . setDialogActive ( " 02both " , isActive : true )
shared ? . set ( [ " 02both " ] , forKey : " muted_chats_keys " )
# expect ( InAppNotificationManager . shouldSuppress ( senderKey : " 02both " ) = = true )
MessageRepository . shared . setDialogActive ( " 02both " , isActive : false )
shared ? . removeObject ( forKey : " muted_chats_keys " )
}
@ Test ( " Muted but NOT active — still suppressed " )
func mutedNotActiveSuppressed ( ) {
clearState ( )
shared ? . set ( [ " 02muted_only " ] , forKey : " muted_chats_keys " )
# expect ( InAppNotificationManager . shouldSuppress ( senderKey : " 02muted_only " ) = = true )
shared ? . removeObject ( forKey : " muted_chats_keys " )
}
@ Test ( " Active but NOT muted — suppressed (active chat open) " )
func activeNotMutedSuppressed ( ) {
clearState ( )
MessageRepository . shared . setDialogActive ( " 02active_only " , isActive : true )
# expect ( InAppNotificationManager . shouldSuppress ( senderKey : " 02active_only " ) = = true )
MessageRepository . shared . setDialogActive ( " 02active_only " , isActive : false )
}
@ Test ( " Neither muted nor active — NOT suppressed " )
func neitherMutedNorActivNotSuppressed ( ) {
clearState ( )
# expect ( InAppNotificationManager . shouldSuppress ( senderKey : " 02normal " ) = = false )
}
@ Test ( " System presentation options always return empty set " )
func systemPresentationAlwaysEmpty ( ) {
clearState ( )
// E v e n f o r n o n - s u p p r e s s e d c h a t s , s y s t e m b a n n e r i s a l w a y s s u p p r e s s e d
// ( c u s t o m i n - a p p b a n n e r s h o w n i n s t e a d )
let options = AppDelegate . foregroundPresentationOptions ( for : [ " dialog " : " 02any_user " ] )
# expect ( options = = [ ] )
}
}
// MARK: - N S E T h r e a d I d e n t i f i e r ( N o t i f i c a t i o n G r o u p i n g ) T e s t s
// / T e s t s n o t i f i c a t i o n t h r e a d i n g ( g r o u p i n g b y c o n v e r s a t i o n ) .
// / N S E s e t s ` t h r e a d I d e n t i f i e r = s e n d e r K e y ` o n n o t i f i c a t i o n s .
struct PushNotificationThreadingTests {
@ Test ( " Thread identifier matches sender key for personal messages " )
func threadIdPersonalMessage ( ) {
let senderKey = " 02alice_key "
let threadIdentifier = senderKey . isEmpty ? nil : senderKey
# expect ( threadIdentifier = = " 02alice_key " )
}
@ Test ( " Thread identifier matches group ID for group messages " )
func threadIdGroupMessage ( ) {
let senderKey = " groupABC123 "
let threadIdentifier = senderKey . isEmpty ? nil : senderKey
# expect ( threadIdentifier = = " groupABC123 " )
}
@ Test ( " Empty sender key produces nil thread identifier " )
func threadIdEmptySender ( ) {
let senderKey = " "
let threadIdentifier = senderKey . isEmpty ? nil : senderKey
# expect ( threadIdentifier = = nil )
}
}
// MARK: - P u s h N o t i f i c a t i o n P a c k e t E x t e n d e d T e s t s
// / V a l i d a t e s P a c k e t P u s h N o t i f i c a t i o n f o r b o t h F C M a n d V o I P t o k e n t y p e s .
struct PushNotificationPacketExtendedTests {
@ Test ( " FCM subscribe packet preserves all fields through round-trip " )
func fcmSubscribeRoundTrip ( ) throws {
var original = PacketPushNotification ( )
original . notificationsToken = " dGVzdF9mY21fdG9rZW5fMTIzNDU2Nzg5MA== "
original . action = . subscribe
original . tokenType = . fcm
original . deviceId = " DEVICE-UUID-1234 "
let decoded = try decodePacket ( original )
# expect ( decoded . notificationsToken = = original . notificationsToken )
# expect ( decoded . action = = . subscribe )
# expect ( decoded . tokenType = = . fcm )
# expect ( decoded . deviceId = = " DEVICE-UUID-1234 " )
}
@ Test ( " VoIP subscribe packet preserves all fields through round-trip " )
func voipSubscribeRoundTrip ( ) throws {
var original = PacketPushNotification ( )
original . notificationsToken = " abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 "
original . action = . subscribe
original . tokenType = . voipApns
original . deviceId = " VOIP-DEVICE-5678 "
let decoded = try decodePacket ( original )
# expect ( decoded . notificationsToken = = original . notificationsToken )
# expect ( decoded . action = = . subscribe )
# expect ( decoded . tokenType = = . voipApns )
# expect ( decoded . deviceId = = " VOIP-DEVICE-5678 " )
}
@ Test ( " Unsubscribe packet round-trip " )
func unsubscribeRoundTrip ( ) throws {
var original = PacketPushNotification ( )
original . notificationsToken = " old_token_to_remove "
original . action = . unsubscribe
original . tokenType = . fcm
original . deviceId = " DEVICE-CLEANUP "
let decoded = try decodePacket ( original )
# expect ( decoded . action = = . unsubscribe )
# expect ( decoded . notificationsToken = = " old_token_to_remove " )
}
@ Test ( " Both token types have correct raw values (server parity) " )
func tokenTypeRawValues ( ) {
// S e r v e r T o k e n T y p e . j a v a : F C M ( 0 ) , V o I P A p n s ( 1 )
# expect ( PushTokenType . fcm . rawValue = = 0 )
# expect ( PushTokenType . voipApns . rawValue = = 1 )
}
@ Test ( " Both actions have correct raw values (server parity) " )
func actionRawValues ( ) {
// S e r v e r N e t w o r k N o t i f i c a t i o n A c t i o n . j a v a : S U B S C R I B E ( 0 ) , U N S U B S C R I B E ( 1 )
# expect ( PushNotificationAction . subscribe . rawValue = = 0 )
# expect ( PushNotificationAction . unsubscribe . rawValue = = 1 )
}
// MARK: - H e l p e r
private func decodePacket (
_ packet : PacketPushNotification
) throws -> PacketPushNotification {
let encoded = PacketRegistry . encode ( packet )
guard let decoded = PacketRegistry . decode ( from : encoded ) ,
let decodedPacket = decoded . packet as ? PacketPushNotification
else {
throw NSError (
domain : " PushNotificationPacketExtendedTests " , code : 1 ,
userInfo : [ NSLocalizedDescriptionKey : " Failed to decode PacketPushNotification " ]
)
}
# expect ( decoded . packetId = = 0x10 )
return decodedPacket
}
}
// MARK: - N S E C a t e g o r y A s s i g n m e n t T e s t s
// / T e s t s n o t i f i c a t i o n c a t e g o r y a s s i g n m e n t f o r m e s s a g e r o u t i n g .
struct PushNotificationCategoryTests {
@ Test ( " Message type gets 'message' category " )
func messageCategoryAssignment ( ) {
let pushType = " personal_message "
let category : String
if pushType = = " call " {
category = " call "
} else {
category = " message "
}
# expect ( category = = " message " )
}
@ Test ( " Call type gets 'call' category " )
func callCategoryAssignment ( ) {
let pushType = " call "
let category : String
if pushType = = " call " {
category = " call "
} else {
category = " message "
}
# expect ( category = = " call " )
}
@ Test ( " Group message type gets 'message' category (not 'group') " )
func groupMessageCategoryIsMessage ( ) {
let pushType = " group_message "
let category : String
if pushType = = " call " {
category = " call "
} else {
category = " message "
}
# expect ( category = = " message " )
}
}
// MARK: - C o n t a c t N a m e R e s o l u t i o n T e s t s
// / T e s t s n a m e r e s o l u t i o n l o g i c ( N S E + A p p D e l e g a t e u s e t h i s p a t t e r n ) .
// / U s e s i n - m e m o r y d i c t i o n a r i e s t o s i m u l a t e A p p G r o u p c a c h e .
struct PushNotificationNameResolutionTests {
@ Test ( " Resolved name from contact_display_names cache " )
func resolvedNameFromCache ( ) {
let contactNames : [ String : String ] = [
" 02alice " : " Alice Smith " ,
" 02bob " : " Bob Jones "
]
# expect ( contactNames [ " 02alice " ] = = " Alice Smith " )
# expect ( contactNames [ " 02bob " ] = = " Bob Jones " )
# expect ( contactNames [ " 02unknown " ] = = nil )
}
@ Test ( " Fallback to push payload title when not in cache " )
func fallbackToPayloadTitle ( ) {
let contactNames : [ String : String ] = [ : ] // E m p t y c a c h e
let payload : [ AnyHashable : Any ] = [
" dialog " : " 02unknown_sender " ,
" title " : " Server Title "
]
let senderKey = payload [ " dialog " ] as ? String ? ? " "
let resolvedName = contactNames [ senderKey ] ? ? ( payload [ " title " ] as ? String )
# expect ( resolvedName = = " Server Title " )
}
@ Test ( " Empty cache and no title — name is nil " )
func emptyEverythingNameNil ( ) {
let contactNames : [ String : String ] = [ : ] // E m p t y c a c h e
let payload : [ AnyHashable : Any ] = [
" dialog " : " 02no_name_sender "
]
let senderKey = payload [ " dialog " ] as ? String ? ? " "
let resolvedName = contactNames [ senderKey ] ? ? ( payload [ " title " ] as ? String )
# expect ( resolvedName = = nil )
}
@ Test ( " Cache prefers local name over server title " )
func cachePreferredOverServerTitle ( ) {
let contactNames : [ String : String ] = [ " 02alice " : " Local Alice " ]
let payload : [ AnyHashable : Any ] = [
" dialog " : " 02alice " ,
" title " : " Server Alice "
]
let senderKey = payload [ " dialog " ] as ? String ? ? " "
let resolvedName = contactNames [ senderKey ] ? ? ( payload [ " title " ] as ? String )
# expect ( resolvedName = = " Local Alice " )
}
}