# Testing

Test your Aeropay integration before accepting live payments or payouts.

## Overview

Test the complete Aeropay journey, including component configuration, new and returning shoppers, one-time passcode (OTP) verification, bank linking, every supported intent, transaction failures, and backend verification.

Use `environment: .test` with sandbox credentials and test data. Don't use production Aeropay credentials, real bank accounts, or real shopper data during development.

Get approved test users, OTP behaviour, bank account scenarios, and expected provider responses from your PXP or Aeropay integration contact.

## Test environment

Initialise the SDK with a sandbox session and matching test transaction data:

```swift
let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionResult.session,
    transactionData: TransactionData(
        amount: Decimal(10.00),
        currency: "USD",
        entryType: .ecom,
        intent: TransactionIntentData(aeropay: .authorisation),
        merchantTransactionId: sessionResult.merchantTransactionId,
        merchantTransactionDate: { Date() }
    ),
    merchantShopperId: "test-shopper-1",
    ownerId: "MERCHANT_GROUP_1",
    onGetShopper: { TransactionShopper(
            id: "test-shopper-1",
            firstName: "Test",
            lastName: "Shopper",
            email: "test.shopper@example.com",
            phoneNumber: "+14155550123"
        )
    },
    analyticsEvent: { event in
        // Log event.eventName only. Don't log personal or bank data.
    }
)
```

Use environment settings consistently:

| SDK environment | Aeropay and Aerosync configuration | Purpose |
|  --- | --- | --- |
| `.test` | Sandbox configuration (`sandbox`) | Development, automated testing, and UAT |
| `.live` | Production configuration (`production`) | Live payments and payouts |


Confirm which credentials and test identities apply to each environment before testing.

## Add diagnostic callbacks

Use callbacks to trace the flow without logging personal or bank data:

```swift
let config = AeropayButtonComponentConfig()
config.onClick = {
    // Button tapped
}
config.onCustomValidation = { true }
config.onUserVerificationSuccess = { _ in
    // User verified
}
config.onCancel = {
    // Popup dismissed
}
config.onPreAuthorisation = { true }
config.onPostAuthorisation = { _ in
    // Log only that a result was received
}
config.onSubmitError = { error in
    guard let failed = error as? FailedSubmitResult else { return }
    // Log failed.errorCode and failed.correlationId only
}
config.onError = { error in
    // Log error.errorCode and a redacted message
}
```

Don't log shopper details, OTPs, Aeropay user IDs, bank account IDs, credentials, or complete provider responses.

## Configuration tests

Test each required configuration rule before testing the shopper flow:

| Scenario | Expected result |
|  --- | --- |
| Valid Aeropay-enabled session | The component is created and `buildContent()` renders the button. |
| Missing `externalMerchantId` | `create` throws `SDK0113`. |
| Missing `configurationId` | `create` throws `SDK0113`. |
| Missing Aeropay intent | `create` throws `SDK0115`. |
| Entry type other than `.ecom` | `create` throws `SDK0114`. |
| Currency other than `USD` | `create` throws `SDK0116`. |


`SDK0106` is the shared `intent type not supported for component` code. With the current public `AeropayIntentType` cases, typed Aeropay creation won't hit that path for an unsupported intent. Focus practical tests on `SDK0113`, `SDK0114`, `SDK0115`, and `SDK0116`.

For example, confirm that an unsupported currency fails during component creation:

```swift
do {
    let invalidCheckout = try createCheckoutWithCurrency("GBP")
    let config = AeropayButtonComponentConfig()
    config.onPreAuthorisation = { true }
    _ = try invalidCheckout.create(.aeropayButton, componentConfig: config)
} catch let error as BaseSdkException {
    // Expected: SDK0116
}
```

## Component tests

Verify basic button behaviour:

* The Aeropay presentation and default payment label render.
* The payout label renders when the intent is `.payout`.
* Custom `label` and `styles` values are applied for non-payout intents.
* Disabled and loading states remain readable.
* `disabled = true` on the config before creation, or `setDisabled(true)` after creation, prevents the flow from starting.
* `onCustomValidation` returning `false` keeps the popup closed after `onClick`.
* `hide()` and `show()` update visibility when you cast the result of `create` to `AeropayButtonComponent`.
* Before creating the Aeropay button again after a configuration change, call `pxpCheckout.unmount()` to clear the internal component registry, and drop or replace the `BaseComponent` / `AeropayButtonComponent` you keep in SwiftUI state (or initialise a new `PxpCheckout`). `create(.aeropayButton, …)` appends; it doesn't replace an existing instance or dismiss an open popup. Prefer a fresh checkout when transaction data, session, or shopper flow config changes between tests.


Test the button on iPhone and iPad layouts in portrait and landscape.

## New-shopper tests

### No prefilled data

Omit `onGetShopper` or return only a shopper ID. Confirm that:

1. The consumer data screen opens with empty fields.
2. All four fields are required.
3. Inline errors appear for empty or invalid values.
4. Valid data advances to OTP verification.
5. Successful OTP verification calls `onUserVerificationSuccess`.
6. The Aerosync bank-selection screen opens after verification.


### Partial prefilled data

Return some consumer fields:

```swift
onGetShopper: { TransactionShopper(
        id: "test-shopper-2",
        firstName: "Test",
        email: "test.shopper@example.com"
    )
}
```

Confirm that provided fields are prefilled and read-only unless listed in `editableFields`, while missing fields remain empty for the shopper to complete.

### Editable prefilled data

Make selected fields editable:

```swift
let config = AeropayButtonComponentConfig()
config.consumerDataCollectionConfig = ConsumerDataCollectionConfig(
    editableFields: [.email]
)
config.onPreAuthorisation = { true }
```

Confirm that only the configured fields can be changed and that edited values still receive validation.

### Skip consumer data collection

Return all four valid consumer fields and set `skipConsumerDataCollection` to `true`:

```swift
let config = AeropayButtonComponentConfig()
config.skipConsumerDataCollection = true
config.onPreAuthorisation = { true }
```

Confirm that the SDK creates the user in the background and opens OTP verification directly.

Also test that the data screen still appears when:

* A required field is missing or empty.
* `consumerDataCollectionConfig.editableFields` contains a value.


If a supplied field is invalid, confirm that `onError` receives `SDK1300` and the popup doesn't open.

## Consumer data tests

Test valid, missing, boundary, and invalid values:

| Field | Valid examples | Invalid or boundary examples |
|  --- | --- | --- |
| First name | `Test`, `María`, `Test User` | Empty, more than 100 characters, `Test-User`, `Test2` |
| Last name | `Shopper`, `García`, `Test Shopper` | Empty, more than 100 characters, `O'Neil`, `Shopper2` |
| Email | `test@example.com` | Empty, missing `@`, missing domain dot, whitespace, more than 128 characters |
| Phone number from `onGetShopper` | `+14155550123` | Empty when skipping, missing `+1`, too few or too many digits, formatting characters |
| Phone number entered in the form | `4155550123` | Empty, fewer or more than 10 digits |


The current name rules accept Unicode letters and spaces only. Test real customer-name patterns against this limitation before launch.

## OTP tests

Test the OTP screen with provider-approved UAT scenarios:

* Confirm that verification remains unavailable until all six-digit cells contain a value.
* Confirm that typed or pasted non-digit characters are filtered out.
* Test an incorrect six-digit code.
* Test a correct six-digit code.
* Test a network failure during verification or resend.
* Try to resend while the countdown is active.
* Resend successfully after the countdown.
* Confirm that provider verification failures call `onError` with `SDK1302`, including provider code `AP112` if your UAT setup returns it.


## Bank selection and Aerosync tests

Test bank account loading, selection, and linking:

| Scenario  | Expected result |
|  --- | --- |
| One or more linked accounts | Accounts load and the shopper can select an account. |
| No linked accounts | The component shows the empty state and offers bank linking for payment flows. |
| Successful bank linking | Aerosync completes, the account is linked, and the bank list refreshes. |
| Failed bank linking | `onError` receives an Aerosync or link-account failure code. |
| Payout with default settings | Link-bank is hidden. |
| Payout with `allowLinkBankOnPayout = true` | Link-bank is available. |


Before testing bank linking, register your callback scheme in `Info.plist` (`CFBundleURLTypes` / `CFBundleURLSchemes`). Sample apps use `pxpcheckout`. If you set `PXP_CALLBACK_SCHEME` in `sdk-config.json`, use the same value in `CFBundleURLSchemes`. Without this, bank linking can't complete in a real test build.

Confirm that your app handles the Aerosync callback URL built by the SDK, `{callbackScheme}://aerosync/callback`. The scheme comes from the SDK configuration and defaults to `pxpcheckout`, so the default callback is `pxpcheckout://aerosync/callback`.

## Returning-shopper tests

Pass a stored Aeropay user ID and confirm that:

1. Consumer data and OTP screens are skipped.
2. The Aerosync bank-selection screen opens after user lookup.
3. An inactive user calls `onError` with `SDK1307`.
4. A failed user lookup calls `onError` with `SDK1308`.
5. Your app can clear the stored ID and restart the new-shopper flow.


## Intent and transaction tests

Run end-to-end tests for each supported intent:

* `.authorisation`
* `.purchase`
* `.estimatedAuthorisation`
* `.payout`


For each intent, confirm:

* `onPreAuthorisation` returning `false` stops submission.
* Successful submission calls `onPostAuthorisation`.
* Failed submission calls `onSubmitError`.
* Your backend verification accepts only authoritative PXP transaction data.


During submission, the SDK may call `onGetShopper` again when building the transaction request. Return consistent shopper data or cache the first response in your test app.

## Cancellation tests

Confirm that dismissing the popup with the close control or swipe dismiss calls `onCancel`, and that a successful payment dismissal doesn't.