// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import LibSignalClient import XCTest @testable import SignalServiceKit class ProfileManagerTest: XCTestCase { func testNormalizeRecipientInProfileWhitelist() { let aci = Aci.constantForTesting("00000000-0000-4000-8000-000000000aaa") let phoneNumber = E164("+16505550100")! let pni = Pni.constantForTesting("PNI:00000000-0000-4000-8000-000000000bbb") let serviceIdStore = InMemoryKeyValueStore(collection: "") let phoneNumberStore = InMemoryKeyValueStore(collection: "") func normalizeRecipient(_ recipient: SignalRecipient) { MockDB().write { tx in OWSProfileManager.swift_normalizeRecipientInProfileWhitelist( recipient, serviceIdStore: serviceIdStore, phoneNumberStore: phoneNumberStore, tx: tx ) } } // Don't add any values unless one is already present. MockDB().read { tx in normalizeRecipient(SignalRecipient(aci: aci, pni: pni, phoneNumber: phoneNumber)) XCTAssertFalse(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx)) XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx)) XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx)) } // Move the PNI identifier to the phone number. MockDB().write { tx in serviceIdStore.setBool(true, key: pni.serviceIdUppercaseString, transaction: tx) normalizeRecipient(SignalRecipient(aci: nil, pni: pni, phoneNumber: phoneNumber)) XCTAssertFalse(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx)) XCTAssertTrue(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx)) XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx)) } // Clear lower priority identifiers when multiple are present. MockDB().write { tx in serviceIdStore.setBool(true, key: aci.serviceIdUppercaseString, transaction: tx) normalizeRecipient(SignalRecipient(aci: aci, pni: pni, phoneNumber: phoneNumber)) XCTAssertTrue(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx)) XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx)) XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx)) } // Keep the highest priority identifier if it's already present. MockDB().write { tx in normalizeRecipient(SignalRecipient(aci: aci, pni: pni, phoneNumber: phoneNumber)) XCTAssertTrue(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx)) XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx)) XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx)) } } func testEncodeDecodeProfileChanges() throws { let testCases: [(PendingProfileUpdate, String)] = [ ( PendingProfileUpdate( profileGivenName: .setTo(.init(truncating: "Alice")!), profileFamilyName: .setTo(.init(truncating: "Johnson")!), profileBio: .setTo("A short bio."), profileBioEmoji: .setTo("💙"), profileAvatarData: .setTo(Data(1...3)), visibleBadgeIds: .setTo(["BOOST"]), userProfileWriter: .registration ), "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGvEA8LDCEiIyQlJicsLTM3ODxVJG51bGzaDQ4PEBESExQVFhcYGRobHB0eHyBfEA9wcm9maWxlQmlvRW1vamlacHJvZmlsZUJpb1YkY2xhc3NSaWRfEBh1bnNhdmVkUm90YXRlZFByb2ZpbGVLZXlfEBF1c2VyUHJvZmlsZVdyaXRlcl8QD3Zpc2libGVCYWRnZUlkc18QEXByb2ZpbGVBdmF0YXJEYXRhXxARcHJvZmlsZUZhbWlseU5hbWVfEBBwcm9maWxlR2l2ZW5OYW1lgAaABYAOgAKACxAEgAiAB4AEgANfECQwMDAwMDAwMC0wMDAwLTQwMDAtQTAwMC0wMDAwMDAwMDAwMDBVQWxpY2VXSm9obnNvblxBIHNob3J0IGJpby5i2D3cmUMBAgPSKA8pK1pOUy5vYmplY3RzoSqACYAKVUJPT1NU0i4vMDFaJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0FycmF5ojAyWE5TT2JqZWN00jQPNTZXa2V5RGF0YYAMgA1PECABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fINIuLzk6XE9XU0FFUzI1NktleaI7MlxPV1NBRVMyNTZLZXnSLi89Pl8QJFNpZ25hbE1lc3NhZ2luZy5QZW5kaW5nUHJvZmlsZVVwZGF0ZaI/Ml8QJFNpZ25hbE1lc3NhZ2luZy5QZW5kaW5nUHJvZmlsZVVwZGF0ZQAIABEAGgAkACkAMgA3AEkATABRAFMAZQBrAIAAkgCdAKQApwDCANYA6AD8ARABIwElAScBKQErAS0BLwExATMBNQE3AV4BZAFsAXkBfgGCAYcBkgGUAZYBmAGeAaMBrgG3Ab8BwgHLAdAB2AHaAdwB/wIEAhECFAIhAiYCTQJQAAAAAAAAAgEAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAnc=" ), ( PendingProfileUpdate( profileGivenName: .setTo(.init(truncating: "Alice")!), profileFamilyName: .setTo(.init(truncating: "Johnson")!), profileBio: .setTo("A short bio."), profileBioEmoji: .setTo("💙"), profileAvatarData: .setTo(Data(1...3)), visibleBadgeIds: .setTo(["BOOST"]), userProfileWriter: .registration ), "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGsCwwfICEiIyQlKisxVSRudWxs2Q0ODxAREhMUFRYXGBkaGxwdHl8QD3Byb2ZpbGVCaW9FbW9qaVpwcm9maWxlQmlvViRjbGFzc1JpZF8QEXVzZXJQcm9maWxlV3JpdGVyXxAPdmlzaWJsZUJhZGdlSWRzXxARcHJvZmlsZUF2YXRhckRhdGFfEBFwcm9maWxlRmFtaWx5TmFtZV8QEHByb2ZpbGVHaXZlbk5hbWWABoAFgAuAAhAEgAiAB4AEgANfECQwMDAwMDAwMC0wMDAwLTQwMDAtQTAwMC0wMDAwMDAwMDAwMDBVQWxpY2VXSm9obnNvblxBIHNob3J0IGJpby5i2D3cmUMBAgPSJg8nKVpOUy5vYmplY3RzoSiACYAKVUJPT1NU0iwtLi9aJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0FycmF5oi4wWE5TT2JqZWN00iwtMjNfECRTaWduYWxNZXNzYWdpbmcuUGVuZGluZ1Byb2ZpbGVVcGRhdGWiNDBfECRTaWduYWxNZXNzYWdpbmcuUGVuZGluZ1Byb2ZpbGVVcGRhdGUACAARABoAJAApADIANwBJAEwAUQBTAGAAZgB5AIsAlgCdAKAAtADGANoA7gEBAQMBBQEHAQkBCwENAQ8BEQETAToBQAFIAVUBWgFeAWMBbgFwAXIBdAF6AX8BigGTAZsBngGnAawB0wHWAAAAAAAAAgEAAAAAAAAANQAAAAAAAAAAAAAAAAAAAf0=" ), ( PendingProfileUpdate( profileGivenName: .noChange, profileFamilyName: .noChange, profileBio: .noChange, profileBioEmoji: .noChange, profileAvatarData: .noChange, visibleBadgeIds: .noChange, userProfileWriter: .localUser ), "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwdHlUkbnVsbNgNDg8QERITFBUWFxcXFxsXViRjbGFzc1JpZF8QFnByb2ZpbGVCaW9FbW9qaUNoYW5nZWRfEBhwcm9maWxlRmFtaWx5TmFtZUNoYW5nZWRfEBFwcm9maWxlQmlvQ2hhbmdlZF8QF3Byb2ZpbGVHaXZlbk5hbWVDaGFuZ2VkXxARdXNlclByb2ZpbGVXcml0ZXJfEBhwcm9maWxlQXZhdGFyRGF0YUNoYW5nZWSAA4ACCAgICBAACF8QJDAwMDAwMDAwLTAwMDAtNDAwMC1BMDAwLTAwMDAwMDAwMDAwMNIfICEiWiRjbGFzc25hbWVYJGNsYXNzZXNfECRTaWduYWxNZXNzYWdpbmcuUGVuZGluZ1Byb2ZpbGVVcGRhdGWiIyRfECRTaWduYWxNZXNzYWdpbmcuUGVuZGluZ1Byb2ZpbGVVcGRhdGVYTlNPYmplY3QACAARABoAJAApADIANwBJAEwAUQBTAFgAXgBvAHYAeQCSAK0AwQDbAO8BCgEMAQ4BDwEQAREBEgEUARUBPAFBAUwBVQF8AX8BpgAAAAAAAAIBAAAAAAAAACUAAAAAAAAAAAAAAAAAAAGv" ), ( PendingProfileUpdate( profileGivenName: .noChange, profileFamilyName: .noChange, profileBio: .noChange, profileBioEmoji: .noChange, profileAvatarData: .noChangeButMustReupload, visibleBadgeIds: .noChange, userProfileWriter: .localUser ), "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwfIFUkbnVsbNkNDg8QERITFBUWFxgYGBgYHR5WJGNsYXNzUmlkXxAYcHJvZmlsZUF2YXRhckRhdGFDaGFuZ2VkXxAYcHJvZmlsZUZhbWlseU5hbWVDaGFuZ2VkXxARcHJvZmlsZUJpb0NoYW5nZWRfEBZwcm9maWxlQmlvRW1vamlDaGFuZ2VkXxAXcHJvZmlsZUdpdmVuTmFtZUNoYW5nZWRfEBF1c2VyUHJvZmlsZVdyaXRlcl8QHXByb2ZpbGVBdmF0YXJEYXRhTXVzdFJldXBsb2FkgAOAAggICAgIEAAJXxAkMDAwMDAwMDAtMDAwMC00MDAwLUEwMDAtMDAwMDAwMDAwMDAw0iEiIyRaJGNsYXNzbmFtZVgkY2xhc3Nlc18QJVNpZ25hbFNlcnZpY2VLaXQuUGVuZGluZ1Byb2ZpbGVVcGRhdGWiJSZfECVTaWduYWxTZXJ2aWNlS2l0LlBlbmRpbmdQcm9maWxlVXBkYXRlWE5TT2JqZWN0AAgAEQAaACQAKQAyADcASQBMAFEAUwBYAF4AcQB4AHsAlgCxAMUA3gD4AQwBLAEuATABMQEyATMBNAE1ATcBOAFfAWQBbwF4AaABowHLAAAAAAAAAgEAAAAAAAAAJwAAAAAAAAAAAAAAAAAAAdQ=" ), ] let expectedId = UUID(uuidString: "00000000-0000-4000-A000-000000000000")! for (expectedValue, encodedValue) in testCases { // Test that it can be encoded/decoded. let decodedEncodedValue = try Self.deserialize(data: Self.serialize(expectedValue)) XCTAssertEqual(decodedEncodedValue.id, expectedValue.id) XCTAssertEqual(decodedEncodedValue.profileGivenName, expectedValue.profileGivenName) XCTAssertEqual(decodedEncodedValue.profileFamilyName, expectedValue.profileFamilyName) XCTAssertEqual(decodedEncodedValue.profileBio, expectedValue.profileBio) XCTAssertEqual(decodedEncodedValue.profileBioEmoji, expectedValue.profileBioEmoji) XCTAssertEqual(decodedEncodedValue.profileAvatarData, expectedValue.profileAvatarData) XCTAssertEqual(decodedEncodedValue.visibleBadgeIds, expectedValue.visibleBadgeIds) XCTAssertEqual(decodedEncodedValue.userProfileWriter, expectedValue.userProfileWriter) // Test a stable decoding. let decodedValue = try Self.deserialize(data: XCTUnwrap(Data(base64Encoded: encodedValue))) XCTAssertEqual(decodedValue.id, expectedId) XCTAssertEqual(decodedValue.profileGivenName, expectedValue.profileGivenName) XCTAssertEqual(decodedValue.profileFamilyName, expectedValue.profileFamilyName) XCTAssertEqual(decodedValue.profileBio, expectedValue.profileBio) XCTAssertEqual(decodedValue.profileBioEmoji, expectedValue.profileBioEmoji) XCTAssertEqual(decodedValue.profileAvatarData, expectedValue.profileAvatarData) XCTAssertEqual(decodedValue.visibleBadgeIds, expectedValue.visibleBadgeIds) XCTAssertEqual(decodedValue.userProfileWriter, expectedValue.userProfileWriter) } } private static func serialize(_ pendingProfileUpdate: PendingProfileUpdate) throws -> Data { try NSKeyedArchiver.archivedData(withRootObject: pendingProfileUpdate, requiringSecureCoding: false) } private static func deserialize(data: Data) throws -> PendingProfileUpdate { NSKeyedUnarchiver.setClass(PendingProfileUpdate.self, forClassName: "SignalMessaging.PendingProfileUpdate") return try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(ofClass: PendingProfileUpdate.self, from: data, requiringSecureCoding: false)) } }