Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
3C47A975292642B100312125 /* OneSignalConfigManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C47A973292642B100312125 /* OneSignalConfigManager.m */; };
3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */; };
3C5117172B15C31E00563465 /* OSUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5117162B15C31E00563465 /* OSUserState.swift */; };
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */; };
3C789DBD293C2206004CF83D /* OSFocusInfluenceParam.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A600B432453790700514A53 /* OSFocusInfluenceParam.m */; };
3C789DBE293D8EAD004CF83D /* OSFocusInfluenceParam.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A600B41245378ED00514A53 /* OSFocusInfluenceParam.h */; settings = {ATTRIBUTES = (Public, ); }; };
3C7A39C12B7BED900082665E /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; };
Expand Down Expand Up @@ -1124,6 +1125,7 @@
3C47A973292642B100312125 /* OneSignalConfigManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalConfigManager.m; sourceTree = "<group>"; };
3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSOperationRepo.swift; sourceTree = "<group>"; };
3C5117162B15C31E00563465 /* OSUserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserState.swift; sourceTree = "<group>"; };
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = "<group>"; };
3C7A39D42B7C18EE0082665E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserRequests.swift; sourceTree = "<group>"; };
3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefines.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1963,6 +1965,7 @@
isa = PBXGroup;
children = (
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */,
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */,
);
path = OneSignalUserTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -3743,6 +3746,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,62 @@
extension NSDictionary {
func contains(key: String, value: Any) -> Bool {
/*
This method goes one level deep into dictionaries
*/
private func contains(key: String, value: Any) -> Bool {
guard let dictVal = self[key] else {
return false
}

return equals(dictVal, value)
if let value = value as? [String: Any],
let dictVal = dictVal as? NSDictionary {
return dictVal.contains(value)
} else {
return equals(dictVal, value)
}
}

func contains(_ dict: [String: Any]) -> Bool {
/*
let parent = [
"apple": [
"type": "fruit",
"count": 5,
"fresh": true
],
"orange": [
"type": "color"
],
"cactus": "error"
]

// 1. Example 1 -
let child1 = [
"apple": [
"type": "fruit",
"fresh": true
]
]
parent.contains(child1) = true

// 2. Example 2 -
let child2 = [
"apple": [
"type": "fruit"
],
"orange": [
"type": "fruit"
]
]
parent.contains(child2) = false

// 3. Example 3 -
let child3 = [
"orange": [
"type": "color"
],
"cactus": "error"
]
parent.contains(child3) = true
*/
public func contains(_ dict: [String: Any]) -> Bool {
for (key, value) in dict {
if !contains(key: key, value: value) {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
if executeInstantaneously {
finishExecutingRequest(request, onSuccess: successBlock, onFailure: failureBlock)
} else {
executionQueue.async {
executionQueue.asyncAfter(deadline: .now() + .milliseconds(50)) {
self.finishExecutingRequest(request, onSuccess: successBlock, onFailure: failureBlock)
}
}
Expand Down Expand Up @@ -177,4 +177,10 @@ extension MockOneSignalClient {

return found
}

public func hasExecutedRequestOfType(_ type: AnyClass) -> Bool {
executedRequests.contains { request in
request.isKind(of: type)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
init(aliases: [String: String], identityModel: OSIdentityModel) {
self.identityModel = identityModel
self.aliases = aliases
self.stringDescription = "OSRequestAddAliases with aliases: \(aliases)"
self.stringDescription = "<OSRequestAddAliases with aliases: \(aliases)>"
super.init()
self.parameters = ["identity": aliases]
self.method = PATCH
Expand Down Expand Up @@ -82,7 +82,7 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
}
self.identityModel = identityModel
self.aliases = aliases
self.stringDescription = "OSRequestAddAliases with parameters: \(parameters)"
self.stringDescription = "<OSRequestAddAliases with aliases: \(aliases)>"
super.init()
self.parameters = parameters
self.method = HTTPMethod(rawValue: rawMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest {
init(subscriptionModel: OSSubscriptionModel, identityModel: OSIdentityModel) {
self.subscriptionModel = subscriptionModel
self.identityModel = identityModel
self.stringDescription = "OSRequestCreateSubscription with subscriptionModel: \(subscriptionModel.address ?? "nil")"
self.stringDescription = "<OSRequestCreateSubscription with token: \(subscriptionModel.address ?? "nil")>"
super.init()
self.parameters = ["subscription": subscriptionModel.jsonRepresentation()]
self.method = POST
Expand Down Expand Up @@ -86,7 +86,7 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest {
}
self.subscriptionModel = subscriptionModel
self.identityModel = identityModel
self.stringDescription = "OSRequestCreateSubscription with subscriptionModel: \(subscriptionModel.address ?? "nil")"
self.stringDescription = "<OSRequestCreateSubscription with token: \(subscriptionModel.address ?? "nil")>"
super.init()
self.parameters = parameters
self.method = HTTPMethod(rawValue: rawMethod)
Expand Down
61 changes: 61 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,31 @@ extension MockUserRequests {
)
}

/**
Returns many user data to mimic pulling remote data. Used to test for hydration.
*/
public static func setDefaultFetchUserResponseForHydration(with client: MockOneSignalClient, externalId: String) {
let osid = getOneSignalId(for: externalId)

var fetchResponse: [String: Any] = [
"identity": ["onesignal_id": osid, "external_id": externalId, "remote_alias": "remote_id"],
"properties": [
"tags": ["remote_tag": "remote_value"],
"language": "remote_language"
],
"subscriptions": [
["type": "Email",
"id": "remote_email_id",
"token": "remote_email@example.com"
]
]
]
client.setMockResponseForRequest(
request: "<OSRequestFetchUser with external_id: \(externalId)>",
response: fetchResponse
)
}

public static func setAddTagsResponse(with client: MockOneSignalClient, tags: [String: String]) {
let tagsResponse = MockUserRequests.testPropertiesPayload(properties: ["tags": tags])

Expand All @@ -133,4 +158,40 @@ extension MockUserRequests {
response: tagsResponse
)
}

public static func setSetLanguageResponse(with client: MockOneSignalClient, language: String) {
client.setMockResponseForRequest(
request: "<OSRequestUpdateProperties with properties: [\"language\": Optional(\"\(language)\")] deltas: nil refreshDeviceMetadata: false>",
response: [:] // The SDK does not use the response in any way
)
}

public static func setAddAliasesResponse(with client: MockOneSignalClient, aliases: [String: String]) {
client.setMockResponseForRequest(
request: "<OSRequestAddAliases with aliases: \(aliases)>",
response: [:] // The SDK does not use the response in any way
)
}

/** The real response will either contain a subscription payload or be empty (if already exists on user) */
public static func setAddEmailResponse(with client: MockOneSignalClient, email: String) {
let response = [
"subscription": [
"id": "\(email)_id",
"type": "Email",
"token": email
]
]
client.setMockResponseForRequest(
request: "<OSRequestCreateSubscription with token: \(email)>",
response: response
)
}

public static func setTransferSubscriptionResponse(with client: MockOneSignalClient, externalId: String) {
client.setMockResponseForRequest(
request: "<OSRequestTransferSubscription to external_id: \(externalId)>",
response: [:] // The SDK does not use the response
)
}
}
103 changes: 0 additions & 103 deletions iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,107 +137,4 @@ final class OneSignalUserTests: XCTestCase {
identityModel.clearData()
}
}

func testSwitchUser_sendsCorrectTags() throws {
/* Setup */

let client = MockOneSignalClient()

// 1. Set up mock responses for the anonymous user
MockUserRequests.setDefaultCreateAnonUserResponses(with: client)

// 2. Set up mock responses for User A
let tagsUserA = ["tag_a": "value_a"]
MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: userA_EUID)
MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserA)

// 3. Set up mock responses for User B
let tagsUserB = ["tag_b": "value_b"]
MockUserRequests.setDefaultCreateUserResponses(with: client, externalId: userB_EUID)
MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserB)

OneSignalCoreImpl.setSharedClient(client)

/* When */

// 1. Login to user A and add tag
OneSignalUserManagerImpl.sharedInstance.login(externalId: userA_EUID, token: nil)
OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_a", value: "value_a")

// 2. Login to user B and add tag
OneSignalUserManagerImpl.sharedInstance.login(externalId: userB_EUID, token: nil)
OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_b", value: "value_b")

// 3. Run background threads
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

/* Then */

// Assert that every request SDK makes has a response set, and is handled
XCTAssertTrue(client.allRequestsHandled)

// Assert there is only one request containing these tags and they are sent to the Anon User
// This is because the Identify User request succeeded, so the user remains the same
XCTAssertTrue(client.onlyOneRequest(
contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)",
contains: ["properties": ["tags": tagsUserA]])
)
// Assert there is only one request containing these tags and they are sent to userB
XCTAssertTrue(client.onlyOneRequest(
contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)",
contains: ["properties": ["tags": tagsUserB]])
)
}

/**
Motivation: We had a bug where we did not hydrate the middle user's OSID, so any pending updates got dropped.
*/
func testIdentifyUserWithConflict_whenNotCurrentUser_sendsCorrectTags() throws {
/* Setup */

let client = MockOneSignalClient()
OneSignalCoreImpl.setSharedClient(client)

// 1. Set up mock responses for the anonymous user
MockUserRequests.setDefaultCreateAnonUserResponses(with: client)

// 2. Set up mock responses for User A with 409 conflict response
let tagsUserA = ["tag_a": "value_a"]
MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: userA_EUID, conflicted: true)
MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserA)

// 3. Set up mock responses for User B
let tagsUserB = ["tag_b": "value_b"]
MockUserRequests.setDefaultCreateUserResponses(with: client, externalId: userB_EUID)
MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserB)

/* When */

// 1. Login to user A (will result in 409 conflict) and add tag
OneSignalUserManagerImpl.sharedInstance.login(externalId: userA_EUID, token: nil)
OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_a", value: "value_a")

// 2. Login to user B and add tag
OneSignalUserManagerImpl.sharedInstance.login(externalId: userB_EUID, token: nil)
OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_b", value: "value_b")

// 3. Run background threads
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

/* Then */

// Assert that every request SDK makes has a response set, and is handled
XCTAssertTrue(client.allRequestsHandled)

// Assert only one request containing these tags and they are sent to userA by external_id
XCTAssertTrue(client.onlyOneRequest(
contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)",
contains: ["properties": ["tags": tagsUserA]])
)
// Assert there is only one request containing these tags and they are sent to userB
XCTAssertTrue(client.onlyOneRequest(
contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)",
contains: ["properties": ["tags": tagsUserB]])
)
}
}
Loading