diff --git a/IntegrationTests/.gitignore b/IntegrationTests/.gitignore new file mode 100644 index 00000000..0cb725f1 --- /dev/null +++ b/IntegrationTests/.gitignore @@ -0,0 +1,17 @@ +# Temporary artifacts directory +temp_artifacts/ + +# Xcode +*.xcworkspace +!*.xcworkspace/contents.xcworkspacedata +!*.xcworkspace/xcshareddata/ +IntegrationTests.xcodeproj/ +Derived/ + +# Build artifacts +build/ +*.app + +# Tuist +.tuist/ + diff --git a/IntegrationTests/Project.swift b/IntegrationTests/Project.swift new file mode 100644 index 00000000..5d222946 --- /dev/null +++ b/IntegrationTests/Project.swift @@ -0,0 +1,25 @@ +import ProjectDescription + +let project = Project( + name: "IntegrationTests", + packages: [ + .local(path: "../") + ], + targets: [ + .target( + name: "IntegrationTests", + destinations: .iOS, + product: .app, + bundleId: "com.mparticle.IntegrationTests", + deploymentTargets: .iOS("14.0"), + sources: ["Sources/**"], + dependencies: [ + .package(product: "mParticle-Apple-SDK", type: .runtime) + ] + ) + ], + additionalFiles: [ + .glob(pattern: "wiremock-recordings/**/*.json"), + .glob(pattern: "*.sh") + ] +) diff --git a/IntegrationTests/README.md b/IntegrationTests/README.md new file mode 100644 index 00000000..9ee050e2 --- /dev/null +++ b/IntegrationTests/README.md @@ -0,0 +1,110 @@ +# Integration Tests + +Tools for recording mParticle Apple SDK API requests using WireMock for later use in integration testing. + +## Prerequisites + +Before getting started, install Tuist: + +```bash +brew install tuist +``` + +Then generate the Xcode project: + +```bash +cd IntegrationTests +tuist generate +``` + +This will create the `IntegrationTests.xcodeproj` and `IntegrationTests.xcworkspace` files and automatically open them in Xcode. + +If you need to edit the Tuist project configuration (`Project.swift`): + +```bash +tuist edit +``` + +This will open a temporary Xcode project for editing Tuist manifest files. + +If you encounter any issues with project generation, you can clean the Tuist cache first: + +```bash +tuist clean +tuist generate +``` + +## Overview + +This project provides tools for recording mParticle SDK API requests by: +- Generating a test iOS app using Tuist +- Linking directly to local SDK source code +- Running the app in iOS Simulator +- Recording all API traffic with WireMock for later use in testing + +The project uses Tuist with `.local(path: "../")` package reference, which allows Xcode to resolve the local SDK package and use source files directly, automatically picking up your latest code changes. + +## Available Scripts + +### `run_wiremock_recorder.sh` - Record API Requests for Testing + +Records all mParticle SDK API requests using WireMock for later use in integration testing. + +```bash +./run_wiremock_recorder.sh +``` + +**What it does:** +1. Generates Tuist project with local SDK sources +2. Builds the integration test application +3. Finds and resets iOS Simulators +4. Automatically selects available iPhone simulator (iPhone 17/16/15 priority) +5. Starts simulator +6. Installs test application +7. Starts WireMock in recording mode +8. Launches test application +9. Records all API traffic to mapping files +10. Waits for application completion +11. Stops WireMock and shows results + +**Recorded Files:** +- `wiremock-recordings/mappings/*.json` - API request/response mappings +- `wiremock-recordings/__files/*` - Response body files + +## Troubleshooting + +### Port Already in Use / No Recordings Created + +If you see "port already allocated" errors or if no API requests were recorded, it's likely that the ports (8080 and 443) where WireMock should be running are already occupied by another container or application. + +Check what's running on the ports: + +```bash +# Check running Docker containers +docker ps + +# Check what's using port 443 +lsof -i :443 + +# Check what's using port 8080 +lsof -i :8080 +``` + +Stop any conflicting Docker containers: + +```bash +docker stop +docker rm +``` + +If another application is using the ports, terminate it before running the script. + +## Development Workflow + +1. Make changes to SDK source code +2. Run `./run_wiremock_recorder.sh` +3. Script automatically uses your latest changes, runs the app, and records API traffic +4. Review recorded mappings in `wiremock-recordings/` +5. Commit mappings to document expected API behavior + +**Note:** No need to rebuild the SDK separately - the project links directly to source files and automatically picks up your changes! diff --git a/IntegrationTests/Sources/main.swift b/IntegrationTests/Sources/main.swift new file mode 100644 index 00000000..e6d62f3c --- /dev/null +++ b/IntegrationTests/Sources/main.swift @@ -0,0 +1,43 @@ +// +// main.swift +// IntegrationTests +// +// Created by Denis Chilik on 11/4/25. +// + +import Foundation +import mParticle_Apple_SDK + + +var options = MParticleOptions( + key: "", // Put your key + secret: "" // Put your secret +) + +var identityRequest = MPIdentityApiRequest.withEmptyUser() +identityRequest.email = "foo@example.com"; +identityRequest.customerId = "123456"; +options.identifyRequest = identityRequest; + +options.onIdentifyComplete = { apiResult, error in + if let apiResult { + apiResult.user.setUserAttribute("example attribute key", value: "example attribute value") + } +} +options.logLevel = .verbose + +var networkOptions = MPNetworkOptions() +networkOptions.configHost = "127.0.0.1"; // config2.mparticle.com +networkOptions.eventsHost = "127.0.0.1"; // nativesdks.mparticle.com +networkOptions.identityHost = "127.0.0.1"; // identity.mparticle.com +networkOptions.pinningDisabled = true; + +options.networkOptions = networkOptions; +let mparticle = MParticle.sharedInstance() +mparticle.start(with: options) + +sleep(1) + +mparticle.logEvent("Simple Event Name", eventType: .other, eventInfo: ["SimpleKey": "SimpleValue"]) + +sleep(7) diff --git a/IntegrationTests/Tuist.swift b/IntegrationTests/Tuist.swift new file mode 100644 index 00000000..ebf98941 --- /dev/null +++ b/IntegrationTests/Tuist.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let tuist = Tuist(project: .tuist()) \ No newline at end of file diff --git a/IntegrationTests/run_wiremock_recorder.sh b/IntegrationTests/run_wiremock_recorder.sh new file mode 100755 index 00000000..f824e7ef --- /dev/null +++ b/IntegrationTests/run_wiremock_recorder.sh @@ -0,0 +1,217 @@ +echo "๐Ÿ”„ Generating project with Tuist..." +tuist generate --no-open + +# === Configuration === +APP_NAME="IntegrationTests" +SCHEME="IntegrationTests" +BUNDLE_ID="com.mparticle.IntegrationTests" +CONFIGURATION="Debug" +DERIVED_DATA="$HOME/Library/Developer/Xcode/DerivedData" + +HTTP_PORT=${1:-8080} +HTTPS_PORT=${2:-443} +MAPPINGS_DIR=${3:-"./wiremock-recordings"} +TARGET_URL=${4:-"https://config2.mparticle.com"} +CONTAINER_NAME="wiremock-recorder" + +# Global variables +DEVICE_NAME="" +DEVICE_ID="" +APP_PATH="" +APP_PID="" + +# === Prepare local directory for mappings === +mkdir -p "${MAPPINGS_DIR}/mappings" +mkdir -p "${MAPPINGS_DIR}/__files" + +build_application() { + echo "๐Ÿ“ฆ Building application '$APP_NAME'..." + xcodebuild \ + -project IntegrationTests.xcodeproj \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "$DERIVED_DATA" \ + -quiet \ + build || { echo "โŒ Build error"; exit 1; } + echo "โœ… Build completed." +} + +reset_simulators() { + echo "๐Ÿงน Resetting simulators..." + xcrun simctl shutdown all || true + xcrun simctl erase all || true + killall Simulator || true + echo "โœ… Simulators cleaned." +} + +find_available_device() { + echo "๐Ÿ” Searching for available iPhone simulator..." + + # Get list of available iPhone devices (excluding unavailable ones) + AVAILABLE_DEVICES=$(xcrun simctl list devices iPhone | grep -v "unavailable" | grep "iPhone" | grep -v "==" | head -5) + + if [ -z "$AVAILABLE_DEVICES" ]; then + echo "โŒ No iPhone simulators found. Please install iPhone simulators in Xcode." + exit 1 + fi + + # Try to find iPhone 17, 16, 15, or any available iPhone + for device_pattern in "iPhone 17" "iPhone 16" "iPhone 15" "iPhone"; do + DEVICE_NAME=$(echo "$AVAILABLE_DEVICES" | grep "$device_pattern" | head -1 | sed 's/^[[:space:]]*//' | sed 's/ (.*//') + if [ -n "$DEVICE_NAME" ]; then + echo "โœ… Selected device: $DEVICE_NAME" + break + fi + done + + if [ -z "$DEVICE_NAME" ]; then + echo "โŒ No suitable iPhone simulator found" + exit 1 + fi +} + +find_device() { + echo "๐Ÿ” Finding simulator device '$DEVICE_NAME'..." + DEVICE_ID=$(xcrun simctl list devices | grep "$DEVICE_NAME" | grep -v "unavailable" | awk -F '[()]' '{print $2}' | head -1) + + if [ -z "$DEVICE_ID" ]; then + echo "โŒ Simulator '$DEVICE_NAME' not found. Check Xcode > Devices & Simulators." + exit 1 + fi + echo "โœ… Found device: $DEVICE_ID" +} + +start_simulator() { + echo "๐Ÿ“ฑ Starting simulator $DEVICE_NAME..." + xcrun simctl boot "$DEVICE_ID" || true + open -a Simulator + + echo "โณ Waiting for simulator to start..." + xcrun simctl bootstatus "$DEVICE_ID" -b + echo "โœ… Simulator started." +} + +install_application() { + echo "๐Ÿ“ฒ Installing '$APP_NAME'..." + xcrun simctl install "$DEVICE_ID" "$APP_PATH" + + echo "โณ Waiting for app installation to complete..." + local MAX_WAIT=30 + local WAIT_COUNT=0 + while ! xcrun simctl get_app_container "$DEVICE_ID" "$BUNDLE_ID" &>/dev/null; do + sleep 1 + WAIT_COUNT=$((WAIT_COUNT + 1)) + if [ $WAIT_COUNT -ge $MAX_WAIT ]; then + echo "โŒ App installation timed out after ${MAX_WAIT} seconds" + exit 1 + fi + done + echo "โœ… App installed successfully" +} + +launch_application() { + echo "โ–ถ๏ธ Launching application..." + LAUNCH_OUTPUT=$(xcrun simctl launch "$DEVICE_ID" "$BUNDLE_ID") + APP_PID=$(echo "$LAUNCH_OUTPUT" | awk -F': ' '{print $2}') + + if [ -z "$APP_PID" ]; then + echo "โŒ Failed to get app PID" + exit 1 + fi + + echo "โœ… Application '$APP_NAME' started with PID: $APP_PID" +} + +wait_for_app_completion() { + echo "โณ Waiting for app to complete execution..." + MAX_WAIT=60 + WAIT_COUNT=0 + while kill -0 "$APP_PID" 2>/dev/null; do + sleep 1 + WAIT_COUNT=$((WAIT_COUNT + 1)) + if [ $WAIT_COUNT -ge $MAX_WAIT ]; then + echo "โš ๏ธ App still running after ${MAX_WAIT} seconds, proceeding anyway..." + break + fi + done + if [ $WAIT_COUNT -lt $MAX_WAIT ]; then + echo "โœ… App execution completed" + fi +} + +find_app_path() { + echo "๐Ÿ” Finding application path..." + APP_PATH=$(find "$DERIVED_DATA" -type d -name "${APP_NAME}.app" | head -1) + + if [ -z "$APP_PATH" ]; then + echo "โŒ Application not found in DerivedData" + exit 1 + fi + echo "โœ… Found app at: $APP_PATH" +} + +start_wiremock() { + stop_wiremock + + docker run -d --name ${CONTAINER_NAME} \ + -p ${HTTP_PORT}:8080 \ + -p ${HTTPS_PORT}:8443 \ + -v "$(pwd)/${MAPPINGS_DIR}":/home/wiremock \ + wiremock/wiremock:3.9.1 \ + --enable-browser-proxying \ + --preserve-host-header \ + --record-mappings \ + --proxy-all="${TARGET_URL}" \ + --https-port 8443 +} + +wait_for_wiremock() { + echo "โณ Waiting for WireMock to start..." + MAX_RETRIES=30 + RETRY_COUNT=0 + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if curl -k -s -o /dev/null -w "%{http_code}" https://localhost:${HTTPS_PORT}/__admin/mappings | grep -q "200"; then + echo "โœ… WireMock is ready!" + break + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Waiting... ($RETRY_COUNT/$MAX_RETRIES)" + sleep 1 + done + + if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo "โŒ WireMock failed to start within ${MAX_RETRIES} seconds" + exit 1 + fi + + echo "" + echo "๐Ÿ“ WireMock is running and recording traffic to: ${MAPPINGS_DIR}" + echo "๐Ÿ”— Admin UI: http://localhost:${HTTP_PORT}/__admin" + echo "๐Ÿ”— HTTPS Proxy: https://localhost:${HTTPS_PORT}" + echo "" + echo "Press Ctrl+C to stop WireMock and exit..." + echo "" +} + +stop_wiremock() { + echo "" + echo "๐Ÿ›‘ Stopping WireMock container..." + docker stop ${CONTAINER_NAME} 2>/dev/null || true + docker rm ${CONTAINER_NAME} 2>/dev/null || true + echo "โœ… WireMock stopped" +} + +trap stop_wiremock EXIT INT TERM + +build_application +find_app_path +reset_simulators +find_available_device +find_device +start_wiremock +wait_for_wiremock +start_simulator +install_application +launch_application +wait_for_app_completion diff --git a/IntegrationTests/wiremock-recordings/mappings/proxy-events.json b/IntegrationTests/wiremock-recordings/mappings/proxy-events.json new file mode 100644 index 00000000..f15500c3 --- /dev/null +++ b/IntegrationTests/wiremock-recordings/mappings/proxy-events.json @@ -0,0 +1,9 @@ +{ + "priority": 1, + "request": { + "urlPathPattern": "/v2/events" + }, + "response": { + "proxyBaseUrl": "https://nativesdks.mparticle.com" + } +} diff --git a/IntegrationTests/wiremock-recordings/mappings/proxy-identify.json b/IntegrationTests/wiremock-recordings/mappings/proxy-identify.json new file mode 100644 index 00000000..f6633297 --- /dev/null +++ b/IntegrationTests/wiremock-recordings/mappings/proxy-identify.json @@ -0,0 +1,9 @@ +{ + "priority": 1, + "request": { + "urlPathPattern": "/v1/identify" + }, + "response": { + "proxyBaseUrl": "https://identity.mparticle.com" + } +}