mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 04:05:19 +00:00
CI caught a real problem this introduced. `NotificationServiceTests`
already asserted the full-preview title and body, and both it and the new
redaction tests read `NotificationPrivacySettings` from
`UserDefaults.standard` in the same process. Whichever ran second
depended on the other's cleanup, so it passed locally and failed in CI:
XCTAssertEqual failed: ("🔒 new dm") is not equal to ("🔒 DM from Alice")
Fixed at the source rather than by ordering or serialization.
`NotificationService` now takes a `hidePreviewsProvider`, defaulting to
the real preference, so each test states which behavior it asserts. The
pre-existing test asks for previews shown and gains a redacted
counterpart; the redaction tests no longer touch the shared store.
`NotificationPrivacySettings` also gained store-injecting accessors so
the default-value and round-trip assertions can use an isolated suite
rather than mutating preferences other tests read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.8 KiB
Swift
148 lines
5.8 KiB
Swift
import XCTest
|
|
import UserNotifications
|
|
import BitFoundation
|
|
@testable import bitchat
|
|
|
|
final class NotificationServiceTests: XCTestCase {
|
|
func test_requestAuthorization_skipsWhenRunningTests() {
|
|
let authorizer = RecordingNotificationAuthorizer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { true },
|
|
authorizer: authorizer,
|
|
requestDeliverer: RecordingNotificationRequestDeliverer()
|
|
)
|
|
|
|
service.requestAuthorization()
|
|
|
|
XCTAssertEqual(authorizer.requestCallCount, 0)
|
|
}
|
|
|
|
func test_requestAuthorization_requestsAlertSoundAndBadgePermissions() {
|
|
let authorizer = RecordingNotificationAuthorizer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { false },
|
|
authorizer: authorizer,
|
|
requestDeliverer: RecordingNotificationRequestDeliverer()
|
|
)
|
|
|
|
service.requestAuthorization()
|
|
|
|
XCTAssertEqual(authorizer.requestCallCount, 1)
|
|
XCTAssertEqual(authorizer.lastOptions, [.alert, .sound, .badge])
|
|
}
|
|
|
|
func test_sendLocalNotification_buildsImmediateRequestWithUserInfo() {
|
|
let deliverer = RecordingNotificationRequestDeliverer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { false },
|
|
authorizer: RecordingNotificationAuthorizer(),
|
|
requestDeliverer: deliverer
|
|
)
|
|
|
|
service.sendLocalNotification(
|
|
title: "Hello",
|
|
body: "World",
|
|
identifier: "custom-id",
|
|
userInfo: ["peerID": "abcd"],
|
|
interruptionLevel: .timeSensitive
|
|
)
|
|
|
|
let request = deliverer.requests.singleValue
|
|
XCTAssertEqual(request?.identifier, "custom-id")
|
|
XCTAssertEqual(request?.content.title, "Hello")
|
|
XCTAssertEqual(request?.content.body, "World")
|
|
XCTAssertEqual(request?.content.userInfo["peerID"] as? String, "abcd")
|
|
XCTAssertEqual(request?.content.interruptionLevel, .timeSensitive)
|
|
XCTAssertNil(request?.trigger)
|
|
}
|
|
|
|
/// Previews shown: the opt-in behavior. Stated explicitly rather than
|
|
/// inherited from the shared preference, which now defaults to hidden.
|
|
func test_sendPrivateMessageNotification_populatesPeerMetadata() {
|
|
let deliverer = RecordingNotificationRequestDeliverer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { false },
|
|
authorizer: RecordingNotificationAuthorizer(),
|
|
requestDeliverer: deliverer,
|
|
hidePreviewsProvider: { false }
|
|
)
|
|
let peerID = PeerID(str: "deadbeefdeadbeef")
|
|
|
|
service.sendPrivateMessageNotification(from: "Alice", message: "hi", peerID: peerID)
|
|
|
|
let request = deliverer.requests.singleValue
|
|
XCTAssertEqual(request?.content.title, "🔒 DM from Alice")
|
|
XCTAssertEqual(request?.content.body, "hi")
|
|
XCTAssertEqual(request?.content.userInfo["peerID"] as? String, peerID.id)
|
|
XCTAssertEqual(request?.content.userInfo["senderName"] as? String, "Alice")
|
|
}
|
|
|
|
/// Previews hidden: the default. The routing payload has to survive
|
|
/// redaction, or tapping the alert would not open the conversation.
|
|
func test_sendPrivateMessageNotification_withPreviewsHidden_keepsRoutingButDropsContent() {
|
|
let deliverer = RecordingNotificationRequestDeliverer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { false },
|
|
authorizer: RecordingNotificationAuthorizer(),
|
|
requestDeliverer: deliverer,
|
|
hidePreviewsProvider: { true }
|
|
)
|
|
let peerID = PeerID(str: "deadbeefdeadbeef")
|
|
|
|
service.sendPrivateMessageNotification(from: "Alice", message: "hi", peerID: peerID)
|
|
|
|
let request = deliverer.requests.singleValue
|
|
XCTAssertFalse(request?.content.title.contains("Alice") ?? true)
|
|
XCTAssertFalse(request?.content.body.contains("hi") ?? true)
|
|
XCTAssertFalse(request?.content.title.isEmpty ?? true)
|
|
XCTAssertEqual(request?.content.userInfo["peerID"] as? String, peerID.id)
|
|
}
|
|
|
|
func test_wrapperNotifications_setExpectedIdentifiersAndDeepLinks() {
|
|
let deliverer = RecordingNotificationRequestDeliverer()
|
|
let service = NotificationService(
|
|
isRunningTestsProvider: { false },
|
|
authorizer: RecordingNotificationAuthorizer(),
|
|
requestDeliverer: deliverer
|
|
)
|
|
|
|
service.sendGeohashActivityNotification(geohash: "87yv", bodyPreview: "Someone is here")
|
|
service.sendNetworkAvailableNotification(peerCount: 2)
|
|
|
|
XCTAssertEqual(deliverer.requests.count, 2)
|
|
XCTAssertEqual(deliverer.requests[0].content.userInfo["deeplink"] as? String, "bitchat://geohash/87yv")
|
|
XCTAssertTrue(deliverer.requests[0].identifier.hasPrefix("geo-activity-87yv-"))
|
|
XCTAssertEqual(deliverer.requests[1].identifier, "network-available")
|
|
XCTAssertEqual(deliverer.requests[1].content.interruptionLevel, .timeSensitive)
|
|
XCTAssertEqual(deliverer.requests[1].content.body, "2 people around")
|
|
}
|
|
}
|
|
|
|
private final class RecordingNotificationAuthorizer: NotificationAuthorizing {
|
|
private(set) var requestCallCount = 0
|
|
private(set) var lastOptions: UNAuthorizationOptions?
|
|
|
|
func requestAuthorization(
|
|
options: UNAuthorizationOptions,
|
|
completionHandler: @escaping (Bool, Error?) -> Void
|
|
) {
|
|
requestCallCount += 1
|
|
lastOptions = options
|
|
completionHandler(true, nil)
|
|
}
|
|
}
|
|
|
|
private final class RecordingNotificationRequestDeliverer: NotificationRequestDelivering {
|
|
private(set) var requests: [UNNotificationRequest] = []
|
|
|
|
func add(_ request: UNNotificationRequest) {
|
|
requests.append(request)
|
|
}
|
|
}
|
|
|
|
private extension Array {
|
|
var singleValue: Element? {
|
|
count == 1 ? self[0] : nil
|
|
}
|
|
}
|