Skip to content
Closed
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
8 changes: 4 additions & 4 deletions .github/scripts/integration-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ cat <<EOT > ./IntegrationTester/Env.plist
</plist>
EOT

# Find iPhone 16 Pro device ID from the latest iOS version
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 16 Pro" | tail -1 | grep -oE '[A-F0-9-]{36}')
# Find iPhone 17 Pro device ID from the latest iOS version
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 17 Pro" | tail -1 | grep -oE '[A-F0-9-]{36}')

if [ -z "$DEVICE_ID" ]; then
echo "Error: No iPhone 16 Pro simulator found"
echo "Error: No iPhone 17 Pro simulator found"
xcrun simctl list devices available
exit 1
fi
Expand All @@ -40,5 +40,5 @@ xcodebuild clean test \
-project ./IntegrationTester/IntegrationTester.xcodeproj \
-scheme IntegrationTester \
-configuration Debug \
-destination "platform=iOS Simulator,id=$DEVICE_ID" \
-destination "platform=iOS Simulator,arch=arm64,id=$DEVICE_ID" \
| xcpretty
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
test:
environment: PR
name: Testing BasisTheoryElements Package
runs-on: macOS-latest
runs-on: macos-latest-xlarge
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,51 @@ final public class BasisTheoryElements {
}
}

public static func updateToken(id: String, body: UpdateToken, apiKey: String? = nil, completion: @escaping ((_ data: CreateTokenResponse?, _ error: Error?) -> Void)) -> Void {
let endpoint = "PATCH /tokens/\(id)"
let btTraceId = UUID().uuidString
logBeginningOfApiCall(endpoint: endpoint, btTraceId: btTraceId)

var mutableBody = body
var mutableData = body.data
do {
try replaceElementRefs(body: &mutableData, endpoint: endpoint, btTraceId: btTraceId)
} catch {
completion(nil, TokenizingError.invalidInput)
return
}

mutableBody.data = mutableData
let updateTokenRequest = mutableBody.toUpdateTokenRequest()

var headers = getBasisTheoryHeaders(apiKey: getApiKey(apiKey), btTraceId: btTraceId)
headers["Content-Type"] = "application/merge-patch+json"
let url = "\(basePath)/tokens/\(id)"

HttpClientHelpers.executeTypedRequest(
method: .patch,
url: url,
headers: headers,
body: updateTokenRequest
) { (result: CreateTokenResponse?, error: Error?) in
if let error = error {
TelemetryLogging.error("Unsuccessful API response", error: error, attributes: [
"endpoint": endpoint,
"BT-TRACE-ID": btTraceId,
"apiSuccess": false
])
completion(nil, error)
} else {
TelemetryLogging.info("Successful API response", attributes: [
"endpoint": endpoint,
"BT-TRACE-ID": btTraceId,
"apiSuccess": true
])
completion(result, nil)
}
}
}

public static func createTokenIntent(
request: CreateTokenIntentRequest, apiKey: String? = nil,
completion: @escaping ((_ data: TokenIntent?, _ error: Error?) -> Void)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// UpdateToken.swift
//
//
// Created by Cascade on 02/11/26.
//

import AnyCodable

public struct UpdateToken {
public var data: [String: Any]
public var privacy: Privacy?
public var metadata: [String: String]?
public var searchIndexes: [String]?
public var fingerprintExpression: String?
public var mask: String?
public var deduplicateToken: Bool?
public var expiresAt: String?
public var containers: [String]?

public init(data: [String: Any], privacy: Privacy? = nil, metadata: [String: String]? = nil, searchIndexes: [String]? = nil, fingerprintExpression: String? = nil, mask: String? = nil, deduplicateToken: Bool? = nil, expiresAt: String? = nil, containers: [String]? = nil) {
self.data = data
self.privacy = privacy
self.metadata = metadata
self.searchIndexes = searchIndexes
self.fingerprintExpression = fingerprintExpression
self.mask = mask
self.deduplicateToken = deduplicateToken
self.expiresAt = expiresAt
self.containers = containers
}

func toUpdateTokenRequest() -> UpdateTokenRequest {
UpdateTokenRequest(
data: AnyCodable(self.data),
privacy: self.privacy,
metadata: self.metadata,
searchIndexes: self.searchIndexes,
fingerprintExpression: self.fingerprintExpression,
mask: AnyCodable(self.mask),
deduplicateToken: self.deduplicateToken,
expiresAt: self.expiresAt,
containers: self.containers
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation
import AnyCodable

internal struct UpdateTokenRequest: Codable {
var data: AnyCodable?
var privacy: Privacy?
var metadata: [String: String]?
var searchIndexes: [String]?
var fingerprintExpression: String?
var mask: AnyCodable?
var deduplicateToken: Bool?
var expiresAt: String?
var containers: [String]?
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
0A5844202E32C81800D61EAF /* TokenIntentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A58441D2E32C81800D61EAF /* TokenIntentViewController.swift */; };
0AB437902A4C9FB4000099CD /* HttpClientViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4378F2A4C9FB4000099CD /* HttpClientViewController.swift */; };
0AD41CA82E32C8D4002070F8 /* TokenIntentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD41CA72E32C8D4002070F8 /* TokenIntentTests.swift */; };
CA5CAD022F0C000000000001 /* UpdateTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5CAD012F0C000000000001 /* UpdateTokenTests.swift */; };
428CB1062937C53000C8220E /* CardExpirationDateUITextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428CB1052937C53000C8220E /* CardExpirationDateUITextFieldTests.swift */; };
428CB10A293E398B00C8220E /* CardNumberUITextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428CB109293E398B00C8220E /* CardNumberUITextFieldTests.swift */; };
5EC845072EEB95F100B10269 /* EnvironmentConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC844F82EEA49ED00B10269 /* EnvironmentConfigurationTests.swift */; };
Expand Down Expand Up @@ -87,6 +88,7 @@
0A58441D2E32C81800D61EAF /* TokenIntentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenIntentViewController.swift; sourceTree = "<group>"; };
0AB4378F2A4C9FB4000099CD /* HttpClientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpClientViewController.swift; sourceTree = "<group>"; };
0AD41CA72E32C8D4002070F8 /* TokenIntentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenIntentTests.swift; sourceTree = "<group>"; };
CA5CAD012F0C000000000001 /* UpdateTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTokenTests.swift; sourceTree = "<group>"; };
428CB1052937C53000C8220E /* CardExpirationDateUITextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardExpirationDateUITextFieldTests.swift; sourceTree = "<group>"; };
428CB109293E398B00C8220E /* CardNumberUITextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardNumberUITextFieldTests.swift; sourceTree = "<group>"; };
5EC844F82EEA49ED00B10269 /* EnvironmentConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentConfigurationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -220,6 +222,7 @@
023FB3372EC361D800CD88DF /* CardBrandSelectorUIButtonTests.swift */,
023FB3382EC361D800CD88DF /* CobadgeIntegrationTests.swift */,
0AD41CA72E32C8D4002070F8 /* TokenIntentTests.swift */,
CA5CAD012F0C000000000001 /* UpdateTokenTests.swift */,
646506012DC9490300660095 /* EncryptTokenTests.swift */,
929E30C22948D0F900A01A56 /* TokenizeAndCreateTokenServiceTests.swift */,
9253E8E1291EB66E00FDF1A9 /* ElementUITextFieldTests.swift */,
Expand Down Expand Up @@ -473,6 +476,7 @@
929E30C32948D0F900A01A56 /* TokenizeAndCreateTokenServiceTests.swift in Sources */,
92B7DCAF2939523C000DBB8A /* CardBrandTests.swift in Sources */,
0AD41CA82E32C8D4002070F8 /* TokenIntentTests.swift in Sources */,
CA5CAD022F0C000000000001 /* UpdateTokenTests.swift in Sources */,
023FB3392EC361D800CD88DF /* CardBrandSelectorUIButtonTests.swift in Sources */,
023FB33A2EC361D800CD88DF /* CobadgeIntegrationTests.swift in Sources */,
428CB1062937C53000C8220E /* CardExpirationDateUITextFieldTests.swift in Sources */,
Expand Down
85 changes: 85 additions & 0 deletions IntegrationTester/UnitTests/TextElementUITextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,89 @@ final class TextElementUITextFieldTests: XCTestCase {

waitForExpectations(timeout: 3)
}

func testTextSetterTriggersChangeEventWithValidation() throws {
let textField = TextElementUITextField()
let customRegex = try! NSRegularExpression(pattern: "^\\d{16}$")
try! textField.setConfig(options: TextElementOptions(validation: customRegex))

let changeExpectation = self.expectation(description: "text setter triggers change event")
var cancellables = Set<AnyCancellable>()

textField.subject.sink { completion in
print(completion)
} receiveValue: { message in
XCTAssertEqual(message.type, "textChange")
XCTAssertEqual(message.complete, true)
XCTAssertEqual(message.valid, true)
XCTAssertEqual(message.empty, false)

// assert metadata updated
XCTAssertEqual(textField.metadata.complete, message.complete)
XCTAssertEqual(textField.metadata.valid, message.valid)
XCTAssertEqual(textField.metadata.empty, message.empty)

changeExpectation.fulfill()
}.store(in: &cancellables)

textField.text = "4242424242424242"

waitForExpectations(timeout: 1, handler: nil)
}

func testTextSetterTriggersChangeEventWithInvalidValue() throws {
let textField = TextElementUITextField()
let customRegex = try! NSRegularExpression(pattern: "^\\d{16}$")
try! textField.setConfig(options: TextElementOptions(validation: customRegex))

let changeExpectation = self.expectation(description: "text setter triggers change event with invalid value")
var cancellables = Set<AnyCancellable>()

textField.subject.sink { completion in
print(completion)
} receiveValue: { message in
XCTAssertEqual(message.type, "textChange")
XCTAssertEqual(message.complete, false)
XCTAssertEqual(message.valid, false)
XCTAssertEqual(message.empty, false)

// assert metadata updated
XCTAssertEqual(textField.metadata.complete, message.complete)
XCTAssertEqual(textField.metadata.valid, message.valid)
XCTAssertEqual(textField.metadata.empty, message.empty)

changeExpectation.fulfill()
}.store(in: &cancellables)

textField.text = "4242"

waitForExpectations(timeout: 1, handler: nil)
}

func testTextSetterTriggersChangeEventWithEmptyValue() throws {
let textField = TextElementUITextField()

let changeExpectation = self.expectation(description: "text setter triggers change event with empty value")
var cancellables = Set<AnyCancellable>()

textField.subject.sink { completion in
print(completion)
} receiveValue: { message in
XCTAssertEqual(message.type, "textChange")
XCTAssertEqual(message.complete, true)
XCTAssertEqual(message.valid, true)
XCTAssertEqual(message.empty, true)

// assert metadata updated
XCTAssertEqual(textField.metadata.complete, message.complete)
XCTAssertEqual(textField.metadata.valid, message.valid)
XCTAssertEqual(textField.metadata.empty, message.empty)

changeExpectation.fulfill()
}.store(in: &cancellables)

textField.text = ""

waitForExpectations(timeout: 1, handler: nil)
}
}
98 changes: 98 additions & 0 deletions IntegrationTester/UnitTests/UpdateTokenTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// UpdateTokenTests.swift
// IntegrationTesterTests
//

import XCTest
import BasisTheoryElements

final class UpdateTokenTests: XCTestCase {
private final var TIMEOUT_EXPECTATION = 5.0

override func setUpWithError() throws {
BasisTheoryElements.basePath = "https://api.flock-dev.com"
}

override func tearDownWithError() throws {}

func testUpdateTokenReturnsErrorFromApplicationCheck() throws {
let body = UpdateToken(data: [
"cvc": "123"
])

let updateExpectation = self.expectation(description: "Update token")
BasisTheoryElements.updateToken(id: "invalid-token-id", body: body, apiKey: "bad api key") { data, error in
XCTAssertNil(data)
XCTAssertNotNil(error)

updateExpectation.fulfill()
}

waitForExpectations(timeout: TIMEOUT_EXPECTATION)
}

func testUpdateTokenWithElementRef() throws {
let btApiKey = Configuration.getConfiguration().btApiKey!

// First create a token to update
let createExpectation = self.expectation(description: "Create token")
var tokenId: String?

let createBody = CreateToken(type: "card", data: [
"number": "4242424242424242",
"expiration_month": 12,
"expiration_year": 2026,
"cvc": "123"
])

BasisTheoryElements.createToken(body: createBody, apiKey: btApiKey) { data, error in
XCTAssertNil(error)
XCTAssertNotNil(data?.id)
tokenId = data?.id
createExpectation.fulfill()
}

waitForExpectations(timeout: TIMEOUT_EXPECTATION)

guard let id = tokenId else {
XCTFail("Failed to create token for update test")
return
}

// Now update the token with new CVC
let updateExpectation = self.expectation(description: "Update token")

let updateBody = UpdateToken(data: [
"cvc": "456"
])

BasisTheoryElements.updateToken(id: id, body: updateBody, apiKey: btApiKey) { data, error in
XCTAssertNil(error)
XCTAssertNotNil(data)
XCTAssertEqual(data?.id, id)

updateExpectation.fulfill()
}

waitForExpectations(timeout: TIMEOUT_EXPECTATION)
}

func testUpdateTokenWithInvalidId() throws {
let btApiKey = Configuration.getConfiguration().btApiKey!

let updateExpectation = self.expectation(description: "Update token with invalid id")

let updateBody = UpdateToken(data: [
"cvc": "456"
])

BasisTheoryElements.updateToken(id: "nonexistent-token-id", body: updateBody, apiKey: btApiKey) { data, error in
XCTAssertNil(data)
XCTAssertNotNil(error)

updateExpectation.fulfill()
}

waitForExpectations(timeout: TIMEOUT_EXPECTATION)
}
}
Loading