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
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
37 changes: 30 additions & 7 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,13 +156,14 @@ - (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) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}

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];
Expand All @@ -166,8 +184,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")!))
}
}