Skip to content

Commit 703e771

Browse files
authored
chore: kickoff release
2 parents ee6b2fa + 5859f19 commit 703e771

12 files changed

Lines changed: 331 additions & 152 deletions

File tree

.github/workflows/stress_test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
scheme: AuthStressTests
6969
destination: ${{ env.DESTINATION }}
7070
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
71+
other_flags: -test-iterations 3 -retry-tests-on-failure -parallel-testing-enabled NO -test-repetition-relaunch-enabled YES
7172

7273
geo-stress-test:
7374
needs: prepare-for-test
@@ -99,6 +100,7 @@ jobs:
99100
scheme: GeoStressTests
100101
destination: ${{ env.DESTINATION }}
101102
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
103+
other_flags: -test-iterations 3 -retry-tests-on-failure -parallel-testing-enabled NO -test-repetition-relaunch-enabled YES
102104

103105
storage-stress-test:
104106
needs: prepare-for-test
@@ -130,6 +132,7 @@ jobs:
130132
scheme: StorageStressTests
131133
destination: ${{ env.DESTINATION }}
132134
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
135+
other_flags: -test-iterations 3 -retry-tests-on-failure -parallel-testing-enabled NO -test-repetition-relaunch-enabled YES
133136

134137
datastore-stress-test:
135138
needs: prepare-for-test
@@ -161,6 +164,7 @@ jobs:
161164
scheme: DatastoreStressTests
162165
destination: ${{ env.DESTINATION }}
163166
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
167+
other_flags: -test-iterations 3 -retry-tests-on-failure -parallel-testing-enabled NO -test-repetition-relaunch-enabled YES
164168

165169
graphql-api-stress-test:
166170
needs: prepare-for-test
@@ -191,4 +195,5 @@ jobs:
191195
project_path: ./AmplifyPlugins/API/Tests/APIHostApp
192196
scheme: GraphQLAPIStressTests
193197
destination: ${{ env.DESTINATION }}
194-
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
198+
xcode_path: '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'
199+
other_flags: -test-iterations 3 -retry-tests-on-failure -parallel-testing-enabled NO -test-repetition-relaunch-enabled YES

AmplifyPlugins/API/Tests/APIHostApp/GraphQLAPIStressTests/GraphQLAPIStressTests.swift

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,10 @@ final class APIStressTests: XCTestCase {
4141
}
4242

4343
override func setUp() async throws {
44-
await Amplify.reset()
45-
Amplify.Logging.logLevel = .verbose
46-
let plugin = AWSAPIPlugin(modelRegistration: TestModelRegistration())
47-
4844
do {
45+
Amplify.Logging.logLevel = .verbose
46+
let plugin = AWSAPIPlugin(modelRegistration: TestModelRegistration())
4947
try Amplify.add(plugin: plugin)
50-
5148
let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration(
5249
forResource: Self.amplifyConfiguration)
5350
try Amplify.configure(amplifyConfig)
@@ -58,6 +55,7 @@ final class APIStressTests: XCTestCase {
5855

5956
override func tearDown() async throws {
6057
await Amplify.reset()
58+
try await Task.sleep(seconds: 1)
6159
}
6260

6361
// MARK: - Stress tests
@@ -75,41 +73,37 @@ final class APIStressTests: XCTestCase {
7573
let progressInvoked = expectation(description: "progress invoked")
7674
progressInvoked.expectedFulfillmentCount = concurrencyLimit
7775

78-
let uuid = UUID().uuidString
7976
let testMethodName = String("\(#function)".dropLast(2))
8077
let title = testMethodName + "Title"
8178

8279
let sequenceActor = SequenceActor()
8380
DispatchQueue.concurrentPerform(iterations: concurrencyLimit) { index in
8481
Task {
8582
let subscription = Amplify.API.subscribe(request: .subscription(of: Post.self, type: .onCreate))
86-
Task {
87-
for try await subscriptionEvent in subscription {
88-
switch subscriptionEvent {
89-
case .connection(let state):
90-
switch state {
91-
case .connecting:
92-
break
93-
case .connected:
94-
connectedInvoked.fulfill()
95-
case .disconnected:
96-
disconnectedInvoked.fulfill()
97-
}
98-
case .data(let result):
99-
switch result {
100-
case .success(let post):
101-
if post.id == uuid {
102-
progressInvoked.fulfill()
103-
}
104-
case .failure(let error):
105-
XCTFail("\(error)")
83+
await sequenceActor.append(sequence: subscription)
84+
for try await subscriptionEvent in subscription {
85+
switch subscriptionEvent {
86+
case .connection(let state):
87+
switch state {
88+
case .connecting:
89+
break
90+
case .connected:
91+
connectedInvoked.fulfill()
92+
case .disconnected:
93+
disconnectedInvoked.fulfill()
94+
}
95+
case .data(let result):
96+
switch result {
97+
case .success(let post):
98+
if post.title == title {
99+
progressInvoked.fulfill()
106100
}
101+
case .failure(let error):
102+
XCTFail("\(error)")
107103
}
108104
}
109-
completedInvoked.fulfill()
110105
}
111-
112-
await sequenceActor.append(sequence: subscription)
106+
completedInvoked.fulfill()
113107
}
114108
}
115109

@@ -118,7 +112,7 @@ final class APIStressTests: XCTestCase {
118112
let sequenceCount = await sequenceActor.sequences.count
119113
XCTAssertEqual(sequenceCount, concurrencyLimit)
120114

121-
guard try await createPost(id: uuid, title: title) != nil else {
115+
guard try await Self.createPost(id: UUID().uuidString, title: title) != nil else {
122116
XCTFail("Failed to create post")
123117
return
124118
}
@@ -144,7 +138,7 @@ final class APIStressTests: XCTestCase {
144138
Task {
145139
let id = UUID().uuidString
146140
let title = "title" + String(index)
147-
let post = try await createPost(id: id, title: title)
141+
let post = try await APIStressTests.createPost(id: id, title: title)
148142
XCTAssertNotNil(post)
149143
XCTAssertEqual(id, post?.id)
150144
XCTAssertEqual(title, post?.title)
@@ -169,7 +163,7 @@ final class APIStressTests: XCTestCase {
169163
Task {
170164
let id = UUID().uuidString
171165
let title = "title" + String(index)
172-
let post = try await createPost(id: id, title: title)
166+
let post = try await APIStressTests.createPost(id: id, title: title)
173167
XCTAssertNotNil(post)
174168
XCTAssertEqual(id, post?.id)
175169
XCTAssertEqual(title, post?.title)
@@ -184,7 +178,7 @@ final class APIStressTests: XCTestCase {
184178
Task {
185179
var post = await postActor.posts[index]
186180
post.title = "newTitle" + String(index)
187-
let updatedPost = try await mutatePost(post)
181+
let updatedPost = try await APIStressTests.mutatePost(post)
188182
XCTAssertNotNil(updatedPost)
189183
XCTAssertEqual(post.id, updatedPost.id)
190184
XCTAssertEqual(post.title, updatedPost.title)
@@ -211,7 +205,7 @@ final class APIStressTests: XCTestCase {
211205
Task {
212206
let id = UUID().uuidString
213207
let title = "title" + String(index)
214-
let post = try await createPost(id: id, title: title)
208+
let post = try await APIStressTests.createPost(id: id, title: title)
215209
XCTAssertNotNil(post)
216210
XCTAssertEqual(id, post?.id)
217211
XCTAssertEqual(title, post?.title)
@@ -225,7 +219,7 @@ final class APIStressTests: XCTestCase {
225219
DispatchQueue.concurrentPerform(iterations: concurrencyLimit) { index in
226220
Task {
227221
let post = await postActor.posts[index]
228-
let deletedPost = try await deletePost(post: post)
222+
let deletedPost = try await APIStressTests.deletePost(post: post)
229223
XCTAssertNotNil(deletedPost)
230224
XCTAssertEqual(post.id, deletedPost.id)
231225
XCTAssertEqual(post.title, deletedPost.title)
@@ -252,7 +246,7 @@ final class APIStressTests: XCTestCase {
252246
Task {
253247
let id = UUID().uuidString
254248
let title = "title" + String(index)
255-
let post = try await createPost(id: id, title: title)
249+
let post = try await APIStressTests.createPost(id: id, title: title)
256250
XCTAssertNotNil(post)
257251
XCTAssertEqual(id, post?.id)
258252
XCTAssertEqual(title, post?.title)
@@ -301,12 +295,12 @@ final class APIStressTests: XCTestCase {
301295

302296
// MARK: - Helpers
303297

304-
func createPost(id: String, title: String) async throws -> Post? {
298+
static func createPost(id: String, title: String) async throws -> Post? {
305299
let post = Post(id: id, title: title, status: .active, content: "content")
306300
return try await createPost(post: post)
307301
}
308302

309-
func createPost(post: Post) async throws -> Post? {
303+
static func createPost(post: Post) async throws -> Post? {
310304
let data = try await Amplify.API.mutate(request: .create(post))
311305
switch data {
312306
case .success(let post):
@@ -316,7 +310,7 @@ final class APIStressTests: XCTestCase {
316310
}
317311
}
318312

319-
func mutatePost(_ post: Post) async throws -> Post {
313+
static func mutatePost(_ post: Post) async throws -> Post {
320314
let data = try await Amplify.API.mutate(request: .update(post))
321315
switch data {
322316
case .success(let post):
@@ -326,7 +320,7 @@ final class APIStressTests: XCTestCase {
326320
}
327321
}
328322

329-
func deletePost(post: Post) async throws -> Post {
323+
static func deletePost(post: Post) async throws -> Post {
330324
let data = try await Amplify.API.mutate(request: .delete(post))
331325
switch data {
332326
case .success(let post):

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ struct AWSCognitoAuthCredentialStore {
5656
if migrateKeychainItemsOfUserSession {
5757
try? migrateKeychainItemsToAccessGroup()
5858
} else if oldAccessGroup == nil && oldAccessGroup != accessGroup {
59-
try? KeychainStore(service: service)._removeAll()
59+
// Only clear the old keychain if the shared keychain doesn't already have items.
60+
// This prevents data loss when an app extension (e.g., widget) initializes before
61+
// the main app has a chance to record the migration in UserDefaults, since
62+
// UserDefaults is not shared between app and extensions.
63+
if !sharedKeychainHasItems(accessGroup: accessGroup) {
64+
try? KeychainStore(service: service)._removeAll()
65+
}
6066
}
6167

6268
saveStoredAccessGroup()
@@ -252,6 +258,15 @@ extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior {
252258
return
253259
}
254260

261+
// If the shared keychain already has items, migration has already occurred
262+
// (likely by the main app). Skip migration to prevent data loss.
263+
// This check is necessary because UserDefaults is not shared between app and extensions,
264+
// so the extension may not know that migration already happened.
265+
if sharedKeychainHasItems(accessGroup: accessGroup) {
266+
log.info("[AWSCognitoAuthCredentialStore] Shared keychain already has items, migration already completed, aborting")
267+
return
268+
}
269+
255270
let oldService = oldAccessGroup != nil ? sharedService : service
256271
let newService = accessGroup != nil ? sharedService : service
257272

@@ -265,6 +280,17 @@ extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior {
265280
log.verbose("[AWSCognitoAuthCredentialStore] Migration of keychain items from old access group to new access group successful")
266281
}
267282

283+
/// Checks if the shared keychain (with the given access group) already contains items.
284+
/// This is used to determine if migration has already occurred, which helps prevent
285+
/// data loss when app extensions initialize with their own UserDefaults that don't
286+
/// reflect the migration state recorded by the main app.
287+
private func sharedKeychainHasItems(accessGroup: String?) -> Bool {
288+
guard let accessGroup else { return false }
289+
290+
let sharedKeychain = KeychainStore(service: sharedService, accessGroup: accessGroup)
291+
return (try? sharedKeychain._hasItems()) ?? false
292+
}
293+
268294
}
269295

270296
/// Helpers for encode and decoding

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,8 @@ class MockKeychainStoreBehavior: KeychainStoreBehavior {
4343
func _removeAll() throws {
4444
removeAllHandler?()
4545
}
46+
47+
func _hasItems() throws -> Bool {
48+
return !data.isEmpty
49+
}
4650
}

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/DefaultConfig.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ struct MockLegacyStore: KeychainStoreBehavior {
396396

397397
}
398398

399+
func _hasItems() throws -> Bool {
400+
return false
401+
}
402+
399403
}
400404

401405
struct MockASF: AdvancedSecurityBehavior {

AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public protocol KeychainStoreBehavior {
5353
@_spi(KeychainStore)
5454
func _removeAll() throws
5555

56+
/// Checks if the Keychain contains any items for this service and access group.
57+
/// This System Programming Interface (SPI) may have breaking changes in future updates.
58+
/// - Returns: `true` if at least one item exists, `false` otherwise
59+
@_spi(KeychainStore)
60+
func _hasItems() throws -> Bool
61+
5662
}
5763

5864
public struct KeychainStore: KeychainStoreBehavior {
@@ -233,6 +239,29 @@ public struct KeychainStore: KeychainStoreBehavior {
233239
log.verbose("[KeychainStore] Successfully removed all items from keychain")
234240
}
235241

242+
/// Checks if the Keychain contains any items for this service and access group.
243+
/// This System Programming Interface (SPI) may have breaking changes in future updates.
244+
/// - Returns: `true` if at least one item exists, `false` otherwise
245+
@_spi(KeychainStore)
246+
public func _hasItems() throws -> Bool {
247+
log.verbose("[KeychainStore] Checking if keychain has any items")
248+
var query = attributes.defaultGetQuery()
249+
query[Constants.MatchLimit] = Constants.MatchLimitOne
250+
251+
let status = SecItemCopyMatching(query as CFDictionary, nil)
252+
switch status {
253+
case errSecSuccess:
254+
log.verbose("[KeychainStore] Keychain has items")
255+
return true
256+
case errSecItemNotFound:
257+
log.verbose("[KeychainStore] Keychain has no items")
258+
return false
259+
default:
260+
log.error("[KeychainStore] Error checking keychain items with status=\(status)")
261+
throw KeychainStoreError.securityError(status)
262+
}
263+
}
264+
236265
}
237266

238267
extension KeychainStore {

AmplifyPlugins/Internal/Tests/InternalAWSPinpointUnitTests/Mocks/MockKeychainStore.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class MockKeychainStore: KeychainStoreBehavior {
6262
dataValues.removeAll()
6363
}
6464

65+
func _hasItems() throws -> Bool {
66+
return !stringValues.isEmpty || !dataValues.isEmpty
67+
}
68+
6569
func resetCounters() {
6670
dataForKeyCount = 0
6771
stringForKeyCount = 0

AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Persistence/LogRotation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ final class LogRotation {
209209
contents: nil,
210210
attributes: [FileAttributeKey: Any]()
211211
)
212-
if #available(macOS 11.0, *) {
212+
if #available(macOS 11.0, iOS 9.0, *) {
213213
let resourceValues: [URLResourceKey: Any] = [
214-
URLResourceKey.fileProtectionKey: URLFileProtection.complete,
214+
URLResourceKey.fileProtectionKey: URLFileProtection.completeUntilFirstUserAuthentication,
215215
URLResourceKey.isExcludedFromBackupKey: true
216216
]
217217
try (fileURL as NSURL).setResourceValues(resourceValues)

0 commit comments

Comments
 (0)