diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 14c827bd5..48f489b10 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -149,6 +149,7 @@ 3CA6CE0A28E4F19B00CA0585 /* OSUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */; }; 3CA8B8822BEC2FCB0010ADA1 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; }; 3CA8B8832BEC2FCB0010ADA1 /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */; }; 3CBB6C262ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */; }; 3CC063942B6D6B6B002BB07F /* OneSignalCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063932B6D6B6B002BB07F /* OneSignalCore.m */; }; 3CC063A22B6D7A8E002BB07F /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; @@ -1352,6 +1353,7 @@ 3C9AD6D02B228B9200BC1540 /* OSRequestRemoveAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestRemoveAlias.swift; sourceTree = ""; }; 3C9AD6D22B228BB000BC1540 /* OSRequestUpdateProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestUpdateProperties.swift; sourceTree = ""; }; 3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserRequest.swift; sourceTree = ""; }; + 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerTests.swift; sourceTree = ""; }; 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsistencyManagerTestHelpers.swift; sourceTree = ""; }; 3CC063932B6D6B6B002BB07F /* OneSignalCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalCore.m; sourceTree = ""; }; 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalCoreMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2155,6 +2157,7 @@ children = ( 3C01519B2C2E29F90079E076 /* IAMRequestTests.m */, 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */, + 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */, 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */, ); path = OneSignalInAppMessagesTests; @@ -4295,6 +4298,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */, 3C7021E92ECF0CF4001768C6 /* IAMIntegrationTests.swift in Sources */, 3C01519C2C2E29F90079E076 /* IAMRequestTests.m in Sources */, ); diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m index 13f7fa542..51826f8d9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m @@ -181,11 +181,9 @@ - (BOOL)messageMatchesTriggers:(OSInAppMessageInternal *)message { - (BOOL)evaluateTrigger:(OSTrigger *)trigger forMessage:(OSInAppMessageInternal *)message { if (!self.triggers[trigger.property] && [trigger.kind isEqualToString:OS_DYNAMIC_TRIGGER_KIND_CUSTOM]) { // The value doesn't exist - - // The condition for this trigger is true since the value doesn't exist - // Either loop to the next condition, or return true if we are the last condition - return trigger.operatorType == OSTriggerOperatorTypeNotExists || - (trigger.value && trigger.operatorType == OSTriggerOperatorTypeNotEqualTo); + + // Only NotExists operator should return true for non-existent values + return trigger.operatorType == OSTriggerOperatorTypeNotExists; } else if (trigger.operatorType == OSTriggerOperatorTypeExists) { return true; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift index 8bdee3073..931ad17fe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift @@ -78,13 +78,13 @@ public class IAMTestHelpers: NSObject { /// Returns the JSON of an in-app message with trigger. @objc - public static func testMessageJsonWithTrigger(property: String, triggerId: String, type: Int32, value: Any) -> [String: Any] { + public static func testMessageJsonWithTrigger(kind: String, property: String, triggerId: String, type: Int32, value: Any) -> [String: Any] { var testMessage = self.testDefaultMessageJson() testMessage["triggers"] = [ [ [ - "kind": property, + "kind": kind, "property": property, "operator": OS_OPERATOR_TO_STRING(type), "value": value, diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift index 19bafe842..fe968a899 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift @@ -88,7 +88,7 @@ final class IAMIntegrationTests: XCTestCase { MockUserRequests.setDefaultCreateAnonUserResponses(with: client) // 3. Set up mock responses for fetching IAMs - let message = IAMTestHelpers.testMessageJsonWithTrigger(property: "session_time", triggerId: "test_id1", type: 1, value: 10.0) + let message = IAMTestHelpers.testMessageJsonWithTrigger(kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, property: "session_time", triggerId: "test_id1", type: 1, value: 10.0) let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message]) client.setMockResponseForRequest( request: "", diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift new file mode 100644 index 000000000..cdc2aa7f0 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift @@ -0,0 +1,122 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalInAppMessagesMocks + +final class TriggerTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + /** + Test that NotEqualTo trigger does NOT match non-existent properties. + */ + func testNotEqualToTrigger_doesNotMatchNonExistentProperty() throws { + /* Setup */ + let triggerController = OSTriggerController() + + // Create a message with NotEqualTo trigger + let messageJson = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "prop", + triggerId: "test_trigger", + type: 3, // OSTriggerOperatorTypeNotEqualTo + value: "value" + ) + let message = OSInAppMessageInternal.instance(withJson: messageJson) + XCTAssertNotNil(message, "Message should be created successfully") + + // Test 1: NotEqualTo should NOT match when property doesn't exist + XCTAssertFalse(triggerController.messageMatchesTriggers(message!)) + + // Test 2: NotEqualTo SHOULD match when property exists with different value + triggerController.addTriggers(["prop": "other"]) + XCTAssertTrue(triggerController.messageMatchesTriggers(message!)) + + // Test 3: NotEqualTo should NOT match when property exists with same value + triggerController.addTriggers(["prop": "value"]) + XCTAssertFalse(triggerController.messageMatchesTriggers(message!)) + } + + /** + Test that NotExists trigger correctly matches non-existent properties. + */ + func testNotExistsTrigger_matchesNonExistentProperty() throws { + /* Setup */ + let triggerController = OSTriggerController() + + // Create a message with NotExists trigger + let messageJson = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "prop", + triggerId: "test_trigger", + type: 7, // OSTriggerOperatorTypeNotExists + value: "value" + ) + let message = OSInAppMessageInternal.instance(withJson: messageJson) + XCTAssertNotNil(message, "Message should be created successfully") + + // Test 1: NotExists SHOULD match when property doesn't exist + XCTAssertTrue(triggerController.messageMatchesTriggers(message!)) + + // Test 2: NotExists should NOT match when property exists + triggerController.addTriggers(["prop": "other"]) + XCTAssertFalse(triggerController.messageMatchesTriggers(message!)) + } + + /** + Test that Exists trigger correctly matches existing properties. + */ + func testExistsTrigger_matchesExistingProperty() throws { + /* Setup */ + let triggerController = OSTriggerController() + + // Create a message with Exists trigger + let messageJson = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "prop", + triggerId: "test_trigger", + type: 6, // OSTriggerOperatorTypeExists + value: "value" + ) + let message = OSInAppMessageInternal.instance(withJson: messageJson) + XCTAssertNotNil(message, "Message should be created successfully") + + // Test 1: Exists should NOT match when property doesn't exist + XCTAssertFalse(triggerController.messageMatchesTriggers(message!)) + + // Test 2: Exists SHOULD match when property exists + triggerController.addTriggers(["prop": "other"]) + XCTAssertTrue(triggerController.messageMatchesTriggers(message!)) + } +}