diff --git a/.gitignore b/.gitignore index ee458f4..d38b3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -80,5 +80,5 @@ Gemfile #config file Tests/config.json -snyk_output.json -talisman_output.json \ No newline at end of file +snyk_output.log +talisman_output.log \ No newline at end of file diff --git a/.talismanrc b/.talismanrc index 4100d86..0539b40 100644 --- a/.talismanrc +++ b/.talismanrc @@ -28,6 +28,8 @@ fileignoreconfig: checksum: dfabf06aeff3576c9347e52b3c494635477d81c7d121d8f1435d79f28829f4d1 - filename: ContentstackSwift.xcodeproj/project.pbxproj checksum: 8937f832171f26061a209adcd808683f7bdfb739e7fc49aecd853d5055466251 +- filename: ContentstackSwift.xcodeproj/project.pbxproj + checksum: b743f609350e19c2a05a2081f3af3f723992b9610b3b3e6aa402792cad1de2c5 version: "1.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb1370..e9c17f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## v2.2.0 + +### Date: 01-Sep-2025 + +- Async/await support added + ## v2.1.0 ### Date: 06-Jun-2025 diff --git a/ContentstackSwift.xcodeproj/project.pbxproj b/ContentstackSwift.xcodeproj/project.pbxproj index 788647e..80d20c4 100644 --- a/ContentstackSwift.xcodeproj/project.pbxproj +++ b/ContentstackSwift.xcodeproj/project.pbxproj @@ -265,6 +265,9 @@ 47C6EFC52C0B5B9400F0D5CF /* Taxonomy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C6EFC12C0B5B9400F0D5CF /* Taxonomy.swift */; }; 47D561512C9EF97D00DC085D /* ContentstackUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 47D561502C9EF97D00DC085D /* ContentstackUtils */; }; 47D561572C9EFA5900DC085D /* DVR in Frameworks */ = {isa = PBXBuildFile; productRef = 47D561562C9EFA5900DC085D /* DVR */; }; + 672F76992E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672F76982E55ADBE00C248D6 /* AsyncAwaitAPITest.swift */; }; + 672F769A2E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672F76982E55ADBE00C248D6 /* AsyncAwaitAPITest.swift */; }; + 672F769B2E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672F76982E55ADBE00C248D6 /* AsyncAwaitAPITest.swift */; }; 6750778E2D3E256A0076A066 /* DVR in Frameworks */ = {isa = PBXBuildFile; productRef = 6750778D2D3E256A0076A066 /* DVR */; }; 67EE21DF2DDB4013005AC119 /* CSURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EE21DE2DDB3FFE005AC119 /* CSURLSessionDelegate.swift */; }; 67EE21E02DDB4013005AC119 /* CSURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EE21DE2DDB3FFE005AC119 /* CSURLSessionDelegate.swift */; }; @@ -416,6 +419,7 @@ 47B09C242CA952E400B8AB41 /* DVR.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DVR.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47B4DC612C232A8200370CFC /* TaxonomyTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxonomyTest.swift; sourceTree = ""; }; 47C6EFC12C0B5B9400F0D5CF /* Taxonomy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Taxonomy.swift; sourceTree = ""; }; + 672F76982E55ADBE00C248D6 /* AsyncAwaitAPITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAwaitAPITest.swift; sourceTree = ""; }; 67EE21DE2DDB3FFE005AC119 /* CSURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSURLSessionDelegate.swift; sourceTree = ""; }; 67EE222B2DE4868F005AC119 /* GlobalField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalField.swift; sourceTree = ""; }; 67EE22302DE58B51005AC119 /* GlobalFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalFieldModel.swift; sourceTree = ""; }; @@ -666,6 +670,7 @@ 0FFA5D9D241F8F9B003B3AF5 /* APITests */ = { isa = PBXGroup; children = ( + 672F76982E55ADBE00C248D6 /* AsyncAwaitAPITest.swift */, 67EE22352DE5BAF2005AC119 /* GlobalFieldAPITest.swift */, 0F50EA1C244ED88C00E5D705 /* StackCacheAPITest.swift */, 470657532B5E785C00BBFF88 /* ContentTypeQueryAPITest.swift */, @@ -1146,6 +1151,7 @@ 47B4DC622C232A8200370CFC /* TaxonomyTest.swift in Sources */, 0F50EA1D244ED88C00E5D705 /* StackCacheAPITest.swift in Sources */, 470657582B5E788400BBFF88 /* EntryAPITest.swift in Sources */, + 672F76992E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */, 0F096B14243610470094F042 /* ImageTransformTestAdditional.swift in Sources */, 0FD39D4A24237A0400E34826 /* ContentTypeQueryTest.swift in Sources */, 0FFBB44C24470C43000D2795 /* ContentStackLogTest.swift in Sources */, @@ -1228,6 +1234,7 @@ 47B4DC632C232A8200370CFC /* TaxonomyTest.swift in Sources */, 0FFA5D90241F8126003B3AF5 /* ContentstackConfigTest.swift in Sources */, 0F50EA1E244ED88C00E5D705 /* StackCacheAPITest.swift in Sources */, + 672F769B2E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */, 0F096B15243610470094F042 /* ImageTransformTestAdditional.swift in Sources */, 0FD39D4B24237A0400E34826 /* ContentTypeQueryTest.swift in Sources */, 0FFBB44D24470C43000D2795 /* ContentStackLogTest.swift in Sources */, @@ -1310,6 +1317,7 @@ 47B4DC642C232A8200370CFC /* TaxonomyTest.swift in Sources */, 0FFA5D91241F8127003B3AF5 /* ContentstackConfigTest.swift in Sources */, 0F50EA1F244ED88C00E5D705 /* StackCacheAPITest.swift in Sources */, + 672F769A2E55ADBE00C248D6 /* AsyncAwaitAPITest.swift in Sources */, 0F096B16243610470094F042 /* ImageTransformTestAdditional.swift in Sources */, 0FD39D4C24237A0400E34826 /* ContentTypeQueryTest.swift in Sources */, 0FFBB44E24470C43000D2795 /* ContentStackLogTest.swift in Sources */, diff --git a/Sources/Asset.swift b/Sources/Asset.swift index 0a3b9bf..1d4ba9a 100644 --- a/Sources/Asset.swift +++ b/Sources/Asset.swift @@ -246,4 +246,27 @@ extension Asset: ResourceQueryable { } }) } + + // MARK: - Async/Await Implementation + + /// Async version of fetch that returns the Asset directly + /// - Returns: The fetched Asset + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func fetch() async throws -> ResourceType + where ResourceType: EndpointAccessible & Decodable { + guard let uid = self.uid else { fatalError("Please provide Asset uid") } + let response: ContentstackResponse = try await self.stack.fetch( + endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters + [QueryParameter.uid: uid], + headers: headers + ) + + if let resource = response.items.first { + return resource + } else { + throw SDKError.invalidUID(string: uid) + } + } } diff --git a/Sources/ContentType.swift b/Sources/ContentType.swift index bc5cbbf..043a2ba 100644 --- a/Sources/ContentType.swift +++ b/Sources/ContentType.swift @@ -178,4 +178,27 @@ extension ContentType: ResourceQueryable { } }) } + + // MARK: - Async/Await Implementation + + /// Async version of fetch that returns the ContentType directly + /// - Returns: The fetched ContentType + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func fetch() async throws -> ResourceType + where ResourceType: EndpointAccessible & Decodable { + guard let uid = self.uid else { fatalError("Please provide ContentType uid") } + let response: ContentstackResponse = try await self.stack.fetch( + endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters + [QueryParameter.uid: uid], + headers: headers + ) + + if let resource = response.items.first { + return resource + } else { + throw SDKError.invalidUID(string: uid) + } + } } diff --git a/Sources/Entry.swift b/Sources/Entry.swift index 235c917..e566fc3 100644 --- a/Sources/Entry.swift +++ b/Sources/Entry.swift @@ -175,4 +175,28 @@ extension Entry: ResourceQueryable { } }) } + + // MARK: - Async/Await Implementation + + /// Async version of fetch that returns the Entry directly + /// - Returns: The fetched Entry + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func fetch() async throws -> ResourceType + where ResourceType: EndpointAccessible & Decodable { + guard let uid = self.uid else { fatalError("Please provide Entry uid") } + let response: ContentstackResponse = try await self.stack.fetch( + endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters + [QueryParameter.uid: uid, + QueryParameter.contentType: self.contentType.uid!], + headers: headers + ) + + if let resource = response.items.first { + return resource + } else { + throw SDKError.invalidUID(string: uid) + } + } } diff --git a/Sources/GlobalField.swift b/Sources/GlobalField.swift index 680e368..8bf8231 100644 --- a/Sources/GlobalField.swift +++ b/Sources/GlobalField.swift @@ -80,6 +80,29 @@ extension GlobalField: ResourceQueryable { } }) } + + // MARK: - Async/Await Implementation for fetch + + /// Async version of fetch that returns the GlobalField directly + /// - Returns: The fetched GlobalField + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func fetch() async throws -> ResourceType + where ResourceType: EndpointAccessible & Decodable { + guard let uid = self.uid else { fatalError("Please provide Global Field uid") } + let response: ContentstackResponse = try await self.stack.fetch( + endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters + [QueryParameter.uid: uid], + headers: headers + ) + + if let resource = response.items.first { + return resource + } else { + throw SDKError.invalidUID(string: uid) + } + } } extension GlobalField : Queryable{ @@ -92,4 +115,19 @@ extension GlobalField : Queryable{ cachePolicy: self.cachePolicy, parameters: parameters, headers: headers, then: completion) } + // MARK: - Async/Await Implementation + + /// Async implementation of find for GlobalField + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func find() async throws -> ContentstackResponse + where ResourceType: Decodable & EndpointAccessible { + if self.queryParameter.count > 0, + let query = self.queryParameter.jsonString { + self.parameters[QueryParameter.query] = query + } + return try await self.stack.fetch(endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters, + headers: headers) + } } diff --git a/Sources/QueryProtocols.swift b/Sources/QueryProtocols.swift index f639706..631b2ad 100644 --- a/Sources/QueryProtocols.swift +++ b/Sources/QueryProtocols.swift @@ -75,6 +75,22 @@ extension BaseQuery { self.stack.fetch(endpoint: ResourceType.endpoint, cachePolicy: self.cachePolicy, parameters: parameters, headers: headers, then: completion) } + + /// Async version of find that returns the ContentstackResponse directly + /// - Returns: The ContentstackResponse with the requested resources + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func find() async throws -> ContentstackResponse + where ResourceType: Decodable & EndpointAccessible { + if self.queryParameter.count > 0, + let query = self.queryParameter.jsonString { + self.parameters[QueryParameter.query] = query + } + return try await self.stack.fetch(endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters, + headers: headers) + } } /// A concrete implementation of BaseQuery which serves as the base class for `Query`, /// `ContentTypeQuery` and `AssetQuery`. @@ -557,6 +573,13 @@ public protocol ResourceQueryable { /// - completion: A handler which will be called on completion of the operation. func fetch(_ completion: @escaping ResultsHandler) where ResourceType: Decodable & EndpointAccessible + + /// Async version of fetch that returns the resource directly + /// - Returns: The fetched resource + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + func fetch() async throws -> ResourceType + where ResourceType: Decodable & EndpointAccessible } /// The base Queryable protocol to find collections for content types, assets, and entries. @@ -567,4 +590,11 @@ public protocol Queryable { /// - completion: A handler which will be called on completion of the operation. func find(_ completion: @escaping ResultsHandler>) where ResourceType: Decodable & EndpointAccessible + + /// Async version of find that returns the ContentstackResponse directly + /// - Returns: The ContentstackResponse with the requested resources + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + func find() async throws -> ContentstackResponse + where ResourceType: Decodable & EndpointAccessible } diff --git a/Sources/Stack.swift b/Sources/Stack.swift index 0e9f59d..438855c 100644 --- a/Sources/Stack.swift +++ b/Sources/Stack.swift @@ -263,6 +263,29 @@ public class Stack: CachePolicyAccessible { performDataTask(dataTask!, request: request, cachePolicy: cachePolicy, then: completion) } + // MARK: - Async/Await Support + + /// Async version of fetchUrl that returns the result directly + /// - Parameters: + /// - url: The URL to fetch + /// - headers: HTTP headers to include in the request + /// - cachePolicy: The cache policy to use + /// - Returns: A tuple containing the data and response type + /// - Throws: Network or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + private func fetchUrl(_ url: URL, headers: [String: String], cachePolicy: CachePolicy) async throws -> (Data, ResponseType) { + return try await withCheckedThrowingContinuation { continuation in + fetchUrl(url, headers: headers, cachePolicy: cachePolicy) { result, responseType in + switch result { + case .success(let data): + continuation.resume(returning: (data, responseType)) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + internal func fetch(endpoint: Endpoint, cachePolicy: CachePolicy, parameters: Parameters = [:], @@ -284,6 +307,25 @@ public class Stack: CachePolicyAccessible { } }) } + + /// Async version of fetch that returns the decoded resource directly + /// - Parameters: + /// - endpoint: The API endpoint to fetch from + /// - cachePolicy: The cache policy to use + /// - parameters: Query parameters + /// - headers: HTTP headers + /// - Returns: The decoded resource + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + internal func fetch(endpoint: Endpoint, + cachePolicy: CachePolicy, + parameters: Parameters = [:], + headers: [String: String] = [:]) async throws -> ResourceType + where ResourceType: Decodable { + let url = self.url(endpoint: endpoint, parameters: parameters) + let (data, _) = try await fetchUrl(url, headers: headers, cachePolicy: cachePolicy) + return try self.jsonDecoder.decode(ResourceType.self, from: data) + } private func performDataTask(_ dataTask: URLSessionDataTask, request: URLRequest, @@ -397,4 +439,45 @@ extension Stack { } } } + + /// Async version of sync that returns the SyncStack directly + /// - Parameters: + /// - syncStack: The relevant `SyncStack` to perform the subsequent sync on. + /// Defaults to a new empty instance of `SyncStack`. + /// - syncTypes: `SyncableTypes` that can be sync. + /// - Returns: The SyncStack with synced data + /// - Throws: Network or decoding errors + /// + /// Example usage: + ///``` + /// let stack = Contentstack.stack(apiKey: apiKey, + /// deliveryToken: deliveryToken, + /// environment: environment) + /// + /// do { + /// let syncStack = try await stack.sync() + /// let items = syncStack.items + /// } catch { + /// print(error) + /// } + ///``` + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func sync(_ syncStack: SyncStack = SyncStack(), + syncTypes: [SyncStack.SyncableTypes] = [.all]) async throws -> SyncStack { + var parameter = syncStack.parameter + if syncStack.isInitialSync { + for syncType in syncTypes { + parameter = parameter + syncType.parameters + } + } + let url = self.url(endpoint: SyncStack.endpoint, parameters: parameter) + let (data, _) = try await fetchUrl(url, headers: [:], cachePolicy: .networkOnly) + let result = try self.jsonDecoder.decode(SyncStack.self, from: data) + + if result.hasMorePages { + return try await sync(result, syncTypes: syncTypes) + } + + return result + } } diff --git a/Sources/Taxonomy.swift b/Sources/Taxonomy.swift index 98a356f..3044e93 100644 --- a/Sources/Taxonomy.swift +++ b/Sources/Taxonomy.swift @@ -47,4 +47,26 @@ extension Taxonomy: ResourceQueryable { } }) } + + // MARK: - Async/Await Implementation for fetch + + /// Async version of fetch that returns the Taxonomy directly + /// - Returns: The fetched Taxonomy + /// - Throws: Network, decoding, or cache errors + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public func fetch() async throws -> ResourceType + where ResourceType: EndpointAccessible & Decodable { + let response: ContentstackResponse = try await self.stack.fetch( + endpoint: ResourceType.endpoint, + cachePolicy: self.cachePolicy, + parameters: parameters, + headers: headers + ) + + if let resource = response.items.first { + return resource + } else { + throw SDKError.invalidURL(string: "Something went wrong.") + } + } } diff --git a/Tests/AsyncAwaitAPITest.swift b/Tests/AsyncAwaitAPITest.swift new file mode 100644 index 0000000..1ae0110 --- /dev/null +++ b/Tests/AsyncAwaitAPITest.swift @@ -0,0 +1,368 @@ +// +// AsyncAwaitTests.swift +// ContentstackSwift +// +// Created by Contentstack Team +// + +import XCTest +@testable import ContentstackSwift +import DVR + +// MARK: - Stack Async Tests +class AsyncAwaitStackTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "SyncTest") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testStackSyncAsync() async { + // Test that async sync method doesn't crash and handles errors properly + do { + let syncStack = try await AsyncAwaitStackTests.stack.sync() + XCTAssertNotNil(syncStack) + } catch { + // Expected to fail in test environment, but should not crash + XCTAssertTrue(error is APIError || error is DecodingError) + } + } +} + +// MARK: - Asset Async Tests +class AsyncAwaitAssetTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "Asset") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testAssetFetchAsync() async { + // Test that async asset fetch method doesn't crash + // Using UID from Asset.json cassette: "uid_89" + do { + let asset: AssetModel = try await AsyncAwaitAssetTests.stack.asset(uid: "uid_89") + .includeDimension() + .includeMetadata() + .fetch() + XCTAssertNotNil(asset) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } + + func testAssetQueryAsync() async { + // Test that async asset query method doesn't crash + do { + let assets: ContentstackResponse = try await AsyncAwaitAssetTests.stack.asset() + .query() + .limit(to: 5) + .find() + XCTAssertNotNil(assets) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } +} + +// MARK: - ContentType Async Tests +class AsyncAwaitContentTypeTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "ContentType") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testContentTypeFetchAsync() async { + // Test that async content type fetch method doesn't crash + // Using UID from ContentType.json cassette: "session" + do { + let contentType: ContentTypeModel = try await AsyncAwaitContentTypeTests.stack.contentType(uid: "session") + .fetch() + XCTAssertNotNil(contentType) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } + + func testContentTypeQueryAsync() async { + // Test that async content type query method doesn't crash + do { + let contentTypes: ContentstackResponse = try await AsyncAwaitContentTypeTests.stack.contentType() + .query() + .limit(to: 10) + .find() + XCTAssertNotNil(contentTypes) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } +} + +// MARK: - Entry Async Tests +class AsyncAwaitEntryTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "Entry") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testEntryFetchAsync() async { + // Test that async entry fetch method doesn't crash + // Using UIDs from Entry.json cassette: contentType "session", entry "session_uid_1" + do { + let entry: EntryModel = try await AsyncAwaitEntryTests.stack.contentType(uid: "session") + .entry(uid: "session_uid_1") + .fetch() + XCTAssertNotNil(entry) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } + + func testEntryQueryAsync() async { + // Test that async entry query method doesn't crash + do { + let entries: ContentstackResponse = try await AsyncAwaitEntryTests.stack.contentType(uid: "session") + .entry() + .query() + .limit(to: 15) + .find() + XCTAssertNotNil(entries) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } +} + +// MARK: - GlobalField Async Tests +class AsyncAwaitGlobalFieldTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "GlobalField") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testGlobalFieldFetchAsync() async { + // Test that async global field fetch method doesn't crash + // Using UID from GlobalField.json cassette: "feature" + do { + let globalField: GlobalFieldModel = try await AsyncAwaitGlobalFieldTests.stack.globalField(uid: "feature") + .includeGlobalFieldSchema() + .fetch() + XCTAssertNotNil(globalField) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } + + func testGlobalFieldQueryAsync() async { + // Test that async global field query method doesn't crash + do { + let globalFields: ContentstackResponse = try await AsyncAwaitGlobalFieldTests.stack.globalField() + .find() + XCTAssertNotNil(globalFields) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } +} + +// MARK: - Error Handling Tests +class AsyncAwaitErrorTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "Asset") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testInvalidUIDErrorAsync() async { + // Test error handling for invalid UID + do { + let _: AssetModel = try await AsyncAwaitErrorTests.stack.asset(uid: "invalid_uid_that_does_not_exist") + .fetch() + XCTFail("Should have thrown an error for invalid UID") + } catch { + XCTAssertTrue(error is SDKError || error is APIError) + } + } + + func testNetworkErrorAsync() async { + // Test error handling for network issues + do { + let _: ContentstackResponse = try await AsyncAwaitErrorTests.stack.contentType(uid: "invalid_content_type_that_does_not_exist") + .entry() + .query() + .find() + XCTFail("Should have thrown an error for invalid content type") + } catch { + XCTAssertTrue(error is SDKError || error is APIError) + } + } +} + +// MARK: - Concurrent Operations Tests +class AsyncAwaitConcurrentTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "Asset") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testConcurrentAssetAndEntryFetchAsync() async { + // Test concurrent operations + do { + async let assets: ContentstackResponse = AsyncAwaitConcurrentTests.stack.asset() + .query() + .limit(to: 3) + .find() + + async let entries: ContentstackResponse = AsyncAwaitConcurrentTests.stack.contentType(uid: "session") + .entry() + .query() + .limit(to: 3) + .find() + + let (assetsResult, entriesResult) = try await (assets, entries) + + // If we get here, the concurrent async methods worked + XCTAssertNotNil(assetsResult) + XCTAssertNotNil(entriesResult) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } + + func testConcurrentMultipleOperationsAsync() async { + // Test multiple concurrent operations + do { + async let syncStack = AsyncAwaitConcurrentTests.stack.sync() + async let assets: ContentstackResponse = AsyncAwaitConcurrentTests.stack.asset().query().limit(to: 2).find() + async let contentTypes: ContentstackResponse = AsyncAwaitConcurrentTests.stack.contentType().query().limit(to: 2).find() + + let (syncResult, assetsResult, contentTypesResult) = try await (syncStack, assets, contentTypes) + + // If we get here, the concurrent async methods worked + XCTAssertNotNil(syncResult) + XCTAssertNotNil(assetsResult) + XCTAssertNotNil(contentTypesResult) + } catch { + // Expected to fail with invalid credentials, but async/await should work + XCTAssertTrue(error is APIError || error is SDKError) + } + } +} + +// MARK: - Async/Await Syntax Tests +class AsyncAwaitSyntaxTests: XCTestCase { + + static let stack = TestContentstackClient.testStack(cassetteName: "Asset") + + override class func setUp() { + super.setUp() + (stack.urlSession as? DVR.Session)?.beginRecording() + } + + override class func tearDown() { + super.tearDown() + (stack.urlSession as? DVR.Session)?.endRecording() + } + + func testAsyncAwaitSyntax() async { + // Test that async/await syntax works correctly + let expectation = XCTestExpectation(description: "Async operation completed") + + Task { + do { + let _: ContentstackResponse = try await AsyncAwaitSyntaxTests.stack.asset() + .query() + .limit(to: 1) + .find() + expectation.fulfill() + } catch { + // Expected to fail, but async/await syntax should work + expectation.fulfill() + } + } + + await fulfillment(of: [expectation], timeout: 5.0) + } + + func testAsyncLetSyntax() async { + // Test async let syntax for concurrent operations + let expectation = XCTestExpectation(description: "Concurrent operations completed") + + Task { + do { + async let assetQuery: ContentstackResponse = AsyncAwaitSyntaxTests.stack.asset().query().limit(to: 1).find() + async let contentTypeQuery: ContentstackResponse = AsyncAwaitSyntaxTests.stack.contentType().query().limit(to: 1).find() + + let (_, _) = try await (assetQuery, contentTypeQuery) + expectation.fulfill() + } catch { + // Expected to fail, but async let syntax should work + expectation.fulfill() + } + } + + await fulfillment(of: [expectation], timeout: 5.0) + } +}