import Testing @testable import Rosetta // MARK: - Pending Chat Route Expiry Tests /// Tests for the push notification → chat navigation fix. /// Bug: stale `pendingChatRoute` was consumed on tab switches (`.onAppear`) /// instead of only on cold start / foreground resume. /// Fix: `consumeFreshPendingRoute()` checks timestamp < 3 seconds. struct PendingChatRouteExpiryTests { private let routeA = ChatRoute(publicKey: "02aaa", title: "Alice", username: "", verified: 0) private let routeB = ChatRoute(publicKey: "02bbb", title: "Bob", username: "", verified: 0) private func clearStatics() { AppDelegate.pendingChatRoute = nil AppDelegate.pendingChatRouteTimestamp = nil } // MARK: - Fresh Route Consumption @Test("Fresh route (just set) is consumed successfully") func freshRouteConsumed() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date() let result = AppDelegate.consumeFreshPendingRoute() #expect(result != nil) #expect(result?.publicKey == "02aaa") #expect(result?.title == "Alice") } @Test("After consumption, both statics are nil") func staticsNilAfterConsumption() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date() _ = AppDelegate.consumeFreshPendingRoute() #expect(AppDelegate.pendingChatRoute == nil) #expect(AppDelegate.pendingChatRouteTimestamp == nil) } @Test("Second consumption returns nil (idempotent)") func doubleConsumptionReturnsNil() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date() let first = AppDelegate.consumeFreshPendingRoute() let second = AppDelegate.consumeFreshPendingRoute() #expect(first != nil) #expect(second == nil) } // MARK: - Stale Route Rejection @Test("Stale route (5s old) is rejected and cleared") func staleRouteRejected() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date().addingTimeInterval(-5) let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) // Both statics must be cleared even on rejection #expect(AppDelegate.pendingChatRoute == nil) #expect(AppDelegate.pendingChatRouteTimestamp == nil) } @Test("Route exactly at expiry boundary (3s) is rejected") func routeAtExpiryBoundary() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date().addingTimeInterval(-3.0) let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) } @Test("Route just before expiry (2.9s) is consumed") func routeJustBeforeExpiry() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date().addingTimeInterval(-2.9) let result = AppDelegate.consumeFreshPendingRoute() #expect(result != nil) #expect(result?.publicKey == "02aaa") } @Test("Very old route (60s) is rejected") func veryOldRouteRejected() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date().addingTimeInterval(-60) let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) } // MARK: - Missing Data @Test("Route without timestamp is rejected and cleared") func routeWithoutTimestamp() { clearStatics() AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = nil let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) #expect(AppDelegate.pendingChatRoute == nil) } @Test("Timestamp without route is rejected and cleared") func timestampWithoutRoute() { clearStatics() AppDelegate.pendingChatRoute = nil AppDelegate.pendingChatRouteTimestamp = Date() let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) #expect(AppDelegate.pendingChatRouteTimestamp == nil) } @Test("Both nil — returns nil, no crash") func bothNil() { clearStatics() let result = AppDelegate.consumeFreshPendingRoute() #expect(result == nil) } // MARK: - Route Overwrite (rapid notification taps) @Test("Second notification overwrites first — only second is consumed") func overwriteWithSecondNotification() { clearStatics() // First notification AppDelegate.pendingChatRoute = routeA AppDelegate.pendingChatRouteTimestamp = Date() // Second notification immediately after (overwrites) AppDelegate.pendingChatRoute = routeB AppDelegate.pendingChatRouteTimestamp = Date() let result = AppDelegate.consumeFreshPendingRoute() #expect(result != nil) #expect(result?.publicKey == "02bbb") #expect(result?.title == "Bob") } // MARK: - Expiry Constant @Test("Expiry constant is 3 seconds") func expiryConstantValue() { #expect(AppDelegate.pendingRouteExpirySeconds == 3.0) } } // MARK: - Tab Switch Scenario Tests /// Simulates the exact bug scenario: /// 1. Route set from notification tap /// 2. .onReceive fails to consume (simulated by skipping) /// 3. Tab switch triggers .onAppear → stale route must NOT navigate struct TabSwitchScenarioTests { private let route = ChatRoute(publicKey: "02ccc", title: "Charlie", username: "", verified: 0) private func clearStatics() { AppDelegate.pendingChatRoute = nil AppDelegate.pendingChatRouteTimestamp = nil } @Test("Stale route from 10s ago is NOT consumed on tab switch") func staleRouteOnTabSwitch() { clearStatics() // Simulate: notification tapped 10 seconds ago, .onReceive missed it AppDelegate.pendingChatRoute = route AppDelegate.pendingChatRouteTimestamp = Date().addingTimeInterval(-10) // Simulate: .onAppear fires on tab switch let consumed = AppDelegate.consumeFreshPendingRoute() #expect(consumed == nil, "Stale route must NOT be consumed on tab switch") #expect(AppDelegate.pendingChatRoute == nil, "Stale route must be cleared") } @Test("Fresh route from notification tap IS consumed on cold start .onAppear") func freshRouteOnColdStart() { clearStatics() // Simulate: notification tapped just now, app is launching AppDelegate.pendingChatRoute = route AppDelegate.pendingChatRouteTimestamp = Date() // Simulate: .onAppear fires on cold start (< 3s since notification tap) let consumed = AppDelegate.consumeFreshPendingRoute() #expect(consumed != nil, "Fresh route must be consumed on cold start") #expect(consumed?.publicKey == "02ccc") } @Test("Fresh route consumed by .onReceive → didBecomeActive finds nil") func onReceiveConsumesThenDidBecomeActiveNoOp() { clearStatics() // Simulate: notification posted + .onReceive fires (primary path) AppDelegate.pendingChatRoute = route AppDelegate.pendingChatRouteTimestamp = Date() // .onReceive consumes (simulated by direct clear, same as ChatListView line 110-111) AppDelegate.pendingChatRoute = nil AppDelegate.pendingChatRouteTimestamp = nil // didBecomeActiveNotification fires after let consumed = AppDelegate.consumeFreshPendingRoute() #expect(consumed == nil, "didBecomeActive must be no-op after .onReceive consumed") } @Test("Route set → first consumeFresh consumes → second consumeFresh returns nil") func sequentialConsumption() { clearStatics() AppDelegate.pendingChatRoute = route AppDelegate.pendingChatRouteTimestamp = Date() // First call: .onAppear on cold start let first = AppDelegate.consumeFreshPendingRoute() // Second call: didBecomeActiveNotification a moment later let second = AppDelegate.consumeFreshPendingRoute() #expect(first != nil) #expect(second == nil, "Only the first caller should get the route") } }