Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
11 changes: 11 additions & 0 deletions IntegrationTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ Before getting started, install Tuist:
brew install tuist
```

### Environment Variables

The integration tests require mParticle API credentials to be set as environment variables:

```bash
export MPARTICLE_API_KEY="your-api-key"
export MPARTICLE_API_SECRET="your-api-secret"
```

**Important:** These environment variables must be set before running any integration test scripts (`run_wiremock_recorder.sh` or `run_clean_integration_tests.sh`). The scripts will fail with an error if these variables are not set.

Then generate the Xcode project:

```bash
Expand Down
163 changes: 155 additions & 8 deletions IntegrationTests/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class EventUploadWaiter: NSObject, MPListenerProtocol {
var mparticle = MParticle.sharedInstance()

@discardableResult
func wait(timeout: Int = 10) -> Bool {
func wait(timeout: Int = 5) -> Bool {
mparticle.upload()
let semaphore = DispatchSemaphore(value: 0)
uploadCompletedSemaphore = semaphore
Expand Down Expand Up @@ -39,7 +39,7 @@ func testSimpleEvent(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
}

// Test 2: Log Event with Custom Attributes and Custom Flags
// Based on ViewController.m logEvent method (lines 131-147)
// Based on ViewController.m logEvent method
func testEventWithCustomAttributesAndFlags(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
let event = MPEvent(name: "Event Name", type: .transaction)

Expand Down Expand Up @@ -69,14 +69,14 @@ func testEventWithCustomAttributesAndFlags(mparticle: MParticle, uploadWaiter: E
}

// Test 3: Log Screen
// Based on ViewController.m logScreen method (lines 149-151)
// Based on ViewController.m logScreen method
func testLogScreen(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
mparticle.logScreen("Home Screen", eventInfo: nil)
uploadWaiter.wait()
}

// Test 4: Log Commerce Event with Product and Transaction
// Based on ViewController.m logCommerceEvent method (lines 153-180)
// Based on ViewController.m logCommerceEvent method
func testCommerceEvent(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
let product = MPProduct(
name: "Awesome Book",
Expand Down Expand Up @@ -112,7 +112,7 @@ func testCommerceEvent(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
}

// Test 5: Rokt Select Overlay Placement
// Based on ViewController.m selectOverlayPlacement method (lines 182-192)
// Based on ViewController.m selectOverlayPlacement method
// Tests Rokt SDK integration through mParticle for selecting placements with custom attributes
func testRoktSelectPlacement(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
let roktAttributes: [String: String] = [
Expand All @@ -128,9 +128,150 @@ func testRoktSelectPlacement(mparticle: MParticle, uploadWaiter: EventUploadWait
uploadWaiter.wait()
}

// Test 6: Get User Audiences
// Based on ViewController.m getAudience method
// Tests retrieving audience memberships for the current user via Identity API
func testGetUserAudiences(mparticle: MParticle) {
let semaphore = DispatchSemaphore(value: 0)

// Get audiences for current user
if let currentUser = mparticle.identity.currentUser {
currentUser.getAudiencesWithCompletionHandler { audiences, error in
if let error = error {
print("Failed to retrieve Audience: \(error)")
} else {
print("Successfully retrieved Audience for user: \(currentUser.userId) with audiences: \(audiences)")
}
semaphore.signal()
}
} else {
print("No current user available")
semaphore.signal()
}

// Wait for async completion (timeout 10 seconds)
let timeout = DispatchTime.now() + .seconds(10)
let result = semaphore.wait(timeout: timeout)

if result == .timedOut {
print("Warning: getAudiencesWithCompletionHandler timed out")
}
}

// Test 7: Log Timed Event
// Based on ViewController.m logTimedEvent method
// Tests logging timed events - begins a timed event, waits a fixed duration, then ends it
func testLogTimedEvent(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
// Begin a timed event
let eventName = "Timed Event"
let timedEvent = MPEvent(name: eventName, type: .transaction)

if let event = timedEvent {
mparticle.beginTimedEvent(event)

// Use fixed delay instead of random (required for deterministic testing)
// Original code uses arc4random_uniform(4000.0) / 1000.0 + 1.0 which is 1-5 seconds
// We use fixed 2 seconds for consistent test behavior
sleep(2)

// Retrieve the timed event by name and end it
if let retrievedTimedEvent = mparticle.event(withName: eventName) {
mparticle.endTimedEvent(retrievedTimedEvent)
}
}

uploadWaiter.wait()
}

// Test 8: Log Error
// Based on ViewController.m logError method
// Tests logging errors with custom event info dictionary
func testLogError(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
// Log an error with event info - exactly as in ViewController.m
let eventInfo = ["cause": "slippery floor"]
mparticle.logError("Oops", eventInfo: eventInfo)

uploadWaiter.wait()
}

// Test 9: Log Exception
// Based on ViewController.m logException method
// Tests logging NSException with topmost context information
func testLogException(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
// Create an NSException similar to the one caught in ViewController.m
// The original code tries to invoke a non-existing method which throws NSException
let exception = NSException(
name: NSExceptionName(rawValue: "NSInvalidArgumentException"),
reason: "-[ViewController someMethodThatDoesNotExist]: unrecognized selector sent to instance",
userInfo: nil
)

// Log the exception - mParticle SDK will capture exception details
// Note: topmostContext parameter is not available in Swift API,
// so we use the simpler logException method
mparticle.logException(exception)

uploadWaiter.wait()
}

// Test 10: Set User Attributes
// Based on ViewController.m setUserAttribute method
// Tests setting predefined and custom user attributes on the current user
func testSetUserAttributes(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
guard let currentUser = mparticle.identity.currentUser else {
print("No current user available")
return
}

// Set 'Age' as a user attribute using predefined mParticle constant
// Using static value instead of random for deterministic testing
let age = "45" // Original: 21 + arc4random_uniform(80)
currentUser.setUserAttribute(mParticleUserAttributeAge, value: age)

// Set 'Gender' as a user attribute using predefined mParticle constant
// Using static value instead of random for deterministic testing
let gender = "m" // Original: arc4random_uniform(2) ? "m" : "f"
currentUser.setUserAttribute(mParticleUserAttributeGender, value: gender)

// Set a numeric user attribute using a custom key
currentUser.setUserAttribute("Achieved Level", value: 4)

uploadWaiter.wait()
}

// Test 11: Increment User Attribute
// Based on ViewController.m incrementUserAttribute method
// Tests incrementing a numeric user attribute by a specified value
func testIncrementUserAttribute(mparticle: MParticle, uploadWaiter: EventUploadWaiter) {
guard let currentUser = mparticle.identity.currentUser else {
print("No current user available")
return
}

// First, set an initial value for the attribute to ensure it exists
// Using static value 10 for deterministic testing
currentUser.setUserAttribute("Achieved Level", value: 10)

// Wait for the initial set to be uploaded
uploadWaiter.wait()

// Now increment the attribute by 1 - exactly as in ViewController.m
currentUser.incrementUserAttribute("Achieved Level", byValue: NSNumber(value: 1))

// Wait for the increment to be uploaded
uploadWaiter.wait()
}

// Read API key and secret from environment variables
guard let apiKey = ProcessInfo.processInfo.environment["MPARTICLE_API_KEY"],
let apiSecret = ProcessInfo.processInfo.environment["MPARTICLE_API_SECRET"] else {
print("Error: MPARTICLE_API_KEY and MPARTICLE_API_SECRET environment variables must be set")
exit(1)
}

var options = MParticleOptions(
key: "",
secret: ""
key: apiKey,
secret: apiSecret
)

var identityRequest = MPIdentityApiRequest.withEmptyUser()
Expand Down Expand Up @@ -167,4 +308,10 @@ testSimpleEvent(mparticle: mparticle, uploadWaiter: uploadWaiter)
testEventWithCustomAttributesAndFlags(mparticle: mparticle, uploadWaiter: uploadWaiter)
testLogScreen(mparticle: mparticle, uploadWaiter: uploadWaiter)
testCommerceEvent(mparticle: mparticle, uploadWaiter: uploadWaiter)
testRoktSelectPlacement(mparticle: mparticle, uploadWaiter: uploadWaiter)
testRoktSelectPlacement(mparticle: mparticle, uploadWaiter: uploadWaiter)
testGetUserAudiences(mparticle: mparticle)
testLogTimedEvent(mparticle: mparticle, uploadWaiter: uploadWaiter)
testLogError(mparticle: mparticle, uploadWaiter: uploadWaiter)
testLogException(mparticle: mparticle, uploadWaiter: uploadWaiter)
testSetUserAttributes(mparticle: mparticle, uploadWaiter: uploadWaiter)
testIncrementUserAttribute(mparticle: mparticle, uploadWaiter: uploadWaiter)
14 changes: 12 additions & 2 deletions IntegrationTests/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,17 @@ install_application() {

launch_application() {
echo "▶️ Launching application..."
LAUNCH_OUTPUT=$(xcrun simctl launch "$DEVICE_ID" "$BUNDLE_ID")

# Check that required environment variables are set
if [ -z "$MPARTICLE_API_KEY" ] || [ -z "$MPARTICLE_API_SECRET" ]; then
echo "❌ Error: MPARTICLE_API_KEY and MPARTICLE_API_SECRET environment variables must be set"
exit 1
fi

# Launch with environment variables using SIMCTL_CHILD_ prefix
LAUNCH_OUTPUT=$(SIMCTL_CHILD_MPARTICLE_API_KEY="$MPARTICLE_API_KEY" \
SIMCTL_CHILD_MPARTICLE_API_SECRET="$MPARTICLE_API_SECRET" \
xcrun simctl launch "$DEVICE_ID" "$BUNDLE_ID")
APP_PID=$(echo "$LAUNCH_OUTPUT" | awk -F': ' '{print $2}')

if [ -z "$APP_PID" ]; then
Expand All @@ -171,7 +181,7 @@ launch_application() {

wait_for_app_completion() {
echo "⏳ Waiting for app to complete execution..."
local MAX_WAIT=60
local MAX_WAIT=120
local WAIT_COUNT=0
while kill -0 "$APP_PID" 2>/dev/null; do
sleep 1
Expand Down
2 changes: 2 additions & 0 deletions IntegrationTests/transform_mapping_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
'dlc', # Device Locale
'dn', # Device Name
'dosv', # Device OS Version
'el', # Event Length (duration in milliseconds for timed events)
'en', # Event Number (position in session, e.g., 0, 1, 2...)
'est', # Event Start Time
'iba', # Instruction Base Address (memory address for errors)
'ict', # Init Config Time
'id', # ID (various message/event IDs)
'lud', # Last Update Date
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"audience_memberships":[{"audience_id":12345,"audience_name":"Test Audience 1"},{"audience_id":67890,"audience_name":"Test Audience 2"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"109b9248-c587-4f3e-8bcc-f2b294707b6c","ct":1764620922698,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=0b876fb7-59a2-4b88-94c7-0ad23ca58452","e":"2035-11-04T19:37:06.7933933Z"}},"das":"0b876fb7-59a2-4b88-94c7-0ad23ca58452"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"5943ab4c-eeab-48c6-9a8c-9432e7d4f6ae","ct":1764620932703,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=0b876fb7-59a2-4b88-94c7-0ad23ca58452","e":"2035-11-04T19:37:06.7933933Z"}},"das":"0b876fb7-59a2-4b88-94c7-0ad23ca58452"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"62c9bed1-e59c-488f-b30c-f06990223c73","ct":1764611788151,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=cf0a8016-279b-4933-88e5-0644e88ff82c","e":"2035-11-04T19:37:06.7933933Z"}},"das":"cf0a8016-279b-4933-88e5-0644e88ff82c"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"99323d4a-48fa-410f-91df-f8165d1be676","ct":1764613212857,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=3b8cc9d7-af48-43db-8e15-ebff948f6020","e":"2035-11-04T19:37:06.7933933Z"}},"das":"3b8cc9d7-af48-43db-8e15-ebff948f6020"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"9182e451-d96e-4e08-b350-6da24ab3f0e7","ct":1764616655403,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=140f02c6-7a15-4c14-92d0-d1dc1532e292","e":"2035-11-04T19:37:06.7933933Z"}},"das":"140f02c6-7a15-4c14-92d0-d1dc1532e292"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"dt":"rh","id":"283eb295-b5a3-42ea-8463-783756145b93","ct":1764605104156,"msgs":[],"ci":{"mpid":6504934091054997508,"ck":{"uid":{"c":"g=bc7ddd85-f6cd-4739-9852-faaa18237c16","e":"2035-11-04T19:37:06.7933933Z"}},"das":"bc7ddd85-f6cd-4739-9852-faaa18237c16"}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"dt":"ac","id":"03fcd379-b420-43a3-9004-c7bb821e0495","ct":1762457821105,"dbg":false,"cue":"appdefined","pmk":["mp_message","com.urbanairship.push.ALERT","alert","a","message","lp_message","gcm.notification.body"],"cnp":"appdefined","soc":0,"oo":false,"lsv":"6.13.0","rdlat":true,"inhd":false,"iasr":false,"wst":"D38F29DF","amw":90,"crml":1024000,"dur":false,"flags":{"AudienceAPI":"False"},"atos":0,"pio":30}
{"dt":"ac","id":"03fcd379-b420-43a3-9004-c7bb821e0495","ct":1762457821105,"dbg":false,"cue":"appdefined","pmk":["mp_message","com.urbanairship.push.ALERT","alert","a","message","lp_message","gcm.notification.body"],"cnp":"appdefined","soc":0,"oo":false,"lsv":"6.13.0","rdlat":true,"inhd":false,"iasr":false,"wst":"D38F29DF","amw":90,"crml":1024000,"dur":false,"flags":{"AudienceAPI":"True"},"atos":0,"pio":30}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"id": "1185200e-3f83-3ae5-8323-4810b47060cb",
"request": {
"method": "GET",
"urlPattern": "/v1/us1-[a-f0-9]+/audience\\?mpid=[0-9]+"
},
"response": {
"status": 200,
"bodyFileName": "body-get-user-audiences.json",
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Accept-Ranges": "bytes",
"X-Cache": "MISS, MISS",
"Server": "Kestrel",
"X-Origin-Name": "fastlyshield--shield_ssl_cache_iad_kjyo7100175_IAD",
"X-Served-By": "cache-iad-kjyo7100175-IAD, cache-lga21925-LGA",
"X-Cache-Hits": "0, 0",
"Date": "Mon, 01 Dec 2025 15:33:54 GMT",
"X-Timer": "S1764603234.125633,VS0,VE40",
"Via": "1.1 varnish, 1.1 varnish"
}
},
"uuid": "1185200e-3f83-3ae5-8323-4810b47060cb"
}
Loading