From bef6f0106f589a79c6e1e80d5425b7d6847ebe41 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 29 Aug 2026 16:50:07 +0530 Subject: [PATCH] Cover mute store keys and notification skip paths --- ...nversationNotificationMuteStoreTests.swift | 75 +++++++++++++++++++ .../Services/NotificationServiceTests.swift | 51 +++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 bitchatTests/Services/ConversationNotificationMuteStoreTests.swift diff --git a/bitchatTests/Services/ConversationNotificationMuteStoreTests.swift b/bitchatTests/Services/ConversationNotificationMuteStoreTests.swift new file mode 100644 index 00000000..e8545c5d --- /dev/null +++ b/bitchatTests/Services/ConversationNotificationMuteStoreTests.swift @@ -0,0 +1,75 @@ +// +// ConversationNotificationMuteStoreTests.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation +import Testing +@testable import bitchat + +struct ConversationNotificationMuteStoreTests { + private func isolatedDefaults() -> UserDefaults { + let suite = "ConversationNotificationMuteStoreTests-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suite)! + defaults.removePersistentDomain(forName: suite) + return defaults + } + + @Test func directMuteKeysOnTheFingerprint() throws { + let defaults = isolatedDefaults() + let scope = try #require(ConversationNotificationMuteStore.Scope.direct(fingerprint: "ABCD1234")) + ConversationNotificationMuteStore.setMuted(true, for: scope, in: defaults) + #expect(ConversationNotificationMuteStore.isMuted(scope, in: defaults)) + #expect(ConversationNotificationMuteStore.mutedKeys(in: defaults) == ["dm:abcd1234"]) + } + + /// There is no routing-peer-ID fallback on purpose: peer IDs rotate, so a + /// mute keyed on one would silently stop applying while the UI still + /// showed the conversation muted. + @Test func directMuteIsUnavailableWithoutAFingerprint() { + #expect(ConversationNotificationMuteStore.Scope.direct(fingerprint: nil) == nil) + #expect(ConversationNotificationMuteStore.Scope.direct(fingerprint: "") == nil) + #expect(ConversationNotificationMuteStore.Scope.direct(fingerprint: " ") == nil) + } + + /// The bug this replaced: writing `dm:` on the toggle side and + /// looking up `dm:` on the notification side left the mute silently + /// ineffective. One resolver means one key. + @Test func aFingerprintKeyIsNeverConfusedWithAPeerIDKey() throws { + let defaults = isolatedDefaults() + let byFingerprint = try #require(ConversationNotificationMuteStore.Scope.direct(fingerprint: "ABCD1234")) + ConversationNotificationMuteStore.setMuted(true, for: byFingerprint, in: defaults) + // A scope built from what used to be the peer-ID fallback must not read + // as muted. + let byPeerID = try #require(ConversationNotificationMuteStore.Scope.direct(fingerprint: "deadbeef")) + #expect(!ConversationNotificationMuteStore.isMuted(byPeerID, in: defaults)) + } + + @Test func geohashMuteIsCaseInsensitive() { + let defaults = isolatedDefaults() + let scope = ConversationNotificationMuteStore.Scope.geohash("U4PRUY") + ConversationNotificationMuteStore.setMuted(true, for: scope, in: defaults) + #expect( + ConversationNotificationMuteStore.isMuted( + .geohash("u4pruy"), + in: defaults + ) + ) + } + + @Test func unmuteRemovesKeyAndResetClearsAll() throws { + let defaults = isolatedDefaults() + let dm = try #require(ConversationNotificationMuteStore.Scope.direct(fingerprint: "fp")) + let geo = ConversationNotificationMuteStore.Scope.geohash("u4pru") + ConversationNotificationMuteStore.setMuted(true, for: dm, in: defaults) + ConversationNotificationMuteStore.setMuted(true, for: geo, in: defaults) + ConversationNotificationMuteStore.setMuted(false, for: dm, in: defaults) + #expect(!ConversationNotificationMuteStore.isMuted(dm, in: defaults)) + #expect(ConversationNotificationMuteStore.isMuted(geo, in: defaults)) + ConversationNotificationMuteStore.reset(in: defaults) + #expect(ConversationNotificationMuteStore.mutedKeys(in: defaults).isEmpty) + } +} diff --git a/bitchatTests/Services/NotificationServiceTests.swift b/bitchatTests/Services/NotificationServiceTests.swift index 384f0075..02452089 100644 --- a/bitchatTests/Services/NotificationServiceTests.swift +++ b/bitchatTests/Services/NotificationServiceTests.swift @@ -116,6 +116,57 @@ final class NotificationServiceTests: XCTestCase { XCTAssertEqual(deliverer.requests[1].content.interruptionLevel, .timeSensitive) XCTAssertEqual(deliverer.requests[1].content.body, "2 people around") } + + func test_sendPrivateMessageNotification_skipsWhenConversationMuted() { + let deliverer = RecordingNotificationRequestDeliverer() + let peerID = PeerID(str: "deadbeefdeadbeef") + let scope = ConversationNotificationMuteStore.Scope.direct(fingerprint: "fp1") + let service = NotificationService( + isRunningTestsProvider: { false }, + authorizer: RecordingNotificationAuthorizer(), + requestDeliverer: deliverer, + conversationMutedProvider: { $0 == scope } + ) + + service.sendPrivateMessageNotification(from: "Alice", message: "hi", peerID: peerID, fingerprint: "fp1") + + XCTAssertTrue(deliverer.requests.isEmpty) + } + + /// No fingerprint means no mute can have been keyed (the toggle is disabled + /// until one exists), so the notification must still be delivered rather + /// than suppressed by a peer-ID guess that rotation would invalidate. + func test_sendPrivateMessageNotification_deliversWhenNoFingerprintIsKnown() { + let deliverer = RecordingNotificationRequestDeliverer() + let peerID = PeerID(str: "deadbeefdeadbeef") + let service = NotificationService( + isRunningTestsProvider: { false }, + authorizer: RecordingNotificationAuthorizer(), + requestDeliverer: deliverer, + conversationMutedProvider: { _ in true } + ) + + service.sendPrivateMessageNotification(from: "Alice", message: "hi", peerID: peerID, fingerprint: nil) + + XCTAssertEqual(deliverer.requests.count, 1) + } + + func test_sendGeohashActivityNotification_skipsWhenChannelMuted() { + let deliverer = RecordingNotificationRequestDeliverer() + let service = NotificationService( + isRunningTestsProvider: { false }, + authorizer: RecordingNotificationAuthorizer(), + requestDeliverer: deliverer, + conversationMutedProvider: { + if case .geohash("u4pru") = $0 { return true } + return false + } + ) + + service.sendGeohashActivityNotification(geohash: "u4pru", bodyPreview: "hello") + + XCTAssertTrue(deliverer.requests.isEmpty) + } } private final class RecordingNotificationAuthorizer: NotificationAuthorizing {