diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 5e61c85..f5f35e9 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -81,7 +81,8 @@ final class EdgeAPI: Sendable { .compactMap({ URLQueryItem(name: "id", value: $0) }) let hidQueryItems = hids - .compactMap({ $0.extendedIdentifier }) + .map({ $0.extendedIdentifier }) + .filter({ $0.isEmpty == false }) .compactMap({ URLQueryItem(name: "hid", value: $0) }) queryItems.append(contentsOf: hidQueryItems) diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index 8a65694..8aa0ab9 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -31,6 +31,9 @@ enum OptableIdentifierEncoder { case let .hem(value): hem(prefix, value) case let .hashedPhoneNumber(value): hashedPhoneNumber(prefix, value) } + + // An identifier whose value is empty once normalized is dropped rather than sent as "prefix:" + guard let separator = eid.firstIndex(of: ":"), eid.index(after: separator) < eid.endIndex else { return "" } return eid } @@ -50,16 +53,20 @@ enum OptableIdentifierEncoder { } /// Builds Extended Identifier from Email address + /// Returns an empty string when nothing is left to hash, so the SHA256 of an empty string is never sent. static func email(_ prefix: String, _ email: String) -> String { - let normalizedData = Data(email.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) - let identifier = sha256(data: normalizedData) + let normalized = email.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() + guard normalized.isEmpty == false else { return "" } + let identifier = sha256(data: Data(normalized.utf8)) return "\(prefix):\(identifier)" } /// Builds Extended Identifier from Phone number + /// Returns an empty string when nothing is left to hash, so the SHA256 of an empty string is never sent. static func phoneNumber(_ prefix: String, _ phoneNumber: String) -> String { - let normalizedData = Data(phoneNumber.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) - let identifier = sha256(data: normalizedData) + let normalized = phoneNumber.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() + guard normalized.isEmpty == false else { return "" } + let identifier = sha256(data: Data(normalized.utf8)) return "\(prefix):\(identifier)" } diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index 0894270..bace3e6 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -368,6 +368,16 @@ class EdgeAPITests: XCTestCase { /** For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/profile) */ + func test_targeting_request_drops_blank_identifiers() throws { + let postalCode: OptableIdentifier = .postalCode("H2X 1Y4") + let blank: OptableIdentifier = .postalCode(" ") + let urlRequest = try sdk.api.targeting(ids: [postalCode, blank], hids: [postalCode, blank]) + let queryItems = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)!.queryItems ?? [] + + XCTAssertEqual(queryItems.filter({ $0.name == "id" }).map(\.value), [postalCode.extendedIdentifier]) + XCTAssertEqual(queryItems.filter({ $0.name == "hid" }).map(\.value), [postalCode.extendedIdentifier]) + } + func test_profile_request_generation() throws { let urlRequest = try sdk.api.profile(traits: ["test-key": "test-value"], id: "c:id2", neighbors: ["c:id1", "c:id3"]) diff --git a/Tests/Unit/OptableIdentifierEncoderTests.swift b/Tests/Unit/OptableIdentifierEncoderTests.swift index ea44ed1..5a59ff0 100644 --- a/Tests/Unit/OptableIdentifierEncoderTests.swift +++ b/Tests/Unit/OptableIdentifierEncoderTests.swift @@ -142,6 +142,27 @@ class OptableIdentifierEncoderTests: XCTestCase { XCTAssertNotEqual(unexpected, SUT.custom(prefix, "foobarBAZ-01234#98765.!!!")) } + func test_emptyAfterNormalization_isDropped() { + let blank = " \n " + let identifiers: [OptableIdentifier] = [ + .emailAddress(blank), .phoneNumber(blank), .postalCode(blank), + .ipv4Address(blank), .ipv6Address(blank), + .appleIDFA(blank), .googleGAID(blank), .rokuRIDA(blank), .samsungTIFA(blank), .amazonFireAFAI(blank), + .netID(blank), .id5(blank), .utiq(blank), + .custom(nil, blank), .custom(2, blank), .optableVID(blank), + .hem(blank), .hashedPhoneNumber(blank), + ] + + for identifier in identifiers { + XCTAssertEqual("", identifier.extendedIdentifier, "\(identifier) should be dropped") + } + } + + func test_email_and_phoneNumber_neverSendTheHashOfAnEmptyString() { + XCTAssertEqual("", SUT.email("e", "")) + XCTAssertEqual("", SUT.phoneNumber("p", " ")) + } + func test_hem() { let prefix = OptableIdentifier.hem("").prefix let hem = "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift index a839baf..73d0fa2 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -27,7 +27,7 @@ class OptableIdentifiersTests: XCTestCase { .id5("ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg"), .utiq("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), .custom(nil, "d29c551097b9dd0b82423827f65161232efaf7fc"), - .custom(2, ""), + .custom(2, "two"), .custom(1, "AaaZza.dh012"), .custom(1, "another c1"), ] @@ -52,7 +52,7 @@ class OptableIdentifiersTests: XCTestCase { XCTAssertTrue(decodedData.contains(where: { $0 == "f:6e853799-ef31-4a30-8706-9742be254d38" })) XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ" })) XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc" })) - XCTAssertTrue(decodedData.contains(where: { $0 == "c2:" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "c2:two" })) XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012" })) // Test multiple similar ids existance ( double c1 ) @@ -62,7 +62,7 @@ class OptableIdentifiersTests: XCTestCase { let c_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c:d29c551097b9dd0b82423827f65161232efaf7fc")) let c1_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c1:AaaZza.dh012")) - let c2_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c2:")) + let c2_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c2:two")) XCTAssert(c_Idx < c2_Idx) XCTAssert(c2_Idx < c1_Idx)