Files
bitchat/bitchatTests/Services/NotificationRedactionTests.swift
T
jackandClaude Opus 5 89c5a5d999 Inject the previews preference instead of reading shared defaults
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>
2026-07-26 18:16:45 +02:00

131 lines
4.8 KiB
Swift

import BitFoundation
import Foundation
import Testing
import UserNotifications
@testable import bitchat
/// Lock-screen notifications used to carry the DM body and the sender's
/// nickname verbatim, and the geohash in the title, so a locked phone lying on
/// a table narrated conversations to anyone looking at it. These cover the
/// redaction that is now the default.
///
/// The preference is injected rather than written to shared preferences. These
/// run in the same process as `NotificationServiceTests`, and mutating the
/// global store made whichever ran second depend on the other's cleanup — which
/// passed locally and failed in CI.
struct NotificationRedactionTests {
private final class RecordingDeliverer: NotificationRequestDelivering {
var requests: [UNNotificationRequest] = []
func add(_ request: UNNotificationRequest) {
requests.append(request)
}
}
private struct StubAuthorizer: NotificationAuthorizing {
func requestAuthorization(
options: UNAuthorizationOptions,
completionHandler: @escaping (Bool, Error?) -> Void
) {
completionHandler(true, nil)
}
}
private func makeService(
hidePreviews: Bool
) -> (NotificationService, RecordingDeliverer) {
let deliverer = RecordingDeliverer()
let service = NotificationService(
isRunningTestsProvider: { false },
authorizer: StubAuthorizer(),
requestDeliverer: deliverer,
hidePreviewsProvider: { hidePreviews }
)
return (service, deliverer)
}
private func isolatedDefaults() -> UserDefaults {
UserDefaults(suiteName: "bitchat.tests.notifications.\(UUID().uuidString)")!
}
@Test func previewsAreHiddenByDefault() {
// A fresh install must start quiet rather than opt-in quiet.
#expect(NotificationPrivacySettings.hideMessagePreviews(in: isolatedDefaults()))
}
@Test func theSettingRoundTrips() {
let defaults = isolatedDefaults()
NotificationPrivacySettings.setHideMessagePreviews(false, in: defaults)
#expect(!NotificationPrivacySettings.hideMessagePreviews(in: defaults))
// Panic wipe restores the safe default rather than the last choice.
NotificationPrivacySettings.reset(in: defaults)
#expect(NotificationPrivacySettings.hideMessagePreviews(in: defaults))
}
@Test func redactedDirectMessageWithholdsSenderAndBody() throws {
let (service, deliverer) = makeService(hidePreviews: true)
service.sendPrivateMessageNotification(
from: "alice",
message: "meet at the north gate",
peerID: PeerID(str: "00112233445566ff")
)
let content = try #require(deliverer.requests.first).content
#expect(!content.title.contains("alice"))
#expect(!content.body.contains("north gate"))
#expect(!content.title.isEmpty)
// Still routable: userInfo is never rendered on the lock screen.
#expect(content.userInfo["peerID"] as? String == "00112233445566ff")
}
@Test func redactedMentionWithholdsSenderAndBody() throws {
let (service, deliverer) = makeService(hidePreviews: true)
service.sendMentionNotification(from: "bob", message: "regroup now")
let content = try #require(deliverer.requests.first).content
#expect(!content.title.contains("bob"))
#expect(!content.body.contains("regroup"))
}
@Test func redactedGeohashActivityWithholdsTheGeohash() throws {
let (service, deliverer) = makeService(hidePreviews: true)
service.sendGeohashActivityNotification(
geohash: "u4pruyd",
bodyPreview: "someone said something"
)
let content = try #require(deliverer.requests.first).content
#expect(!content.title.contains("u4pruyd"))
#expect(!content.body.contains("someone said"))
// The deep link still carries it: tapping must land in the channel.
#expect(content.userInfo["deeplink"] as? String == "bitchat://geohash/u4pruyd")
}
@Test func previewsShownWhenTheSettingIsOff() {
let (service, deliverer) = makeService(hidePreviews: false)
service.sendPrivateMessageNotification(
from: "alice",
message: "meet at the north gate",
peerID: PeerID(str: "00112233445566ff")
)
service.sendGeohashActivityNotification(
geohash: "u4pruyd",
bodyPreview: "someone said something"
)
#expect(deliverer.requests.count == 2)
let dm = deliverer.requests[0].content
#expect(dm.title.contains("alice"))
#expect(dm.body == "meet at the north gate")
let geo = deliverer.requests[1].content
#expect(geo.title.contains("u4pruyd"))
#expect(geo.body == "someone said something")
}
}