Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,10 @@ let package = Package(
exclude: ["Dummy.swift", "Info.plist"],
resources: [.process("PrivacyInfo.xcprivacy")],
publicHeadersPath: "."),
.testTarget(
name: "mParticle-AdobeTests",
dependencies: ["mParticle-Adobe"],
path: "mParticle-AdobeTests"
),
]
)
10 changes: 7 additions & 3 deletions mParticle-Adobe-Media/MPIAdobe.m
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ @interface MPIAdobe ()

@implementation MPIAdobe

- (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserId:(NSString *)advertiserId pushToken:(NSString *)pushToken organizationId:(NSString *)organizationId userIdentities:(NSDictionary<NSNumber *, NSString *> *)userIdentities audienceManagerServer:(NSString *)audienceManagerServer completion:(void (^)(NSString *marketingCloudId, NSString *blob, NSString *locationHint, NSError *))completion {
- (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId
advertiserId:(NSString *)advertiserId
pushToken:(NSString *)pushToken
organizationId:(NSString *)organizationId
userIdentities:(NSDictionary<NSNumber *, NSString *> *)userIdentities
audienceManagerServer:(NSString *)audienceManagerServer
completion:(void (^)(NSString *marketingCloudId, NSString *blob, NSString *locationHint, NSError *))completion {

if (audienceManagerServer != nil && audienceManagerServer.length > 0) {
host = audienceManagerServer;
Expand Down Expand Up @@ -147,8 +153,6 @@ - (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserI
__weak MPIAdobe *weakSelf = self;

[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {


void (^callbackWithCode)(MPIAdobeErrorCode code, NSString *message, NSError *error) = ^void(MPIAdobeErrorCode code, NSString *message, NSError *error) {
MPIAdobeError *adobeError = [[MPIAdobeError alloc] initWithCode:code message:message error:error];
NSError *compositeError = [NSError errorWithDomain:errorDomain code:adobeError.code userInfo:@{MPIAdobeErrorKey:adobeError}];
Expand Down
9 changes: 9 additions & 0 deletions mParticle-Adobe/MPIAdobe.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
#import <Foundation/Foundation.h>

@protocol SessionProtocol

- (NSURLSessionDataTask * _Nonnull)dataTaskWithRequest:(NSURLRequest * _Nonnull)request
completionHandler:(void (NS_SWIFT_SENDABLE ^_Nonnull)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

@end

@interface MPIAdobe : NSObject

- (instancetype)initWithSession:(id<SessionProtocol>) session;

- (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserId:(NSString *)advertiserId pushToken:(NSString *)pushToken organizationId:(NSString *)organizationId userIdentities:(NSDictionary<NSNumber *, NSString *> *)userIdentities audienceManagerServer:(NSString *)audienceManagerServer completion:(void (^)(NSString *marketingCloudId, NSString *locationHint, NSString *blob, NSError *error))completion;

- (NSString *)marketingCloudIdFromUserDefaults;
Expand Down
34 changes: 26 additions & 8 deletions mParticle-Adobe/MPIAdobe.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

static NSString *const marketingCloudIdUserDefaultsKey = @"ADBMOBILE_PERSISTED_MID";


@interface MPIAdobeError ()

- (id)initWithCode:(MPIAdobeErrorCode)code message:(NSString *)message error:(NSError *)error;
Expand Down Expand Up @@ -79,7 +80,23 @@ @interface MPIAdobe ()

@implementation MPIAdobe

- (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserId:(NSString *)advertiserId pushToken:(NSString *)pushToken organizationId:(NSString *)organizationId userIdentities:(NSDictionary<NSNumber *, NSString *> *)userIdentities audienceManagerServer:(NSString *)audienceManagerServer completion:(void (^)(NSString *marketingCloudId, NSString *locationHint, NSString *blob, NSError *))completion {
id<SessionProtocol> _session;

- (instancetype)initWithSession:(id<SessionProtocol>) session {
self = [super init];
if (self != nil) {
_session = session;
}
return self;
}

- (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId
advertiserId:(NSString *)advertiserId
pushToken:(NSString *)pushToken
organizationId:(NSString *)organizationId
userIdentities:(NSDictionary<NSNumber *, NSString *> *)userIdentities
audienceManagerServer:(NSString *)audienceManagerServer
completion:(void (^)(NSString *marketingCloudId, NSString *locationHint, NSString *blob, NSError *))completion {

if (audienceManagerServer != nil && audienceManagerServer.length > 0) {
host = audienceManagerServer;
Expand Down Expand Up @@ -139,14 +156,10 @@ - (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserI
NSURL *url = components.URL;

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

__weak MPIAdobe *weakSelf = self;

[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {


[[_session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
void (^callbackWithCode)(MPIAdobeErrorCode code, NSString *message, NSError *error) = ^void(MPIAdobeErrorCode code, NSString *message, NSError *error) {
MPIAdobeError *adobeError = [[MPIAdobeError alloc] initWithCode:code message:message error:error];
NSError *compositeError = [NSError errorWithDomain:errorDomain code:adobeError.code userInfo:@{MPIAdobeErrorKey:adobeError}];
Expand All @@ -166,8 +179,13 @@ - (void)sendRequestWithMarketingCloudId:(NSString *)marketingCloudId advertiserI

NSDictionary *errorDictionary = dictionary[errorResponseKey];
if (errorDictionary) {
NSError *error = [NSError errorWithDomain:serverErrorDomain code:0 userInfo:errorDictionary];
return callbackWithCode(MPIAdobeErrorCodeServerError, @"Server returned an error", error);
if ([errorDictionary isKindOfClass:[NSDictionary class]]) {
NSError *error = [NSError errorWithDomain:serverErrorDomain code:0 userInfo:errorDictionary];
return callbackWithCode(MPIAdobeErrorCodeServerError, @"Server returned an error", error);
} else {
NSError *error = [NSError errorWithDomain:serverErrorDomain code:0 userInfo:@{}];
return callbackWithCode(MPIAdobeErrorCodeServerError, @"Server returned an error", error);
}
}

NSString *marketingCloudId = [dictionary[marketingCloudIdKey] isKindOfClass:[NSString class]] ? dictionary[marketingCloudIdKey] : nil;
Expand Down
15 changes: 13 additions & 2 deletions mParticle-Adobe/MPKitAdobe.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ @interface MPKitAdobe ()

@end

@interface NSURLSession (SessionProtocol) <SessionProtocol>
@end

@implementation MPKitAdobe

static NSString *_midOverride = nil;
Expand Down Expand Up @@ -70,7 +73,9 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu

_configuration = configuration;
_started = YES;
_adobe = [[MPIAdobe alloc] init];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
_adobe = [[MPIAdobe alloc] initWithSession: session];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didEnterBackground:)
Expand Down Expand Up @@ -155,7 +160,13 @@ - (void)sendNetworkRequest {
NSString *pushToken = [self pushToken];
FilteredMParticleUser *user = [self currentUser];
NSDictionary *userIdentities = user.userIdentities;
[_adobe sendRequestWithMarketingCloudId:marketingCloudId advertiserId:advertiserId pushToken:pushToken organizationId:_organizationId userIdentities:userIdentities audienceManagerServer:_audienceManagerServer completion:^(NSString *marketingCloudId, NSString *locationHint, NSString *blob, NSError *error) {
[_adobe sendRequestWithMarketingCloudId:marketingCloudId
advertiserId:advertiserId
pushToken:pushToken
organizationId:_organizationId
userIdentities:userIdentities
audienceManagerServer:_audienceManagerServer
completion:^(NSString *marketingCloudId, NSString *locationHint, NSString *blob, NSError *error) {
if (error) {
NSLog(@"mParticle -> Adobe kit request failed with error: %@", error);
return;
Expand Down
3 changes: 3 additions & 0 deletions mParticle-Adobe/mParticle_Adobe.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ FOUNDATION_EXPORT const unsigned char mParticle_AdobeVersionString[];
#if defined(__has_include) && __has_include(<mParticle_Adobe/MPKitAdobe.h>)
#import <mParticle_Adobe/MPKitAdobe.h>
#else

#import "MPKitAdobe.h"
#import "MPIAdobe.h"

#endif
195 changes: 195 additions & 0 deletions mParticle-AdobeTests/MPAdobeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//
// MPKitAdobeTests.swift
// mParticle-Adobe
//
// Created by Denis Chilik on 10/9/25.
//
@testable import mParticle_Adobe
import Foundation
import XCTest

import Foundation

extension URLSession: @retroactive SessionProtocol {
}

final class MPAdobeTests: XCTestCase {
var session: SessionProtocolMock!

override func setUp() {
super.setUp()

session = SessionProtocolMock()
}

func testSendRequestEncodeAllParametersIntoURL() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "marketingCloudId",
advertiserId: "advertiserId",
pushToken: "pushToken",
organizationId: "organizationId",
userIdentities: [
NSNumber(value: MPUserIdentity.other.rawValue) : "1",
NSNumber(value: MPUserIdentity.customerId.rawValue) : "2",
NSNumber(value: MPUserIdentity.facebook.rawValue) : "3",
NSNumber(value: MPUserIdentity.twitter.rawValue) : "4",
NSNumber(value: MPUserIdentity.google.rawValue) : "5",
NSNumber(value: MPUserIdentity.microsoft.rawValue) : "6",
NSNumber(value: MPUserIdentity.yahoo.rawValue) : "7",
NSNumber(value: MPUserIdentity.email.rawValue) : "8",
NSNumber(value: MPUserIdentity.alias.rawValue) : "9",
NSNumber(value: MPUserIdentity.facebookCustomAudienceId.rawValue) : "10",
NSNumber(value: MPUserIdentity.other5.rawValue) : "11",
],
audienceManagerServer: "audienceManagerServer"
) { _, _, _, _ in }

let expected = "https://audienceManagerServer/id?d_mid=marketingCloudId&d_cid=20915%2501advertiserId&d_cid=20920%2501pushToken&d_cid_ic=google%25015&d_cid_ic=facebook%25013&d_cid_ic=customerid%25012&d_cid_ic=twitter%25014&d_cid_ic=alias%25019&d_cid_ic=microsoft%25016&d_cid_ic=email%25018&d_cid_ic=yahoo%25017&d_cid_ic=other%25011&d_cid_ic=facebookcustomaudienceid%250110&d_orgid=organizationId&d_ptfm=ios&d_ver=2"

guard
let actualURL = session.dataTaskRequestParam?.url,
let expectedURL = URL(string: expected),
let actualComponents = URLComponents(url: actualURL, resolvingAgainstBaseURL: false),
let expectedComponents = URLComponents(url: expectedURL, resolvingAgainstBaseURL: false)
else {
XCTFail("URLs could not be parsed")
return
}

XCTAssertEqual(actualComponents.scheme, expectedComponents.scheme)
XCTAssertEqual(actualComponents.host, expectedComponents.host)
XCTAssertEqual(actualComponents.path, expectedComponents.path)

let actualItems = Set(actualComponents.queryItems ?? [])
let expectedItems = Set(expectedComponents.queryItems ?? [])

XCTAssertEqual(actualItems, expectedItems)
}

func testCompletionCallback_success() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "",
advertiserId: "",
pushToken: "",
organizationId: "",
userIdentities: [:],
audienceManagerServer: ""
) { marketingCloudId, locationHint, blob, error in
XCTAssertNil(error)
XCTAssertEqual(marketingCloudId, "mock_mid")
XCTAssertEqual(locationHint, "mock_region")
XCTAssertEqual(blob, "mock_blob")
}

let json: [String: Any] = [
"d_mid": "mock_mid",
"d_blob": "mock_blob",
"dcs_region": "mock_region"
]

let data = try! JSONSerialization.data(withJSONObject: json, options: [])

session.dataTaskCompletionHandlerParam?(data, URLResponse(), nil)
}

func testCompletionCallback_success_empty_json() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "",
advertiserId: "",
pushToken: "",
organizationId: "",
userIdentities: [:],
audienceManagerServer: ""
) { marketingCloudId, locationHint, blob, error in
XCTAssertNil(error)
XCTAssertNil(marketingCloudId)
XCTAssertNil(locationHint)
XCTAssertNil(blob)
}

let json: [String: Any] = [:]

let data = try! JSONSerialization.data(withJSONObject: json, options: [])

session.dataTaskCompletionHandlerParam?(data, URLResponse(), nil)
}

func testCompletionCallback_success_parametersNotStrings() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "",
advertiserId: "",
pushToken: "",
organizationId: "",
userIdentities: [:],
audienceManagerServer: ""
) { marketingCloudId, locationHint, blob, error in
XCTAssertNil(error)
XCTAssertNil(marketingCloudId)
XCTAssertNil(locationHint)
XCTAssertNil(blob)
}

let json: [String: Any] = [
"d_mid": 1,
"d_blob": 2,
"dcs_region": 3
]

let data = try! JSONSerialization.data(withJSONObject: json, options: [])
session.dataTaskCompletionHandlerParam?(data, URLResponse(), nil)
}

func testCompletionCallback_success_errorFromBackend() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "",
advertiserId: "",
pushToken: "",
organizationId: "",
userIdentities: [:],
audienceManagerServer: ""
) { marketingCloudId, locationHint, blob, error in
XCTAssertNil(marketingCloudId)
XCTAssertNil(locationHint)
XCTAssertNil(blob)
XCTAssertNotNil(error)
}

let json: [String: Any] = [
"error_msg": [
"some_key": "Invalid request parameters"
]
]

let data = try! JSONSerialization.data(withJSONObject: json, options: [])
session.dataTaskCompletionHandlerParam?(data, URLResponse(), nil)
}

func testCompletionCallback_success_errorErrorMsgContains_shouldNotCrash() {
let sut = MPIAdobe(session: session)!
sut.sendRequest(
withMarketingCloudId: "",
advertiserId: "",
pushToken: "",
organizationId: "",
userIdentities: [:],
audienceManagerServer: ""
) { marketingCloudId, locationHint, blob, error in
XCTAssertNil(marketingCloudId)
XCTAssertNil(locationHint)
XCTAssertNil(blob)
XCTAssertNotNil(error)
}

let json: [String: Any] = [
"error_msg": "Invalid request parameters"
]

let data = try! JSONSerialization.data(withJSONObject: json, options: [])
session.dataTaskCompletionHandlerParam?(data, URLResponse(), nil)
}
}
25 changes: 25 additions & 0 deletions mParticle-AdobeTests/SessionProtocolMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// SessionProtocolMock.swift
// mParticle-Adobe
//
// Created by Denis Chilik on 10/14/25.
//

@testable import mParticle_Adobe

class SessionProtocolMock: SessionProtocol {
var dataTaskCalled = false
var dataTaskRequestParam: URLRequest?
var dataTaskCompletionHandlerParam: ((Data?, URLResponse?, (any Error)?) -> Void)?

func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask {
dataTaskCalled = true
dataTaskRequestParam = request
dataTaskCompletionHandlerParam = completionHandler

let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)

return session.dataTask(with: URLRequest(url: URL(string: "https://localhost")!))
}
}