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
4 changes: 2 additions & 2 deletions .github/workflows/browserstack-prepare-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ on:
description: 'Keychain password'
required: true

# 1. Find the 'com.pingidentity.PingTestHost' provisioning profile (~/Library/MobileDevice/Provisioning\ Profiles)
# 1. Find the 'com.pingidentity.PingTestHost' provisioning profile (~/Library/MobileDevice/Provisioning\ Profiles) Xcode 16+ (~/Library/Developer/Xcode/UserData/Provisioning\ Profiles)
# 2. Rename the file to `provisioning_profile.mobileprovision`
# 3. Zip the file: `zip provisioning_profile.mobileprovision.zip provisioning_profile.mobileprovision`
# 4. Convert the file to Base64 string: `base64 -i provisioning_profile.mobileprovision.zip | pbcopy`
# 5. Update the value of the BUILD_PROVISION_PROFILE_ZIP_BASE64 action secret with the content of the clipboard
# 5. Update the value of the BUILD_PROVISION_PROFILE_ZIP_BASE64 action secret with the content of the clipboard, make sure extra new line is deleted at the end
BUILD_PROVISION_PROFILE:
description: 'Apple build provisioning profile'
required: true
Expand Down
24 changes: 0 additions & 24 deletions Binding/PingBindingTests/PingBindingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -469,30 +469,6 @@ final class PingBindingTests: XCTestCase {
}
}

func testUserKeysStorage_ConcurrentAccess() async {
// Test concurrent read/write operations
let bindCallback1 = DeviceBindingCallback()
bindCallback1.userId = "concurrent1"

let bindCallback2 = DeviceBindingCallback()
bindCallback2.userId = "concurrent2"

do {
// Execute bindings concurrently
async let bind1 = Binding.bind(callback: bindCallback1, journey: nil)
async let bind2 = Binding.bind(callback: bindCallback2, journey: nil)

let (_, _) = try await (bind1, bind2)

XCTFail("testUserKeysStorage_ConcurrentAccess Expected to fail")

} catch {
// Cleanup
try? await userKeyStorage.deleteByUserId("concurrent1")
try? await userKeyStorage.deleteByUserId("concurrent2")
}
}

// MARK: - Callback Tests

func testDeviceBindingCallback_InitValue() {
Expand Down
34 changes: 23 additions & 11 deletions Davinci/DavinciTests/CollectorRegistryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// CollectorRegistryTests.swift
// DavinciTests
//
// Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved.
// Copyright (c) 2024 - 2026 Ping Identity Corporation. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand All @@ -16,9 +16,23 @@ import PingDavinciPlugin

@MainActor
final class CollectorRegistryTests: XCTestCase {


override func setUp() async throws {
try await super.setUp()
await CollectorFactory.shared.reset()
}

override func tearDown() async throws {
await CollectorFactory.shared.reset()
try await super.tearDown()
}

func testShouldRegisterCollector() async {
let davinci = DaVinci.createDaVinci()

// Give plugin registration a brief moment to complete to avoid race
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s

let jsonArray: [[String: Any]] = [
["type": "TEXT"],
["type": "PASSWORD"],
Expand All @@ -32,10 +46,10 @@ final class CollectorRegistryTests: XCTestCase {
["inputType": "MULTI_SELECT"],
["inputType": "MULTI_SELECT"],
]

let collectors = await CollectorFactory.shared.collector(daVinci: davinci, from: jsonArray)
XCTAssertEqual(collectors.count, 11)
if collectors.count > 0 {
if collectors.count == 11 {
XCTAssertTrue(collectors[0] is TextCollector)
XCTAssertTrue(collectors[1] is PasswordCollector)
XCTAssertTrue(collectors[2] is SubmitCollector)
Expand All @@ -48,23 +62,21 @@ final class CollectorRegistryTests: XCTestCase {
XCTAssertTrue(collectors[9] is MultiSelectCollector)
XCTAssertTrue(collectors[10] is MultiSelectCollector)
}

await CollectorFactory.shared.reset()
}
func testShouldIgnoreUnknownCollector() async {

func testShouldIgnoreUnknownCollector() async {
let davinci = DaVinci.createDaVinci()
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s

let jsonArray: [[String: Any]] = [
["type": "TEXT"],
["type": "PASSWORD"],
["type": "SUBMIT_BUTTON"],
["inputType": "ACTION"],
["type": "UNKNOWN"]
]

let collectors = await CollectorFactory.shared.collector(daVinci: davinci, from: jsonArray)
XCTAssertEqual(collectors.count, 4)

await CollectorFactory.shared.reset()
}
}
130 changes: 65 additions & 65 deletions Davinci/DavinciTests/PasswordCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,74 +59,74 @@ final class PasswordCollectorTests: XCTestCase {
XCTAssertEqual(collector.validate(), [])
}

// TODO: Reinclude PasswordPolicy test
func testAddsInvalidLengthErrorWhenValueTooShort() {
let input: [String: Any] = [
"passwordPolicy": [
"length": [
"min": 8,
"max": 20
]
]
]

let collector = PasswordCollector(with: [:])
collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])

collector.value = "Short1@"

XCTAssertEqual(collector.validate(), [.invalidLength(min: 8, max: 20)])
}
// TODO: Reinclude when multiple password validation errors are supported
// func testAddsInvalidLengthErrorWhenValueTooShort() {
// let input: [String: Any] = [
// "passwordPolicy": [
// "length": [
// "min": 8,
// "max": 20
// ]
// ]
// ]
//
// let collector = PasswordCollector(with: [:])
// collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])
//
// collector.value = "Short1@"
//
// XCTAssertEqual(collector.validate(), [.invalidLength(min: 8, max: 20)])
// }

// TODO: Reinclude PasswordPolicy test
func testAddsUniqueCharacterErrorWhenNotEnoughUniqueCharacters() {
let input: [String: Any] = [
"passwordPolicy": [
"minUniqueCharacters": 5
]
]

let collector = PasswordCollector(with: [:])
collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])

collector.value = "aaa111@@@"

XCTAssertEqual(collector.validate(), [.uniqueCharacter(min: 5)])
}
// TODO: Reinclude when multiple password validation errors are supported
// func testAddsUniqueCharacterErrorWhenNotEnoughUniqueCharacters() {
// let input: [String: Any] = [
// "passwordPolicy": [
// "minUniqueCharacters": 5
// ]
// ]
//
// let collector = PasswordCollector(with: [:])
// collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])
//
// collector.value = "aaa111@@@"
//
// XCTAssertEqual(collector.validate(), [.uniqueCharacter(min: 5)])
// }

// TODO: Reinclude PasswordPolicy test
func testAddsMaxRepeatErrorWhenTooManyRepeatedCharacters() {
let input: [String: Any] = [
"passwordPolicy": [
"maxRepeatedCharacters": 2
]
]

let collector = PasswordCollector(with: [:])
collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])

collector.value = "aaabbbccc"

XCTAssertEqual(collector.validate(), [.maxRepeat(max: 2)])
}
// TODO: Reinclude when multiple password validation errors are supported
// func testAddsMaxRepeatErrorWhenTooManyRepeatedCharacters() {
// let input: [String: Any] = [
// "passwordPolicy": [
// "maxRepeatedCharacters": 2
// ]
// ]
//
// let collector = PasswordCollector(with: [:])
// collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])
//
// collector.value = "aaabbbccc"
//
// XCTAssertEqual(collector.validate(), [.maxRepeat(max: 2)])
// }

// TODO: Reinclude PasswordPolicy test
func testAddsMinCharactersErrorWhenNotEnoughDigits() {
let input: [String: Any] = [
"passwordPolicy": [
"minCharacters": [
"0123456789": 2
]
]
]

let collector = PasswordCollector(with: [:])
collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])

collector.value = "Password@1"

XCTAssertEqual(collector.validate(), [.minCharacters(character: "0123456789", min: 2)])
}
// TODO: Reinclude when multiple password validation errors are supported
// func testAddsMinCharactersErrorWhenNotEnoughDigits() {
// let input: [String: Any] = [
// "passwordPolicy": [
// "minCharacters": [
// "0123456789": 2
// ]
// ]
// ]
//
// let collector = PasswordCollector(with: [:])
// collector.continueNode = MockContinueNode(context: FlowContext(flowContext: SharedContext()), workflow: Workflow(config: WorkflowConfig()), input: input, actions: [])
//
// collector.value = "Password@1"
//
// XCTAssertEqual(collector.validate(), [.minCharacters(character: "0123456789", min: 2)])
// }

func testAddsMinCharactersErrorWhenEnoughSpecialCharacters() {
let input: [String: Any] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// DaVinciIntegrationTests.swift
// DavinciTests
//
// Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved.
// Copyright (c) 2024 - 2026 Ping Identity Corporation. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -893,6 +893,9 @@ class DaVinciIntegrationTests: DaVinciBaseTests, @unchecked Sendable {
// Verify the DaVinci instance was created successfully
XCTAssertNotNil(customDaVinci)

// call start so that module confis initialize is called
_ = await customDaVinci.start()

// Verify cookie storage is properly set
let cookieStorage = customDaVinci.sharedContext.get(key: SharedContext.Keys.cookieStorage) as? StorageDelegate<[CustomHTTPCookie]>
XCTAssertNotNil(cookieStorage, "Cookie storage should be set in shared context")
Expand Down Expand Up @@ -945,6 +948,10 @@ class DaVinciIntegrationTests: DaVinciBaseTests, @unchecked Sendable {
XCTAssertNotNil(standardDaVinci)
XCTAssertNotNil(transactionDaVinci)

// call start so that module confis initialize is called
_ = await standardDaVinci.start()
_ = await transactionDaVinci.start()

// Verify they have different storage configurations
let standardCookieStorage = standardDaVinci.sharedContext.get(key: SharedContext.Keys.cookieStorage) as? StorageDelegate<[CustomHTTPCookie]>
let transactionCookieStorage = transactionDaVinci.sharedContext.get(key: SharedContext.Keys.cookieStorage) as? StorageDelegate<[CustomHTTPCookie]>
Expand Down Expand Up @@ -1119,6 +1126,9 @@ class DaVinciIntegrationTests: DaVinciBaseTests, @unchecked Sendable {
}
}

// call start so that module confis initialize is called
_ = await testDaVinci.start()

// Initially should have no cookies
let initialHasCookies = await testDaVinci.hasCookies()
XCTAssertFalse(initialHasCookies, "Should not have cookies initially")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FormFieldValidationTests.swift
// DavinciTests
//
// Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
// Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -140,22 +140,24 @@ class FormFieldValidationTests: DaVinciBaseTests, @unchecked Sendable {
// Validate should return list of all the failing password policy items
var passwordValidationResult = password.validate()

XCTAssertEqual(7, passwordValidationResult.count)
XCTAssertEqual(1, passwordValidationResult.count) //TODO: change to 7 when all validations are reenabled
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("This field cannot be empty."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input length must be between 8 and 255 characters."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must contain at least 5 unique characters."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'ABCDEFGHIJKLMNOPQRSTUVWXYZ\'."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'abcdefghijklmnopqrstuvwxyz\'."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'~!@#$%^&*()-_=+[]{}|;:,.<>/?\'."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'0123456789\'."))
// TODO: Reenable these assertions when multiple password validation errors are supported
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input length must be between 8 and 255 characters."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must contain at least 5 unique characters."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'ABCDEFGHIJKLMNOPQRSTUVWXYZ\'."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'abcdefghijklmnopqrstuvwxyz\'."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'~!@#$%^&*()-_=+[]{}|;:,.<>/?\'."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'0123456789\'."))

// Set password that meets some of the policy requirements
password.value = "password123"
passwordValidationResult = password.validate()

XCTAssertEqual(2, passwordValidationResult.count)
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'ABCDEFGHIJKLMNOPQRSTUVWXYZ\'."))
XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'~!@#$%^&*()-_=+[]{}|;:,.<>/?\'."))
XCTAssertEqual(0, passwordValidationResult.count) // TODO: change to 2 when all validations are reenabled
// TODO: Reenable these assertions when multiple password validation errors are supported
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'ABCDEFGHIJKLMNOPQRSTUVWXYZ\'."))
// XCTAssert(passwordValidationResult.map { $0.errorMessage }.contains("The input must include at least 1 character(s) from this set: \'~!@#$%^&*()-_=+[]{}|;:,.<>/?\'."))

// Set password that meets all of the policy requirements
password.value = "Password123!"
Expand Down
Loading
Loading