From 25be6f518f4369127c7d306b7c35b11d9d8821c4 Mon Sep 17 00:00:00 2001 From: Hesham Salman Date: Mon, 9 Oct 2023 16:35:12 -0400 Subject: [PATCH 1/3] Import Test Helpers --- Package.swift | 6 +- .../ApolloInternalTestHelpers.h | 9 + .../AsyncResultObserver.swift | 70 ++++++++ .../ApolloInternalTestHelpers/Info.plist | 24 +++ .../InterceptorTester.swift | 81 +++++++++ .../ApolloInternalTestHelpers/Matchable.swift | 23 +++ .../MockApolloStore.swift | 32 ++++ .../MockGraphQLServer.swift | 107 ++++++++++++ .../MockHTTPRequest.swift | 15 ++ .../MockHTTPResponse.swift | 36 ++++ .../MockInterceptorProvider.swift | 13 ++ .../MockLocalCacheMutation.swift | 40 +++++ .../MockNetworkTransport.swift | 97 +++++++++++ .../MockOperation.swift | 97 +++++++++++ .../MockSchemaMetadata.swift | 111 ++++++++++++ .../MockURLProtocol.swift | 56 ++++++ .../MockURLSession.swift | 75 +++++++++ .../MockWebSocket.swift | 41 +++++ .../MockWebSocketDelegate.swift | 18 ++ .../ApolloInternalTestHelpers/Resources/a.txt | 1 + .../ApolloInternalTestHelpers/Resources/b.txt | 1 + .../ApolloInternalTestHelpers/Resources/c.txt | 1 + .../SQLiteTestCacheProvider.swift | 33 ++++ .../String+Data.swift | 7 + .../TestCacheProvider.swift | 59 +++++++ .../ApolloInternalTestHelpers/TestError.swift | 14 ++ .../TestFileHelper.swift | 40 +++++ .../TestIsolatedFileManager.swift | 144 ++++++++++++++++ .../TestObserver.swift | 43 +++++ .../ApolloInternalTestHelpers/TestURLs.swift | 19 +++ .../XCTAssertHelpers.swift | 159 ++++++++++++++++++ .../XCTestCase+Helpers.swift | 77 +++++++++ .../apollo_ios_paginationTests.swift | 6 - 33 files changed, 1548 insertions(+), 7 deletions(-) create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift create mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift diff --git a/Package.swift b/Package.swift index 2c0337dfa..04b2815a3 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,11 @@ let package = Package( ), .testTarget( name: "apollo-ios-paginationTests", - dependencies: ["apollo-ios-pagination"] + dependencies: [ + "apollo-ios-pagination", + .product(name: "ApolloSQLite", package: "apollo-ios"), + .product(name: "ApolloWebSocket", package: "apollo-ios"), + ] ), ] ) diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h new file mode 100644 index 000000000..e780c4d71 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h @@ -0,0 +1,9 @@ +#import + +//! Project version number for ApolloInternalTestHelpers. +FOUNDATION_EXPORT double ApolloInternalTestHelpersVersionNumber; + +//! Project version string for ApolloInternalTestHelpers. +FOUNDATION_EXPORT const unsigned char ApolloInternalTestHelpersVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift new file mode 100644 index 000000000..5938f3992 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift @@ -0,0 +1,70 @@ +import XCTest + +/// `AsyncResultObserver` is a helper class that can be used to test `Result` values received through a completion handler against one or more expectations. It is primarily useful if you expect the completion handler to be called multiple times, when receiving a fetch result from the cache and then from the server for example. +/// +/// The main benefit is that it avoids having to manually keep track of expectations and mutable closures (like `verifyResult`), which can make code hard to read and is prone to mistakes. Instead, you can use a result observer to create multiple expectations that will be automatically fulfilled in order when results are received. Often, you'll also want to run assertions against the result, which you can do by passing in an optional handler that is specific to that expectation. These handlers are throwing, which means you can use `result.get()` and `XCTUnwrap` for example. Thrown errors will automatically be recorded as failures in the test case (with the right line numbers, etc.). +/// +/// By default, expectations returned from `AsyncResultObserver` only expect to be called once, which is similar to how other built-in expectations work. Unexpected fulfillments will result in test failures. Usually this is what you want, and you add additional expectations with their own assertions if you expect further results. +/// If multiple fulfillments of a single expectation are expected however, you can use the standard `expectedFulfillmentCount` property to change that. +public class AsyncResultObserver where Failure: Error { + public typealias ResultHandler = (Result) throws -> Void + + private class AsyncResultExpectation: XCTestExpectation { + let file: StaticString + let line: UInt + let handler: ResultHandler + + init(description: String, file: StaticString = #filePath, line: UInt = #line, handler: @escaping ResultHandler) { + self.file = file + self.line = line + self.handler = handler + + super.init(description: description) + } + } + + private let testCase: XCTestCase + + // We keep track of the file and line number associated with the constructor as a fallback, in addition te keeping + // these for each expectation. That way, we can still show a failure within the context of the test in case unexpected + // results are received (which by definition do not have an associated expectation). + private let file: StaticString + private let line: UInt + + private var expectations: [AsyncResultExpectation] = [] + + public init(testCase: XCTestCase, file: StaticString = #filePath, line: UInt = #line) { + self.testCase = testCase + self.file = file + self.line = line + } + + public func expectation(description: String, file: StaticString = #filePath, line: UInt = #line, resultHandler: @escaping ResultHandler) -> XCTestExpectation { + let expectation = AsyncResultExpectation(description: description, file: file, line: line, handler: resultHandler) + expectation.assertForOverFulfill = true + + expectations.append(expectation) + + return expectation + } + + public func handler(_ result: Result) { + guard let expectation = expectations.first else { + XCTFail("Unexpected result received by handler", file: file, line: line) + return + } + + do { + try expectation.handler(result) + } catch { + testCase.record(error, file: expectation.file, line: expectation.line) + } + + expectation.fulfill() + + if expectation.numberOfFulfillments >= expectation.expectedFulfillmentCount { + expectations.removeFirst() + } + } +} + diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist new file mode 100644 index 000000000..fbe1e6b31 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift new file mode 100644 index 000000000..4cdf758a0 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift @@ -0,0 +1,81 @@ +import Apollo +import Foundation + +/// Use this interceptor tester to isolate a single `ApolloInterceptor` vs. having to create an +/// `InterceptorRequestChain` and end the interceptor list with `JSONResponseParsingInterceptor` +/// to get a parsed `GraphQLResult` for the standard request chain callback. +public class InterceptorTester { + let interceptor: any ApolloInterceptor + + public init(interceptor: any ApolloInterceptor) { + self.interceptor = interceptor + } + + public func intercept( + request: Apollo.HTTPRequest, + response: Apollo.HTTPResponse? = nil, + completion: @escaping (Result) -> Void + ) { + let requestChain = ResponseCaptureRequestChain({ result in + completion(result) + }) + + self.interceptor.interceptAsync( + chain: requestChain, + request: request, + response: response) { _ in } + } +} + +fileprivate class ResponseCaptureRequestChain: RequestChain { + var isCancelled: Bool = false + let completion: (Result) -> Void + + init(_ completion: @escaping (Result) -> Void) { + self.completion = completion + } + + func kickoff( + request: Apollo.HTTPRequest, + completion: @escaping (Result, Error>) -> Void + ) {} + + func proceedAsync( + request: Apollo.HTTPRequest, + response: Apollo.HTTPResponse?, + completion: @escaping (Result, Error>) -> Void + ) { + self.completion(.success(response?.rawData)) + } + + func proceedAsync( + request: HTTPRequest, + response: HTTPResponse?, + interceptor: any ApolloInterceptor, + completion: @escaping (Result, Error>) -> Void + ) { + self.completion(.success(response?.rawData)) + } + + func cancel() {} + + func retry( + request: Apollo.HTTPRequest, + completion: @escaping (Result, Error>) -> Void + ) {} + + func handleErrorAsync( + _ error: Error, + request: Apollo.HTTPRequest, + response: Apollo.HTTPResponse?, + completion: @escaping (Result, Error>) -> Void + ) { + self.completion(.failure(error)) + } + + func returnValueAsync( + for request: Apollo.HTTPRequest, + value: Apollo.GraphQLResult, + completion: @escaping (Result, Error>) -> Void + ) {} +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift new file mode 100644 index 000000000..3e368b524 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift @@ -0,0 +1,23 @@ +import Foundation +import ApolloAPI + +public protocol Matchable { + associatedtype Base + static func ~=(pattern: Self, value: Base) -> Bool +} + +extension JSONDecodingError: Matchable { + public typealias Base = Error + public static func ~=(pattern: JSONDecodingError, value: Error) -> Bool { + guard let value = value as? JSONDecodingError else { + return false + } + + switch (value, pattern) { + case (.missingValue, .missingValue), (.nullValue, .nullValue), (.wrongType, .wrongType), (.couldNotConvert, .couldNotConvert): + return true + default: + return false + } + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift new file mode 100644 index 000000000..e5d29c87a --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift @@ -0,0 +1,32 @@ +import Apollo +import ApolloAPI + +extension ApolloStore { + + public static func mock(cache: NormalizedCache = NoCache()) -> ApolloStore { + ApolloStore(cache: cache) + } + +} + +/// A `NormalizedCache` that does not cache any data. Used for tests that don't require testing +/// caching behavior. +public class NoCache: NormalizedCache { + + public init() { } + + public func loadRecords(forKeys keys: Set) throws -> [String : Record] { + return [:] + } + + public func merge(records: RecordSet) throws -> Set { + return Set() + } + + public func removeRecord(for key: String) throws { } + + public func removeRecords(matching pattern: String) throws { } + + public func clear() throws { } + +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift new file mode 100644 index 000000000..bbfcd6c95 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift @@ -0,0 +1,107 @@ +import Apollo +import ApolloAPI +import XCTest + +/// A `MockGraphQLServer` can be used during tests to check whether expected GraphQL requests are received, and to respond with appropriate test data for a particular request. +/// +/// You usually create a mock server in the test's `setUpWithError`, and use it to initialize a `MockNetworkTransport` that is in turn used to initialize an `ApolloClient`: +/// ``` +/// let server = MockGraphQLServer() +/// let networkTransport = MockNetworkTransport(server: server, store: store) +/// let client = ApolloClient(networkTransport: networkTransport, store: store) +/// ``` +/// A mock server should be configured to expect particular operation types, and invokes the passed in request handler when a request of that type comes in. Because the request allows access to `operation`, you can return different responses based on query variables for example: + +/// ``` +/// let serverExpectation = server.expect(HeroNameQuery.self) { request in +/// [ +/// "data": [ +/// "hero": [ +/// "name": request.operation.episode == .empire ? "Luke Skywalker" : "R2-D2", +/// "__typename": "Droid" +/// ] +/// ] +/// ] +/// } +/// ``` +/// By default, expectations returned from `MockGraphQLServer` only expect to be called once, which is similar to how other built-in expectations work. Unexpected fulfillments will result in test failures. But if multiple fulfillments are expected, you can use the standard `expectedFulfillmentCount` property to change that. For example, some of the concurrent tests expect the server to receive the same number of request as the number of invoked fetch operations, so in that case we can use: + +/// ``` +/// serverExpectation.expectedFulfillmentCount = numberOfFetches +/// ``` +public class MockGraphQLServer { + enum ServerError: Error, CustomStringConvertible { + case unexpectedRequest(String) + + public var description: String { + switch self { + case .unexpectedRequest(let requestDescription): + return "Mock GraphQL server received an unexpected request: \(requestDescription)" + } + } + } + + public typealias RequestHandler = (HTTPRequest) -> JSONObject + + private class RequestExpectation: XCTestExpectation { + let file: StaticString + let line: UInt + let handler: RequestHandler + + init(description: String, file: StaticString = #filePath, line: UInt = #line, handler: @escaping RequestHandler) { + self.file = file + self.line = line + self.handler = handler + + super.init(description: description) + } + } + + private let queue = DispatchQueue(label: "com.apollographql.MockGraphQLServer") + + public init() { } + + // Since RequestExpectation is generic over a specific GraphQLOperation, we can't store these in the dictionary + // directly. Moreover, there is no way to specify the type relationship that holds between the key and value. + // To work around this, we store values as Any and use a generic subscript as a type-safe way to access them. + private var requestExpectations: [AnyHashable: Any] = [:] + + private subscript(_ operationType: Operation.Type) -> RequestExpectation? { + get { + requestExpectations[ObjectIdentifier(operationType)] as! RequestExpectation? + } + + set { + requestExpectations[ObjectIdentifier(operationType)] = newValue + } + } + + public func expect(_ operationType: Operation.Type, file: StaticString = #filePath, line: UInt = #line, requestHandler: @escaping (HTTPRequest) -> JSONObject) -> XCTestExpectation { + return queue.sync { + let expectation = RequestExpectation(description: "Served request for \(String(describing: operationType))", file: file, line: line, handler: requestHandler) + expectation.assertForOverFulfill = true + + self[operationType] = expectation + + return expectation + } + } + + func serve(request: HTTPRequest, completionHandler: @escaping (Result) -> Void) where Operation: GraphQLOperation { + let operationType = type(of: request.operation) + + if let expectation = self[operationType] { + // Dispatch after a small random delay to spread out concurrent requests and simulate somewhat real-world conditions. + queue.asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 10...50))) { + completionHandler(.success(expectation.handler(request))) + expectation.fulfill() + } + + } else { + queue.async { + completionHandler(.failure(ServerError.unexpectedRequest(String(describing: operationType)))) + } + } + + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift new file mode 100644 index 000000000..ebe72f26f --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift @@ -0,0 +1,15 @@ +import Apollo +import ApolloAPI + +extension HTTPRequest { + public static func mock(operation: Operation) -> HTTPRequest { + return HTTPRequest( + graphQLEndpoint: TestURL.mockServer.url, + operation: operation, + contentType: "application/json", + clientName: "test-client", + clientVersion: "test-version", + additionalHeaders: [:] + ) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift new file mode 100644 index 000000000..9be77c244 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift @@ -0,0 +1,36 @@ +import Apollo +import ApolloAPI +import Foundation + +extension HTTPResponse { + public static func mock( + statusCode: Int = 200, + headerFields: [String : String] = [:], + data: Data = Data() + ) -> HTTPResponse { + return HTTPResponse( + response: .mock( + statusCode: statusCode, + headerFields: headerFields + ), + rawData: data, + parsedResponse: nil + ) + } +} + +extension HTTPURLResponse { + public static func mock( + url: URL = TestURL.mockServer.url, + statusCode: Int = 200, + httpVersion: String? = nil, + headerFields: [String : String]? = nil + ) -> HTTPURLResponse { + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: httpVersion, + headerFields: headerFields + )! + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift new file mode 100644 index 000000000..f3a29119a --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift @@ -0,0 +1,13 @@ +import Apollo + +public struct MockInterceptorProvider: InterceptorProvider { + let interceptors: [any ApolloInterceptor] + + public init(_ interceptors: [any ApolloInterceptor]) { + self.interceptors = interceptors + } + + public func interceptors(for operation: Operation) -> [any ApolloInterceptor] { + self.interceptors + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift new file mode 100644 index 000000000..534ad168e --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift @@ -0,0 +1,40 @@ +import Foundation +import ApolloAPI + +open class MockLocalCacheMutation: LocalCacheMutation { + open class var operationType: GraphQLOperationType { .query } + + public typealias Data = SelectionSet + + open var __variables: GraphQLOperation.Variables? + + public init() {} + +} + +open class MockLocalCacheMutationFromMutation: + MockLocalCacheMutation { + override open class var operationType: GraphQLOperationType { .mutation } +} + +open class MockLocalCacheMutationFromSubscription: + MockLocalCacheMutation { + override open class var operationType: GraphQLOperationType { .subscription } +} + +public protocol MockMutableRootSelectionSet: MutableRootSelectionSet +where Schema == MockSchemaMetadata {} + +public extension MockMutableRootSelectionSet { + static var __parentType: ParentType { Object.mock } + + init() { + self.init(_dataDict: DataDict( + data: [:], + fulfilledFragments: [ObjectIdentifier(Self.self)] + )) + } +} + +public protocol MockMutableInlineFragment: MutableSelectionSet, InlineFragment +where Schema == MockSchemaMetadata {} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift new file mode 100644 index 000000000..e76e2d47b --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift @@ -0,0 +1,97 @@ +import Foundation +import Apollo +import ApolloAPI + +public final class MockNetworkTransport: RequestChainNetworkTransport { + public init( + server: MockGraphQLServer = MockGraphQLServer(), + store: ApolloStore, + clientName: String = "MockNetworkTransport_ClientName", + clientVersion: String = "MockNetworkTransport_ClientVersion" + ) { + super.init(interceptorProvider: TestInterceptorProvider(store: store, server: server), + endpointURL: TestURL.mockServer.url) + self.clientName = clientName + self.clientVersion = clientVersion + } + + struct TestInterceptorProvider: InterceptorProvider { + let store: ApolloStore + let server: MockGraphQLServer + + func interceptors( + for operation: Operation + ) -> [any ApolloInterceptor] where Operation: GraphQLOperation { + return [ + MaxRetryInterceptor(), + CacheReadInterceptor(store: self.store), + MockGraphQLServerInterceptor(server: server), + ResponseCodeInterceptor(), + JSONResponseParsingInterceptor(), + AutomaticPersistedQueryInterceptor(), + CacheWriteInterceptor(store: self.store), + ] + } + } +} + +private final class MockTask: Cancellable { + func cancel() { + // no-op + } +} + +private class MockGraphQLServerInterceptor: ApolloInterceptor { + let server: MockGraphQLServer + + public var id: String = UUID().uuidString + + init(server: MockGraphQLServer) { + self.server = server + } + + public func interceptAsync(chain: RequestChain, request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) where Operation: GraphQLOperation { + server.serve(request: request) { result in + let httpResponse = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + + switch result { + case .failure(let error): + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let body): + let data = try! JSONSerializationFormat.serialize(value: body) + let response = HTTPResponse(response: httpResponse, + rawData: data, + parsedResponse: nil) + chain.proceedAsync(request: request, + response: response, + interceptor: self, + completion: completion) + } + } + } +} + +public class MockWebSocketTransport: NetworkTransport { + public var clientName, clientVersion: String + + public init(clientName: String, clientVersion: String) { + self.clientName = clientName + self.clientVersion = clientVersion + } + + public func send( + operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result, Error>) -> Void + ) -> Cancellable where Operation : GraphQLOperation { + return MockTask() + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift new file mode 100644 index 000000000..5da746bf9 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift @@ -0,0 +1,97 @@ +import ApolloAPI + +open class MockOperation: GraphQLOperation { + public typealias Data = SelectionSet + + open class var operationType: GraphQLOperationType { .query } + + open class var operationName: String { "MockOperationName" } + + open class var operationDocument: OperationDocument { + .init(definition: .init("Mock Operation Definition")) + } + + open var __variables: Variables? + + public init() {} + +} + +open class MockQuery: MockOperation, GraphQLQuery { + public static func mock() -> MockQuery where SelectionSet == MockSelectionSet { + MockQuery() + } +} + +open class MockMutation: MockOperation, GraphQLMutation { + + public override class var operationType: GraphQLOperationType { .mutation } + + public static func mock() -> MockMutation where SelectionSet == MockSelectionSet { + MockMutation() + } +} + +open class MockSubscription: MockOperation, GraphQLSubscription { + + public override class var operationType: GraphQLOperationType { .subscription } + + public static func mock() -> MockSubscription where SelectionSet == MockSelectionSet { + MockSubscription() + } +} + +// MARK: - MockSelectionSets + +@dynamicMemberLookup +open class AbstractMockSelectionSet: RootSelectionSet, Hashable { + public typealias Schema = S + public typealias Fragments = F + + open class var __selections: [Selection] { [] } + open class var __parentType: ParentType { Object.mock } + + public var __data: DataDict = .empty() + + public required init(_dataDict: DataDict) { + self.__data = _dataDict + } + + public subscript(dynamicMember key: String) -> T? { + __data[key] + } + + public subscript(dynamicMember key: String) -> T? { + __data[key] + } + + public static func == (lhs: MockSelectionSet, rhs: MockSelectionSet) -> Bool { + lhs.__data == rhs.__data + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(__data) + } +} + +public typealias MockSelectionSet = AbstractMockSelectionSet + +open class MockFragment: MockSelectionSet, Fragment { + public typealias Schema = MockSchemaMetadata + + open class var fragmentDefinition: StaticString { "" } +} + +open class MockTypeCase: MockSelectionSet, InlineFragment { + public typealias RootEntityType = MockSelectionSet +} + +open class ConcreteMockTypeCase: MockSelectionSet, InlineFragment { + public typealias RootEntityType = T +} + +extension DataDict { + public static func empty() -> DataDict { + DataDict(data: [:], fulfilledFragments: []) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift new file mode 100644 index 000000000..115acf7b3 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift @@ -0,0 +1,111 @@ +import Apollo +import ApolloAPI + +extension Object { + public static let mock = Object(typename: "Mock", implementedInterfaces: []) +} + +public class MockSchemaMetadata: SchemaMetadata { + public init() { } + + public static var _configuration: SchemaConfiguration.Type = SchemaConfiguration.self + public static var configuration: ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self + + private static let testObserver = TestObserver() { _ in + stub_objectTypeForTypeName = nil + stub_cacheKeyInfoForType_Object = nil + } + + public static var stub_objectTypeForTypeName: ((String) -> Object?)? { + didSet { + if stub_objectTypeForTypeName != nil { testObserver.start() } + } + } + + public static var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? { + get { + _configuration.stub_cacheKeyInfoForType_Object + } + set { + _configuration.stub_cacheKeyInfoForType_Object = newValue + if newValue != nil { testObserver.start() } + } + } + + public static func objectType(forTypename __typename: String) -> Object? { + if let stub = stub_objectTypeForTypeName { + return stub(__typename) + } + + return Object(typename: __typename, implementedInterfaces: []) + } + + public class SchemaConfiguration: ApolloAPI.SchemaConfiguration { + static var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? + + public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { + stub_cacheKeyInfoForType_Object?(type, object) + } + } +} + + +// MARK - Mock Cache Key Providers + +public protocol MockStaticCacheKeyProvider { + static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? +} + +extension MockStaticCacheKeyProvider { + public static var resolver: (Object, ObjectData) -> CacheKeyInfo? { + cacheKeyInfo(for:object:) + } +} + +public struct IDCacheKeyProvider: MockStaticCacheKeyProvider { + public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { + try? .init(jsonValue: object["id"]) + } +} + +public struct MockCacheKeyProvider { + let id: String + + public init(id: String) { + self.id = id + } + + public func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? { + .init(id: id, uniqueKeyGroup: nil) + } +} + +// MARK: - Custom Mock Schemas + +public enum MockSchema1: SchemaMetadata { + public static var configuration: SchemaConfiguration.Type = MockSchema1Configuration.self + + public static func objectType(forTypename __typename: String) -> Object? { + Object(typename: __typename, implementedInterfaces: []) + } +} + +public enum MockSchema1Configuration: SchemaConfiguration { + public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { + CacheKeyInfo(id: "one") + } +} + +public enum MockSchema2: SchemaMetadata { + public static var configuration: SchemaConfiguration.Type = MockSchema2Configuration.self + + public static func objectType(forTypename __typename: String) -> Object? { + Object(typename: __typename, implementedInterfaces: []) + } +} + +public enum MockSchema2Configuration: SchemaConfiguration { + public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { + CacheKeyInfo(id: "two") + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift new file mode 100644 index 000000000..e70926bba --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift @@ -0,0 +1,56 @@ +import Foundation + +public class MockURLProtocol: URLProtocol { + + override class public func canInit(with request: URLRequest) -> Bool { + return true + } + + override class public func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override public func startLoading() { + guard let url = self.request.url, + let handler = RequestProvider.requestHandlers[url] else { + fatalError("No MockRequestHandler available for URL.") + } + + DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0.0...0.25)) { + defer { + RequestProvider.requestHandlers.removeValue(forKey: url) + } + + do { + let result = try handler(self.request) + + switch result { + case let .success((response, data)): + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + + if let data = data { + self.client?.urlProtocol(self, didLoad: data) + } + + self.client?.urlProtocolDidFinishLoading(self) + case let .failure(error): + self.client?.urlProtocol(self, didFailWithError: error) + } + + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + } + + override public func stopLoading() { + } + +} + +public protocol MockRequestProvider { + typealias MockRequestHandler = ((URLRequest) throws -> Result<(HTTPURLResponse, Data?), Error>) + + // Dictionary of mock request handlers where the `key` is the URL of the request. + static var requestHandlers: [URL: MockRequestHandler] { get set } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift new file mode 100644 index 000000000..3cff26929 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift @@ -0,0 +1,75 @@ +import Foundation +import Apollo +import ApolloAPI + +public final class MockURLSessionClient: URLSessionClient { + + @Atomic public var lastRequest: URLRequest? + @Atomic public var requestCount = 0 + + public var jsonData: JSONObject? + public var data: Data? + var responseData: Data? { + if let data = data { return data } + if let jsonData = jsonData { + return try! JSONSerializationFormat.serialize(value: jsonData) + } + return nil + } + public var response: HTTPURLResponse? + public var error: Error? + + private let callbackQueue: DispatchQueue + + public init(callbackQueue: DispatchQueue? = nil, response: HTTPURLResponse? = nil, data: Data? = nil) { + self.callbackQueue = callbackQueue ?? .main + self.response = response + self.data = data + } + + public override func sendRequest(_ request: URLRequest, + rawTaskCompletionHandler: URLSessionClient.RawCompletion? = nil, + completion: @escaping URLSessionClient.Completion) -> URLSessionTask { + self.$lastRequest.mutate { $0 = request } + self.$requestCount.increment() + + // Capture data, response, and error instead of self to ensure we complete with the current state + // even if it is changed before the block runs. + callbackQueue.async { [responseData, response, error] in + rawTaskCompletionHandler?(responseData, response, error) + + if let error = error { + completion(.failure(error)) + } else { + guard let data = responseData else { + completion(.failure(URLSessionClientError.dataForRequestNotFound(request: request))) + return + } + + guard let response = response else { + completion(.failure(URLSessionClientError.noHTTPResponse(request: request))) + return + } + + completion(.success((data, response))) + } + } + + let mockTaskType: URLSessionDataTaskMockProtocol.Type = URLSessionDataTaskMock.self + let mockTask = mockTaskType.init() as! URLSessionDataTaskMock + return mockTask + } +} + +protocol URLSessionDataTaskMockProtocol { + init() +} + +private final class URLSessionDataTaskMock: URLSessionDataTask, URLSessionDataTaskMockProtocol { + + override func resume() { + // No-op + } + + override func cancel() {} +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift new file mode 100644 index 000000000..2fa5b851b --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift @@ -0,0 +1,41 @@ +import Foundation +import ApolloWebSocket + +public class MockWebSocket: WebSocketClient { + + public var request: URLRequest + public var callbackQueue: DispatchQueue = DispatchQueue.main + public var delegate: WebSocketClientDelegate? = nil + public var isConnected: Bool = false + + public required init(request: URLRequest, protocol: WebSocket.WSProtocol) { + self.request = request + + self.request.setValue(`protocol`.description, forHTTPHeaderField: "Sec-WebSocket-Protocol") + } + + open func reportDidConnect() { + callbackQueue.async { + self.delegate?.websocketDidConnect(socket: self) + } + } + + open func write(string: String) { + callbackQueue.async { + self.delegate?.websocketDidReceiveMessage(socket: self, text: string) + } + } + + open func write(ping: Data, completion: (() -> ())?) { + } + + public func disconnect(forceTimeout: TimeInterval?) { + } + + public func connect() { + } +} + +public class ProxyableMockWebSocket: MockWebSocket, SOCKSProxyable { + public var enableSOCKSProxy: Bool = false +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift new file mode 100644 index 000000000..4c05c9760 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift @@ -0,0 +1,18 @@ +import Foundation +import ApolloWebSocket + +public class MockWebSocketDelegate: WebSocketClientDelegate { + public var didReceiveMessage: ((String) -> Void)? + + public init() {} + + public func websocketDidConnect(socket: WebSocketClient) {} + + public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {} + + public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + didReceiveMessage?(text) + } + + public func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt new file mode 100644 index 000000000..651cda1a9 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt @@ -0,0 +1 @@ +Alpha file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt new file mode 100644 index 000000000..7cc0a5791 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt @@ -0,0 +1 @@ +Bravo file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt new file mode 100644 index 000000000..3adae37d8 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt @@ -0,0 +1 @@ +Charlie file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift new file mode 100644 index 000000000..55bebd166 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift @@ -0,0 +1,33 @@ +import Foundation +import Apollo +import ApolloSQLite + +public class SQLiteTestCacheProvider: TestCacheProvider { + /// Execute a test block rather than return a cache synchronously, since cache setup may be + /// asynchronous at some point. + public static func withCache(initialRecords: RecordSet? = nil, fileURL: URL? = nil, execute test: (NormalizedCache) throws -> ()) throws { + let fileURL = fileURL ?? temporarySQLiteFileURL() + let cache = try! SQLiteNormalizedCache(fileURL: fileURL) + if let initialRecords = initialRecords { + _ = try cache.merge(records: initialRecords) + } + try test(cache) + } + + public static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) { + let fileURL = temporarySQLiteFileURL() + let cache = try! SQLiteNormalizedCache(fileURL: fileURL) + completionHandler(.success((cache, nil))) + } + + public static func temporarySQLiteFileURL() -> URL { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) + + // Create a folder with a random UUID to hold the SQLite file, since creating them in the + // same folder this close together will cause DB locks when you try to delete between tests. + let folder = temporaryDirectoryURL.appendingPathComponent(UUID().uuidString) + try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) + + return folder.appendingPathComponent("db.sqlite3") + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift new file mode 100644 index 000000000..6b6c8d3c8 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension String { + func crlfFormattedData() -> Data { + return replacingOccurrences(of: "\n\n", with: "\r\n\r\n").data(using: .utf8)! + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift new file mode 100644 index 000000000..8341e89ad --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift @@ -0,0 +1,59 @@ +import XCTest +import Apollo + +public typealias TearDownHandler = () throws -> () +public typealias TestDependency = (Resource, TearDownHandler?) + +public protocol TestCacheProvider: AnyObject { + static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) +} + +public class InMemoryTestCacheProvider: TestCacheProvider { + public static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) { + let cache = InMemoryNormalizedCache() + completionHandler(.success((cache, nil))) + } +} + +public protocol CacheDependentTesting { + var cacheType: TestCacheProvider.Type { get } + var cache: NormalizedCache! { get } +} + +extension CacheDependentTesting where Self: XCTestCase { + public func makeNormalizedCache() throws -> NormalizedCache { + var result: Result = .failure(XCTestError(.timeoutWhileWaiting)) + + let expectation = XCTestExpectation(description: "Initialized normalized cache") + + cacheType.makeNormalizedCache() { [weak self] testDependencyResult in + guard let self = self else { return } + + result = testDependencyResult.map { testDependency in + let (cache, tearDownHandler) = testDependency + + if let tearDownHandler = tearDownHandler { + self.addTeardownBlock { + do { + try tearDownHandler() + } catch { + self.record(error) + } + } + } + + return cache + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + + return try result.get() + } + + public func mergeRecordsIntoCache(_ records: RecordSet) { + _ = try! cache.merge(records: records) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift new file mode 100644 index 000000000..7bf8e579e --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift @@ -0,0 +1,14 @@ +import Foundation + +public struct TestError: Error, CustomDebugStringConvertible { + let message: String? + + public init(_ message: String? = nil) { + self.message = message + } + + public var debugDescription: String { + message ?? "TestError" + } + +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift new file mode 100644 index 000000000..f675744a3 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift @@ -0,0 +1,40 @@ +// +// TestFileHelper.swift +// ApolloTests +// +// Created by Ellen Shapiro on 3/18/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +public struct TestFileHelper { + + public static func testParentFolder(for file: StaticString = #file) -> URL { + let fileAsString = file.withUTF8Buffer { + String(decoding: $0, as: UTF8.self) + } + let url = URL(fileURLWithPath: fileAsString) + return url.deletingLastPathComponent() + } + + public static func uploadServerFolder(from file: StaticString = #file) -> URL { + self.testParentFolder(for: file) + .deletingLastPathComponent() // test root + .deletingLastPathComponent() // source root + .appendingPathComponent("SimpleUploadServer") + } + + public static func uploadsFolder(from file: StaticString = #file) -> URL { + self.uploadServerFolder(from: file) + .appendingPathComponent("uploads") + } + + public static func fileURLForFile(named name: String, extension fileExtension: String) -> URL { + return self.testParentFolder() + .appendingPathComponent("Resources") + .appendingPathComponent(name) + .appendingPathExtension(fileExtension) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift new file mode 100644 index 000000000..083346d4f --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift @@ -0,0 +1,144 @@ +import Foundation +import XCTest + +/// A test helper object that manages creation and deletion of files in a temporary directory +/// that ensures test isolation. +/// +/// **Creating a `TestIsolatedFileManager` sets the current working directory for the +/// current process to it's `directoryURL`.** After the test finishes, the current working +/// directory is reset to its previous value. +/// +/// All files and directories created by this class will be automatically deleted upon test +/// completion prior to the test case's `tearDown()` function being called. +/// +/// You can create a file manager from within a specific unit test with the +/// `testIsolatedFileManager()` function on `XCTestCase`. +public class TestIsolatedFileManager { + + public let directoryURL: URL + public let fileManager: FileManager + private let previousWorkingDirectory: String + + /// The paths for the files written to by the ``ApolloFileManager``. + public private(set) var writtenFiles: Set = [] + + fileprivate init(directoryURL: URL, fileManager: FileManager) throws { + self.directoryURL = directoryURL + self.fileManager = fileManager + + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) + previousWorkingDirectory = fileManager.currentDirectoryPath + fileManager.changeCurrentDirectoryPath(directoryURL.path) + } + + func cleanUp() throws { + fileManager.changeCurrentDirectoryPath(previousWorkingDirectory) + try fileManager.removeItem(at: directoryURL) + } + + /// Creates a file in the test directory. + /// + /// - Parameters: + /// - data: File content + /// - filename: Target name of the file. This should not include any path information + /// + /// - Returns: + /// - The full path of the created file. + @discardableResult + public func createFile( + containing data: Data, + named fileName: String, + inDirectory subDirectory: String? = nil + ) throws -> String { + let fileDirectoryURL: URL + if let subDirectory { + fileDirectoryURL = directoryURL.appendingPathComponent(subDirectory, isDirectory: true) + try fileManager.createDirectory(at: fileDirectoryURL, withIntermediateDirectories: true) + } else { + fileDirectoryURL = directoryURL + } + + let filePath: String = fileDirectoryURL + .resolvingSymlinksInPath() + .appendingPathComponent(fileName, isDirectory: false).path + + guard fileManager.createFile(atPath: filePath, contents: data) else { + throw Error.cannotCreateFile(at: filePath) + } + + writtenFiles.insert(filePath) + return filePath + } + + @discardableResult + public func createFile( + body: @autoclosure () -> String, + named fileName: String, + inDirectory directory: String? = nil + ) throws -> String { + let bodyString = body() + guard let data = bodyString.data(using: .utf8) else { + throw Error.cannotEncodeFileData(from: bodyString) + } + + return try createFile( + containing: data, + named: fileName, + inDirectory: directory + ) + } + + public enum Error: Swift.Error { + case cannotCreateFile(at: String) + case cannotEncodeFileData(from: String) + + public var errorDescription: String { + switch self { + case .cannotCreateFile(let path): + return "Cannot create file at \(path)" + case .cannotEncodeFileData(let body): + return "Cannot encode provided body string into UTF-8 data. Body:\n\(body)" + } + } + } + +} + +public extension XCTestCase { + + /// Creates a `TestIsolatedFileManager` for the current test. + /// + /// **Creating a `TestIsolatedFileManager` sets the current working directory for the + /// current process to it's `directoryURL`.** After the test finishes, the current working + /// directory is reset to its previous value. + func testIsolatedFileManager( + with fileManager: FileManager = .default + ) throws -> TestIsolatedFileManager { + let manager = try TestIsolatedFileManager( + directoryURL: computeTestTempDirectoryURL(), + fileManager: fileManager + ) + + addTeardownBlock { + try manager.cleanUp() + } + + return manager + } + + private func computeTestTempDirectoryURL() -> URL { + let directoryURL: URL + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, *) { + directoryURL = URL(filePath: NSTemporaryDirectory(), directoryHint: .isDirectory) + } else { + directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + } + + return directoryURL + .appendingPathComponent("ApolloTests") + .appendingPathComponent(name + .trimmingCharacters(in: CharacterSet(charactersIn: "-[]")) + .replacingOccurrences(of: " ", with: "_") + ) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift new file mode 100644 index 000000000..437ae83e1 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift @@ -0,0 +1,43 @@ +import Apollo +import XCTest + +public class TestObserver: NSObject, XCTestObservation { + + private let onFinish: (XCTestCase) -> Void + + @Atomic private var isStarted: Bool = false + let stopAfterEachTest: Bool + + public init( + startOnInit: Bool = true, + stopAfterEachTest: Bool = true, + onFinish: @escaping ((XCTestCase) -> Void) + ) { + self.stopAfterEachTest = stopAfterEachTest + self.onFinish = onFinish + super.init() + + if startOnInit { start() } + } + + public func start() { + guard !isStarted else { return } + $isStarted.mutate { + XCTestObservationCenter.shared.addTestObserver(self) + $0 = true + } + } + + public func stop() { + guard isStarted else { return } + $isStarted.mutate { + XCTestObservationCenter.shared.removeTestObserver(self) + $0 = false + } + } + + public func testCaseDidFinish(_ testCase: XCTestCase) { + onFinish(testCase) + if stopAfterEachTest { stop() } + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift new file mode 100644 index 000000000..4b1a88605 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift @@ -0,0 +1,19 @@ +import Foundation + +/// URLs used in testing +public enum TestURL { + case mockServer + case mockPort8080 + + public var url: URL { + let urlString: String + switch self { + case .mockServer: + urlString = "http://localhost/dummy_url" + case .mockPort8080: + urlString = "http://localhost:8080/graphql" + } + + return URL(string: urlString)! + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift new file mode 100644 index 000000000..258ecf6be --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift @@ -0,0 +1,159 @@ +import XCTest +import Apollo + +public func XCTAssertEqual(_ expression1: @autoclosure () throws -> [T : U]?, _ expression2: @autoclosure () throws -> [T : U]?, file: StaticString = #filePath, line: UInt = #line) rethrows { + let optionalValue1 = try expression1() + let optionalValue2 = try expression2() + + let message = { + "(\"\(String(describing: optionalValue1))\") is not equal to (\"\(String(describing: optionalValue2))\")" + } + + switch (optionalValue1, optionalValue2) { + case (.none, .none): + break + case let (value1 as NSDictionary, value2 as NSDictionary): + XCTAssertEqual(value1, value2, message(), file: file, line: line) + default: + XCTFail(message(), file: file, line: line) + } +} + +public func XCTAssertEqualUnordered(_ expression1: @autoclosure () throws -> C1, _ expression2: @autoclosure () throws -> C2, file: StaticString = #filePath, line: UInt = #line) rethrows where Element: Hashable, C1.Element == Element, C2.Element == Element { + let collection1 = try expression1() + let collection2 = try expression2() + + // Convert to sets to ignore ordering and only check whether all elements are accounted for, + // but also check count to detect duplicates. + XCTAssertEqual(collection1.count, collection2.count, file: file, line: line) + XCTAssertEqual(Set(collection1), Set(collection2), file: file, line: line) +} + +public func XCTAssertMatch(_ valueExpression: @autoclosure () throws -> Pattern.Base, _ patternExpression: @autoclosure () throws -> Pattern, file: StaticString = #filePath, line: UInt = #line) rethrows { + let value = try valueExpression() + let pattern = try patternExpression() + + let message = { + "(\"\(value)\") does not match (\"\(pattern)\")" + } + + if case pattern = value { return } + + XCTFail(message(), file: file, line: line) +} + +// We need overloaded versions instead of relying on default arguments +// due to https://bugs.swift.org/browse/SR-1534 + +public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { + try XCTAssertSuccessResult(expression(), file: file, line: line, {_ in }) +} + +public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ successHandler: (_ value: Success) throws -> Void) rethrows { + let result = try expression() + + switch result { + case .success(let value): + try successHandler(value) + case .failure(let error): + XCTFail("Expected success, but result was an error: \(String(describing: error))", file: file, line: line) + } +} + +public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { + try XCTAssertFailureResult(expression(), file: file, line: line, {_ in }) +} + +public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: Error) throws -> Void) rethrows { + let result = try expression() + + switch result { + case .success(let success): + XCTFail("Expected failure, but result was successful: \(String(describing: success))", file: file, line: line) + case .failure(let error): + try errorHandler(error) + } +} + +/// Checks that the condition is eventually true with a given timeout (default 1 second). +/// +/// This assertion runs the run loop for 0.01 second after each time it checks the condition until +/// the condition is true or the timeout is reached. +/// +/// - Parameters: +/// - test: An autoclosure for the condition to test for truthiness. +/// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. +/// - message: A message to send on failure. +public func XCTAssertTrueEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { + let runLoop = RunLoop.current + let timeoutDate = Date(timeIntervalSinceNow: timeout) + repeat { + if test() { + return + } + runLoop.run(until: Date(timeIntervalSinceNow: 0.01)) + } while Date().compare(timeoutDate) == .orderedAscending + + XCTFail(message, file: file, line: line) +} + +/// Checks that the condition is eventually false with a given timeout (default 1 second). +/// +/// This assertion runs the run loop for 0.01 second after each time it checks the condition until +/// the condition is false or the timeout is reached. +/// +/// - Parameters: +/// - test: An autoclosure for the condition to test for falsiness. +/// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. +/// - message: A message to send on failure. +public func XCTAssertFalseEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { + XCTAssertTrueEventually(!test(), timeout: timeout, message: message, file: file, line: line) +} + +/// Downcast an expression to a specified type. +/// +/// Generates a failure when the downcast doesn't succeed. +/// +/// - Parameters: +/// - expression: An expression to downcast to `ExpectedType`. +/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. +/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. +/// - Returns: A value of type `ExpectedType`, the result of evaluating and downcasting the given `expression`. +/// - Throws: An error when the downcast doesn't succeed. It will also rethrow any error thrown while evaluating the given expression. +public func XCTDowncast(_ expression: @autoclosure () throws -> AnyObject, to type: ExpectedType.Type, file: StaticString = #filePath, line: UInt = #line) throws -> ExpectedType { + let object = try expression() + + guard let expected = object as? ExpectedType else { + throw XCTFailure("Expected type to be \(ExpectedType.self), but found \(Swift.type(of: object))", file: file, line: line) + } + + return expected +} + +/// An error which causes the current test to cease executing and fail when it is thrown. +/// Similar to `XCTSkip`, but without marking the test as skipped. +public struct XCTFailure: Error, CustomNSError { + + public init(_ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + XCTFail(message(), file: file, line: line) + } + + /// The domain of the error. + public static let errorDomain = XCTestErrorDomain + + /// The error code within the given domain. + public let errorCode: Int = 0 + + /// The user-info dictionary. + public let errorUserInfo: [String : Any] = [ + // Make sure the thrown error doesn't show up as a test failure, because we already record + // a more detailed failure (with the right source location) ourselves. + "XCTestErrorUserInfoKeyShouldIgnore": true + ] +} + +public extension Optional { + func xctUnwrapped(file: StaticString = #filePath, line: UInt = #line) throws -> Wrapped { + try XCTUnwrap(self, file: file, line: line) + } +} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift new file mode 100644 index 000000000..38ddf06c9 --- /dev/null +++ b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift @@ -0,0 +1,77 @@ +import XCTest + +extension XCTestExpectation { + /// Private API for accessing the number of times an expectation has been fulfilled. + public var numberOfFulfillments: Int { + value(forKey: "numberOfFulfillments") as! Int + } +} + +public extension XCTestCase { + /// Record the specified`error` as an `XCTIssue`. + func record(_ error: Error, compactDescription: String? = nil, file: StaticString = #filePath, line: UInt = #line) { + var issue = XCTIssue(type: .thrownError, compactDescription: compactDescription ?? String(describing: error)) + + issue.associatedError = error + + let location = XCTSourceCodeLocation(filePath: file, lineNumber: line) + issue.sourceCodeContext = XCTSourceCodeContext(location: location) + + record(issue) + } + + /// Invoke a throwing closure, and record any thrown errors without rethrowing. This is useful if you need to run code that may throw + /// in a place where throwing isn't allowed, like `measure` blocks. + func whileRecordingErrors(file: StaticString = #file, line: UInt = #line, _ perform: () throws -> Void) { + do { + try perform() + } catch { + // Respect XCTestErrorUserInfoKeyShouldIgnore key that is used by XCTUnwrap, XCTSkip, and our own XCTFailure. + let shouldIgnore = (((error as NSError).userInfo["XCTestErrorUserInfoKeyShouldIgnore"] as? Bool) == true) + if !shouldIgnore { + record(error, file: file, line: line) + } + } + } + + /// Wrapper around `XCTContext.runActivity` to allow for future extension. + func runActivity(_ name: String, perform: (XCTActivity) throws -> Result) rethrows -> Result { + return try XCTContext.runActivity(named: name, block: perform) + } +} + +import Apollo +import ApolloAPI + +public extension XCTestCase { + /// Make an `AsyncResultObserver` for receiving results of the specified GraphQL operation. + func makeResultObserver(for operation: Operation, file: StaticString = #filePath, line: UInt = #line) -> AsyncResultObserver, Error> { + return AsyncResultObserver(testCase: self, file: file, line: line) + } +} + +public protocol StoreLoading { + static var defaultWaitTimeout: TimeInterval { get } + var store: ApolloStore! { get } +} + +public extension StoreLoading { + static var defaultWaitTimeout: TimeInterval { 1.0 } +} + +extension StoreLoading where Self: XCTestCase { + public func loadFromStore( + operation: Operation, + file: StaticString = #filePath, + line: UInt = #line, + resultHandler: @escaping AsyncResultObserver, Error>.ResultHandler + ) { + let resultObserver = makeResultObserver(for: operation, file: file, line: line) + + let expectation = resultObserver.expectation(description: "Loaded query from store", file: file, line: line, resultHandler: resultHandler) + + store.load(operation, resultHandler: resultObserver.handler) + + wait(for: [expectation], timeout: Self.defaultWaitTimeout) + } +} diff --git a/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift b/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift index 8d46592f8..24c022ca9 100644 --- a/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift +++ b/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift @@ -2,11 +2,5 @@ import XCTest @testable import apollo_ios_pagination final class apollo_ios_paginationTests: XCTestCase { - func testExample() throws { - // XCTest Documenation - // https://developer.apple.com/documentation/xctest - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } } From d39fa7f8eb4add5d30534b5c3e68d7cb827a20dd Mon Sep 17 00:00:00 2001 From: Hesham Salman Date: Mon, 9 Oct 2023 17:34:03 -0400 Subject: [PATCH 2/3] Update iOS minimum --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 04b2815a3..530bb921e 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "apollo-ios-pagination", platforms: [ - .iOS(.v12), + .iOS(.v13), .macOS(.v10_15), .tvOS(.v12), .watchOS(.v5) From a9d7228786a4dee64b762b23f97a73f774fec874 Mon Sep 17 00:00:00 2001 From: Hesham Salman Date: Mon, 9 Oct 2023 17:34:33 -0400 Subject: [PATCH 3/3] Remove tests --- Package.swift | 8 - .../ApolloInternalTestHelpers.h | 9 - .../AsyncResultObserver.swift | 70 -------- .../ApolloInternalTestHelpers/Info.plist | 24 --- .../InterceptorTester.swift | 81 --------- .../ApolloInternalTestHelpers/Matchable.swift | 23 --- .../MockApolloStore.swift | 32 ---- .../MockGraphQLServer.swift | 107 ------------ .../MockHTTPRequest.swift | 15 -- .../MockHTTPResponse.swift | 36 ---- .../MockInterceptorProvider.swift | 13 -- .../MockLocalCacheMutation.swift | 40 ----- .../MockNetworkTransport.swift | 97 ----------- .../MockOperation.swift | 97 ----------- .../MockSchemaMetadata.swift | 111 ------------ .../MockURLProtocol.swift | 56 ------ .../MockURLSession.swift | 75 --------- .../MockWebSocket.swift | 41 ----- .../MockWebSocketDelegate.swift | 18 -- .../ApolloInternalTestHelpers/Resources/a.txt | 1 - .../ApolloInternalTestHelpers/Resources/b.txt | 1 - .../ApolloInternalTestHelpers/Resources/c.txt | 1 - .../SQLiteTestCacheProvider.swift | 33 ---- .../String+Data.swift | 7 - .../TestCacheProvider.swift | 59 ------- .../ApolloInternalTestHelpers/TestError.swift | 14 -- .../TestFileHelper.swift | 40 ----- .../TestIsolatedFileManager.swift | 144 ---------------- .../TestObserver.swift | 43 ----- .../ApolloInternalTestHelpers/TestURLs.swift | 19 --- .../XCTAssertHelpers.swift | 159 ------------------ .../XCTestCase+Helpers.swift | 77 --------- .../apollo_ios_paginationTests.swift | 6 - 33 files changed, 1557 deletions(-) delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift delete mode 100644 Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift delete mode 100644 Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift diff --git a/Package.swift b/Package.swift index 530bb921e..d828586b8 100644 --- a/Package.swift +++ b/Package.swift @@ -27,13 +27,5 @@ let package = Package( .product(name: "ApolloAPI", package: "apollo-ios"), ] ), - .testTarget( - name: "apollo-ios-paginationTests", - dependencies: [ - "apollo-ios-pagination", - .product(name: "ApolloSQLite", package: "apollo-ios"), - .product(name: "ApolloWebSocket", package: "apollo-ios"), - ] - ), ] ) diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h deleted file mode 100644 index e780c4d71..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/ApolloInternalTestHelpers.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -//! Project version number for ApolloInternalTestHelpers. -FOUNDATION_EXPORT double ApolloInternalTestHelpersVersionNumber; - -//! Project version string for ApolloInternalTestHelpers. -FOUNDATION_EXPORT const unsigned char ApolloInternalTestHelpersVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift deleted file mode 100644 index 5938f3992..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/AsyncResultObserver.swift +++ /dev/null @@ -1,70 +0,0 @@ -import XCTest - -/// `AsyncResultObserver` is a helper class that can be used to test `Result` values received through a completion handler against one or more expectations. It is primarily useful if you expect the completion handler to be called multiple times, when receiving a fetch result from the cache and then from the server for example. -/// -/// The main benefit is that it avoids having to manually keep track of expectations and mutable closures (like `verifyResult`), which can make code hard to read and is prone to mistakes. Instead, you can use a result observer to create multiple expectations that will be automatically fulfilled in order when results are received. Often, you'll also want to run assertions against the result, which you can do by passing in an optional handler that is specific to that expectation. These handlers are throwing, which means you can use `result.get()` and `XCTUnwrap` for example. Thrown errors will automatically be recorded as failures in the test case (with the right line numbers, etc.). -/// -/// By default, expectations returned from `AsyncResultObserver` only expect to be called once, which is similar to how other built-in expectations work. Unexpected fulfillments will result in test failures. Usually this is what you want, and you add additional expectations with their own assertions if you expect further results. -/// If multiple fulfillments of a single expectation are expected however, you can use the standard `expectedFulfillmentCount` property to change that. -public class AsyncResultObserver where Failure: Error { - public typealias ResultHandler = (Result) throws -> Void - - private class AsyncResultExpectation: XCTestExpectation { - let file: StaticString - let line: UInt - let handler: ResultHandler - - init(description: String, file: StaticString = #filePath, line: UInt = #line, handler: @escaping ResultHandler) { - self.file = file - self.line = line - self.handler = handler - - super.init(description: description) - } - } - - private let testCase: XCTestCase - - // We keep track of the file and line number associated with the constructor as a fallback, in addition te keeping - // these for each expectation. That way, we can still show a failure within the context of the test in case unexpected - // results are received (which by definition do not have an associated expectation). - private let file: StaticString - private let line: UInt - - private var expectations: [AsyncResultExpectation] = [] - - public init(testCase: XCTestCase, file: StaticString = #filePath, line: UInt = #line) { - self.testCase = testCase - self.file = file - self.line = line - } - - public func expectation(description: String, file: StaticString = #filePath, line: UInt = #line, resultHandler: @escaping ResultHandler) -> XCTestExpectation { - let expectation = AsyncResultExpectation(description: description, file: file, line: line, handler: resultHandler) - expectation.assertForOverFulfill = true - - expectations.append(expectation) - - return expectation - } - - public func handler(_ result: Result) { - guard let expectation = expectations.first else { - XCTFail("Unexpected result received by handler", file: file, line: line) - return - } - - do { - try expectation.handler(result) - } catch { - testCase.record(error, file: expectation.file, line: expectation.line) - } - - expectation.fulfill() - - if expectation.numberOfFulfillments >= expectation.expectedFulfillmentCount { - expectations.removeFirst() - } - } -} - diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist deleted file mode 100644 index fbe1e6b31..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift deleted file mode 100644 index 4cdf758a0..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/InterceptorTester.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Apollo -import Foundation - -/// Use this interceptor tester to isolate a single `ApolloInterceptor` vs. having to create an -/// `InterceptorRequestChain` and end the interceptor list with `JSONResponseParsingInterceptor` -/// to get a parsed `GraphQLResult` for the standard request chain callback. -public class InterceptorTester { - let interceptor: any ApolloInterceptor - - public init(interceptor: any ApolloInterceptor) { - self.interceptor = interceptor - } - - public func intercept( - request: Apollo.HTTPRequest, - response: Apollo.HTTPResponse? = nil, - completion: @escaping (Result) -> Void - ) { - let requestChain = ResponseCaptureRequestChain({ result in - completion(result) - }) - - self.interceptor.interceptAsync( - chain: requestChain, - request: request, - response: response) { _ in } - } -} - -fileprivate class ResponseCaptureRequestChain: RequestChain { - var isCancelled: Bool = false - let completion: (Result) -> Void - - init(_ completion: @escaping (Result) -> Void) { - self.completion = completion - } - - func kickoff( - request: Apollo.HTTPRequest, - completion: @escaping (Result, Error>) -> Void - ) {} - - func proceedAsync( - request: Apollo.HTTPRequest, - response: Apollo.HTTPResponse?, - completion: @escaping (Result, Error>) -> Void - ) { - self.completion(.success(response?.rawData)) - } - - func proceedAsync( - request: HTTPRequest, - response: HTTPResponse?, - interceptor: any ApolloInterceptor, - completion: @escaping (Result, Error>) -> Void - ) { - self.completion(.success(response?.rawData)) - } - - func cancel() {} - - func retry( - request: Apollo.HTTPRequest, - completion: @escaping (Result, Error>) -> Void - ) {} - - func handleErrorAsync( - _ error: Error, - request: Apollo.HTTPRequest, - response: Apollo.HTTPResponse?, - completion: @escaping (Result, Error>) -> Void - ) { - self.completion(.failure(error)) - } - - func returnValueAsync( - for request: Apollo.HTTPRequest, - value: Apollo.GraphQLResult, - completion: @escaping (Result, Error>) -> Void - ) {} -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift deleted file mode 100644 index 3e368b524..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Matchable.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import ApolloAPI - -public protocol Matchable { - associatedtype Base - static func ~=(pattern: Self, value: Base) -> Bool -} - -extension JSONDecodingError: Matchable { - public typealias Base = Error - public static func ~=(pattern: JSONDecodingError, value: Error) -> Bool { - guard let value = value as? JSONDecodingError else { - return false - } - - switch (value, pattern) { - case (.missingValue, .missingValue), (.nullValue, .nullValue), (.wrongType, .wrongType), (.couldNotConvert, .couldNotConvert): - return true - default: - return false - } - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift deleted file mode 100644 index e5d29c87a..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockApolloStore.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Apollo -import ApolloAPI - -extension ApolloStore { - - public static func mock(cache: NormalizedCache = NoCache()) -> ApolloStore { - ApolloStore(cache: cache) - } - -} - -/// A `NormalizedCache` that does not cache any data. Used for tests that don't require testing -/// caching behavior. -public class NoCache: NormalizedCache { - - public init() { } - - public func loadRecords(forKeys keys: Set) throws -> [String : Record] { - return [:] - } - - public func merge(records: RecordSet) throws -> Set { - return Set() - } - - public func removeRecord(for key: String) throws { } - - public func removeRecords(matching pattern: String) throws { } - - public func clear() throws { } - -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift deleted file mode 100644 index bbfcd6c95..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockGraphQLServer.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Apollo -import ApolloAPI -import XCTest - -/// A `MockGraphQLServer` can be used during tests to check whether expected GraphQL requests are received, and to respond with appropriate test data for a particular request. -/// -/// You usually create a mock server in the test's `setUpWithError`, and use it to initialize a `MockNetworkTransport` that is in turn used to initialize an `ApolloClient`: -/// ``` -/// let server = MockGraphQLServer() -/// let networkTransport = MockNetworkTransport(server: server, store: store) -/// let client = ApolloClient(networkTransport: networkTransport, store: store) -/// ``` -/// A mock server should be configured to expect particular operation types, and invokes the passed in request handler when a request of that type comes in. Because the request allows access to `operation`, you can return different responses based on query variables for example: - -/// ``` -/// let serverExpectation = server.expect(HeroNameQuery.self) { request in -/// [ -/// "data": [ -/// "hero": [ -/// "name": request.operation.episode == .empire ? "Luke Skywalker" : "R2-D2", -/// "__typename": "Droid" -/// ] -/// ] -/// ] -/// } -/// ``` -/// By default, expectations returned from `MockGraphQLServer` only expect to be called once, which is similar to how other built-in expectations work. Unexpected fulfillments will result in test failures. But if multiple fulfillments are expected, you can use the standard `expectedFulfillmentCount` property to change that. For example, some of the concurrent tests expect the server to receive the same number of request as the number of invoked fetch operations, so in that case we can use: - -/// ``` -/// serverExpectation.expectedFulfillmentCount = numberOfFetches -/// ``` -public class MockGraphQLServer { - enum ServerError: Error, CustomStringConvertible { - case unexpectedRequest(String) - - public var description: String { - switch self { - case .unexpectedRequest(let requestDescription): - return "Mock GraphQL server received an unexpected request: \(requestDescription)" - } - } - } - - public typealias RequestHandler = (HTTPRequest) -> JSONObject - - private class RequestExpectation: XCTestExpectation { - let file: StaticString - let line: UInt - let handler: RequestHandler - - init(description: String, file: StaticString = #filePath, line: UInt = #line, handler: @escaping RequestHandler) { - self.file = file - self.line = line - self.handler = handler - - super.init(description: description) - } - } - - private let queue = DispatchQueue(label: "com.apollographql.MockGraphQLServer") - - public init() { } - - // Since RequestExpectation is generic over a specific GraphQLOperation, we can't store these in the dictionary - // directly. Moreover, there is no way to specify the type relationship that holds between the key and value. - // To work around this, we store values as Any and use a generic subscript as a type-safe way to access them. - private var requestExpectations: [AnyHashable: Any] = [:] - - private subscript(_ operationType: Operation.Type) -> RequestExpectation? { - get { - requestExpectations[ObjectIdentifier(operationType)] as! RequestExpectation? - } - - set { - requestExpectations[ObjectIdentifier(operationType)] = newValue - } - } - - public func expect(_ operationType: Operation.Type, file: StaticString = #filePath, line: UInt = #line, requestHandler: @escaping (HTTPRequest) -> JSONObject) -> XCTestExpectation { - return queue.sync { - let expectation = RequestExpectation(description: "Served request for \(String(describing: operationType))", file: file, line: line, handler: requestHandler) - expectation.assertForOverFulfill = true - - self[operationType] = expectation - - return expectation - } - } - - func serve(request: HTTPRequest, completionHandler: @escaping (Result) -> Void) where Operation: GraphQLOperation { - let operationType = type(of: request.operation) - - if let expectation = self[operationType] { - // Dispatch after a small random delay to spread out concurrent requests and simulate somewhat real-world conditions. - queue.asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 10...50))) { - completionHandler(.success(expectation.handler(request))) - expectation.fulfill() - } - - } else { - queue.async { - completionHandler(.failure(ServerError.unexpectedRequest(String(describing: operationType)))) - } - } - - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift deleted file mode 100644 index ebe72f26f..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPRequest.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Apollo -import ApolloAPI - -extension HTTPRequest { - public static func mock(operation: Operation) -> HTTPRequest { - return HTTPRequest( - graphQLEndpoint: TestURL.mockServer.url, - operation: operation, - contentType: "application/json", - clientName: "test-client", - clientVersion: "test-version", - additionalHeaders: [:] - ) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift deleted file mode 100644 index 9be77c244..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockHTTPResponse.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Apollo -import ApolloAPI -import Foundation - -extension HTTPResponse { - public static func mock( - statusCode: Int = 200, - headerFields: [String : String] = [:], - data: Data = Data() - ) -> HTTPResponse { - return HTTPResponse( - response: .mock( - statusCode: statusCode, - headerFields: headerFields - ), - rawData: data, - parsedResponse: nil - ) - } -} - -extension HTTPURLResponse { - public static func mock( - url: URL = TestURL.mockServer.url, - statusCode: Int = 200, - httpVersion: String? = nil, - headerFields: [String : String]? = nil - ) -> HTTPURLResponse { - return HTTPURLResponse( - url: url, - statusCode: statusCode, - httpVersion: httpVersion, - headerFields: headerFields - )! - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift deleted file mode 100644 index f3a29119a..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockInterceptorProvider.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Apollo - -public struct MockInterceptorProvider: InterceptorProvider { - let interceptors: [any ApolloInterceptor] - - public init(_ interceptors: [any ApolloInterceptor]) { - self.interceptors = interceptors - } - - public func interceptors(for operation: Operation) -> [any ApolloInterceptor] { - self.interceptors - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift deleted file mode 100644 index 534ad168e..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import ApolloAPI - -open class MockLocalCacheMutation: LocalCacheMutation { - open class var operationType: GraphQLOperationType { .query } - - public typealias Data = SelectionSet - - open var __variables: GraphQLOperation.Variables? - - public init() {} - -} - -open class MockLocalCacheMutationFromMutation: - MockLocalCacheMutation { - override open class var operationType: GraphQLOperationType { .mutation } -} - -open class MockLocalCacheMutationFromSubscription: - MockLocalCacheMutation { - override open class var operationType: GraphQLOperationType { .subscription } -} - -public protocol MockMutableRootSelectionSet: MutableRootSelectionSet -where Schema == MockSchemaMetadata {} - -public extension MockMutableRootSelectionSet { - static var __parentType: ParentType { Object.mock } - - init() { - self.init(_dataDict: DataDict( - data: [:], - fulfilledFragments: [ObjectIdentifier(Self.self)] - )) - } -} - -public protocol MockMutableInlineFragment: MutableSelectionSet, InlineFragment -where Schema == MockSchemaMetadata {} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift deleted file mode 100644 index e76e2d47b..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockNetworkTransport.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import Apollo -import ApolloAPI - -public final class MockNetworkTransport: RequestChainNetworkTransport { - public init( - server: MockGraphQLServer = MockGraphQLServer(), - store: ApolloStore, - clientName: String = "MockNetworkTransport_ClientName", - clientVersion: String = "MockNetworkTransport_ClientVersion" - ) { - super.init(interceptorProvider: TestInterceptorProvider(store: store, server: server), - endpointURL: TestURL.mockServer.url) - self.clientName = clientName - self.clientVersion = clientVersion - } - - struct TestInterceptorProvider: InterceptorProvider { - let store: ApolloStore - let server: MockGraphQLServer - - func interceptors( - for operation: Operation - ) -> [any ApolloInterceptor] where Operation: GraphQLOperation { - return [ - MaxRetryInterceptor(), - CacheReadInterceptor(store: self.store), - MockGraphQLServerInterceptor(server: server), - ResponseCodeInterceptor(), - JSONResponseParsingInterceptor(), - AutomaticPersistedQueryInterceptor(), - CacheWriteInterceptor(store: self.store), - ] - } - } -} - -private final class MockTask: Cancellable { - func cancel() { - // no-op - } -} - -private class MockGraphQLServerInterceptor: ApolloInterceptor { - let server: MockGraphQLServer - - public var id: String = UUID().uuidString - - init(server: MockGraphQLServer) { - self.server = server - } - - public func interceptAsync(chain: RequestChain, request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) where Operation: GraphQLOperation { - server.serve(request: request) { result in - let httpResponse = HTTPURLResponse(url: TestURL.mockServer.url, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - switch result { - case .failure(let error): - chain.handleErrorAsync(error, - request: request, - response: response, - completion: completion) - case .success(let body): - let data = try! JSONSerializationFormat.serialize(value: body) - let response = HTTPResponse(response: httpResponse, - rawData: data, - parsedResponse: nil) - chain.proceedAsync(request: request, - response: response, - interceptor: self, - completion: completion) - } - } - } -} - -public class MockWebSocketTransport: NetworkTransport { - public var clientName, clientVersion: String - - public init(clientName: String, clientVersion: String) { - self.clientName = clientName - self.clientVersion = clientVersion - } - - public func send( - operation: Operation, - cachePolicy: CachePolicy, - contextIdentifier: UUID?, - callbackQueue: DispatchQueue, - completionHandler: @escaping (Result, Error>) -> Void - ) -> Cancellable where Operation : GraphQLOperation { - return MockTask() - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift deleted file mode 100644 index 5da746bf9..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockOperation.swift +++ /dev/null @@ -1,97 +0,0 @@ -import ApolloAPI - -open class MockOperation: GraphQLOperation { - public typealias Data = SelectionSet - - open class var operationType: GraphQLOperationType { .query } - - open class var operationName: String { "MockOperationName" } - - open class var operationDocument: OperationDocument { - .init(definition: .init("Mock Operation Definition")) - } - - open var __variables: Variables? - - public init() {} - -} - -open class MockQuery: MockOperation, GraphQLQuery { - public static func mock() -> MockQuery where SelectionSet == MockSelectionSet { - MockQuery() - } -} - -open class MockMutation: MockOperation, GraphQLMutation { - - public override class var operationType: GraphQLOperationType { .mutation } - - public static func mock() -> MockMutation where SelectionSet == MockSelectionSet { - MockMutation() - } -} - -open class MockSubscription: MockOperation, GraphQLSubscription { - - public override class var operationType: GraphQLOperationType { .subscription } - - public static func mock() -> MockSubscription where SelectionSet == MockSelectionSet { - MockSubscription() - } -} - -// MARK: - MockSelectionSets - -@dynamicMemberLookup -open class AbstractMockSelectionSet: RootSelectionSet, Hashable { - public typealias Schema = S - public typealias Fragments = F - - open class var __selections: [Selection] { [] } - open class var __parentType: ParentType { Object.mock } - - public var __data: DataDict = .empty() - - public required init(_dataDict: DataDict) { - self.__data = _dataDict - } - - public subscript(dynamicMember key: String) -> T? { - __data[key] - } - - public subscript(dynamicMember key: String) -> T? { - __data[key] - } - - public static func == (lhs: MockSelectionSet, rhs: MockSelectionSet) -> Bool { - lhs.__data == rhs.__data - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(__data) - } -} - -public typealias MockSelectionSet = AbstractMockSelectionSet - -open class MockFragment: MockSelectionSet, Fragment { - public typealias Schema = MockSchemaMetadata - - open class var fragmentDefinition: StaticString { "" } -} - -open class MockTypeCase: MockSelectionSet, InlineFragment { - public typealias RootEntityType = MockSelectionSet -} - -open class ConcreteMockTypeCase: MockSelectionSet, InlineFragment { - public typealias RootEntityType = T -} - -extension DataDict { - public static func empty() -> DataDict { - DataDict(data: [:], fulfilledFragments: []) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift deleted file mode 100644 index 115acf7b3..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockSchemaMetadata.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Apollo -import ApolloAPI - -extension Object { - public static let mock = Object(typename: "Mock", implementedInterfaces: []) -} - -public class MockSchemaMetadata: SchemaMetadata { - public init() { } - - public static var _configuration: SchemaConfiguration.Type = SchemaConfiguration.self - public static var configuration: ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self - - private static let testObserver = TestObserver() { _ in - stub_objectTypeForTypeName = nil - stub_cacheKeyInfoForType_Object = nil - } - - public static var stub_objectTypeForTypeName: ((String) -> Object?)? { - didSet { - if stub_objectTypeForTypeName != nil { testObserver.start() } - } - } - - public static var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? { - get { - _configuration.stub_cacheKeyInfoForType_Object - } - set { - _configuration.stub_cacheKeyInfoForType_Object = newValue - if newValue != nil { testObserver.start() } - } - } - - public static func objectType(forTypename __typename: String) -> Object? { - if let stub = stub_objectTypeForTypeName { - return stub(__typename) - } - - return Object(typename: __typename, implementedInterfaces: []) - } - - public class SchemaConfiguration: ApolloAPI.SchemaConfiguration { - static var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? - - public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { - stub_cacheKeyInfoForType_Object?(type, object) - } - } -} - - -// MARK - Mock Cache Key Providers - -public protocol MockStaticCacheKeyProvider { - static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? -} - -extension MockStaticCacheKeyProvider { - public static var resolver: (Object, ObjectData) -> CacheKeyInfo? { - cacheKeyInfo(for:object:) - } -} - -public struct IDCacheKeyProvider: MockStaticCacheKeyProvider { - public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { - try? .init(jsonValue: object["id"]) - } -} - -public struct MockCacheKeyProvider { - let id: String - - public init(id: String) { - self.id = id - } - - public func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? { - .init(id: id, uniqueKeyGroup: nil) - } -} - -// MARK: - Custom Mock Schemas - -public enum MockSchema1: SchemaMetadata { - public static var configuration: SchemaConfiguration.Type = MockSchema1Configuration.self - - public static func objectType(forTypename __typename: String) -> Object? { - Object(typename: __typename, implementedInterfaces: []) - } -} - -public enum MockSchema1Configuration: SchemaConfiguration { - public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { - CacheKeyInfo(id: "one") - } -} - -public enum MockSchema2: SchemaMetadata { - public static var configuration: SchemaConfiguration.Type = MockSchema2Configuration.self - - public static func objectType(forTypename __typename: String) -> Object? { - Object(typename: __typename, implementedInterfaces: []) - } -} - -public enum MockSchema2Configuration: SchemaConfiguration { - public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { - CacheKeyInfo(id: "two") - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift deleted file mode 100644 index e70926bba..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLProtocol.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation - -public class MockURLProtocol: URLProtocol { - - override class public func canInit(with request: URLRequest) -> Bool { - return true - } - - override class public func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - - override public func startLoading() { - guard let url = self.request.url, - let handler = RequestProvider.requestHandlers[url] else { - fatalError("No MockRequestHandler available for URL.") - } - - DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0.0...0.25)) { - defer { - RequestProvider.requestHandlers.removeValue(forKey: url) - } - - do { - let result = try handler(self.request) - - switch result { - case let .success((response, data)): - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - - if let data = data { - self.client?.urlProtocol(self, didLoad: data) - } - - self.client?.urlProtocolDidFinishLoading(self) - case let .failure(error): - self.client?.urlProtocol(self, didFailWithError: error) - } - - } catch { - self.client?.urlProtocol(self, didFailWithError: error) - } - } - } - - override public func stopLoading() { - } - -} - -public protocol MockRequestProvider { - typealias MockRequestHandler = ((URLRequest) throws -> Result<(HTTPURLResponse, Data?), Error>) - - // Dictionary of mock request handlers where the `key` is the URL of the request. - static var requestHandlers: [URL: MockRequestHandler] { get set } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift deleted file mode 100644 index 3cff26929..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockURLSession.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import Apollo -import ApolloAPI - -public final class MockURLSessionClient: URLSessionClient { - - @Atomic public var lastRequest: URLRequest? - @Atomic public var requestCount = 0 - - public var jsonData: JSONObject? - public var data: Data? - var responseData: Data? { - if let data = data { return data } - if let jsonData = jsonData { - return try! JSONSerializationFormat.serialize(value: jsonData) - } - return nil - } - public var response: HTTPURLResponse? - public var error: Error? - - private let callbackQueue: DispatchQueue - - public init(callbackQueue: DispatchQueue? = nil, response: HTTPURLResponse? = nil, data: Data? = nil) { - self.callbackQueue = callbackQueue ?? .main - self.response = response - self.data = data - } - - public override func sendRequest(_ request: URLRequest, - rawTaskCompletionHandler: URLSessionClient.RawCompletion? = nil, - completion: @escaping URLSessionClient.Completion) -> URLSessionTask { - self.$lastRequest.mutate { $0 = request } - self.$requestCount.increment() - - // Capture data, response, and error instead of self to ensure we complete with the current state - // even if it is changed before the block runs. - callbackQueue.async { [responseData, response, error] in - rawTaskCompletionHandler?(responseData, response, error) - - if let error = error { - completion(.failure(error)) - } else { - guard let data = responseData else { - completion(.failure(URLSessionClientError.dataForRequestNotFound(request: request))) - return - } - - guard let response = response else { - completion(.failure(URLSessionClientError.noHTTPResponse(request: request))) - return - } - - completion(.success((data, response))) - } - } - - let mockTaskType: URLSessionDataTaskMockProtocol.Type = URLSessionDataTaskMock.self - let mockTask = mockTaskType.init() as! URLSessionDataTaskMock - return mockTask - } -} - -protocol URLSessionDataTaskMockProtocol { - init() -} - -private final class URLSessionDataTaskMock: URLSessionDataTask, URLSessionDataTaskMockProtocol { - - override func resume() { - // No-op - } - - override func cancel() {} -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift deleted file mode 100644 index 2fa5b851b..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocket.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation -import ApolloWebSocket - -public class MockWebSocket: WebSocketClient { - - public var request: URLRequest - public var callbackQueue: DispatchQueue = DispatchQueue.main - public var delegate: WebSocketClientDelegate? = nil - public var isConnected: Bool = false - - public required init(request: URLRequest, protocol: WebSocket.WSProtocol) { - self.request = request - - self.request.setValue(`protocol`.description, forHTTPHeaderField: "Sec-WebSocket-Protocol") - } - - open func reportDidConnect() { - callbackQueue.async { - self.delegate?.websocketDidConnect(socket: self) - } - } - - open func write(string: String) { - callbackQueue.async { - self.delegate?.websocketDidReceiveMessage(socket: self, text: string) - } - } - - open func write(ping: Data, completion: (() -> ())?) { - } - - public func disconnect(forceTimeout: TimeInterval?) { - } - - public func connect() { - } -} - -public class ProxyableMockWebSocket: MockWebSocket, SOCKSProxyable { - public var enableSOCKSProxy: Bool = false -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift deleted file mode 100644 index 4c05c9760..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/MockWebSocketDelegate.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import ApolloWebSocket - -public class MockWebSocketDelegate: WebSocketClientDelegate { - public var didReceiveMessage: ((String) -> Void)? - - public init() {} - - public func websocketDidConnect(socket: WebSocketClient) {} - - public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {} - - public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { - didReceiveMessage?(text) - } - - public func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt deleted file mode 100644 index 651cda1a9..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/a.txt +++ /dev/null @@ -1 +0,0 @@ -Alpha file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt deleted file mode 100644 index 7cc0a5791..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/b.txt +++ /dev/null @@ -1 +0,0 @@ -Bravo file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt deleted file mode 100644 index 3adae37d8..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/Resources/c.txt +++ /dev/null @@ -1 +0,0 @@ -Charlie file content. diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift deleted file mode 100644 index 55bebd166..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import Apollo -import ApolloSQLite - -public class SQLiteTestCacheProvider: TestCacheProvider { - /// Execute a test block rather than return a cache synchronously, since cache setup may be - /// asynchronous at some point. - public static func withCache(initialRecords: RecordSet? = nil, fileURL: URL? = nil, execute test: (NormalizedCache) throws -> ()) throws { - let fileURL = fileURL ?? temporarySQLiteFileURL() - let cache = try! SQLiteNormalizedCache(fileURL: fileURL) - if let initialRecords = initialRecords { - _ = try cache.merge(records: initialRecords) - } - try test(cache) - } - - public static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) { - let fileURL = temporarySQLiteFileURL() - let cache = try! SQLiteNormalizedCache(fileURL: fileURL) - completionHandler(.success((cache, nil))) - } - - public static func temporarySQLiteFileURL() -> URL { - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) - - // Create a folder with a random UUID to hold the SQLite file, since creating them in the - // same folder this close together will cause DB locks when you try to delete between tests. - let folder = temporaryDirectoryURL.appendingPathComponent(UUID().uuidString) - try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) - - return folder.appendingPathComponent("db.sqlite3") - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift deleted file mode 100644 index 6b6c8d3c8..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/String+Data.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public extension String { - func crlfFormattedData() -> Data { - return replacingOccurrences(of: "\n\n", with: "\r\n\r\n").data(using: .utf8)! - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift deleted file mode 100644 index 8341e89ad..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestCacheProvider.swift +++ /dev/null @@ -1,59 +0,0 @@ -import XCTest -import Apollo - -public typealias TearDownHandler = () throws -> () -public typealias TestDependency = (Resource, TearDownHandler?) - -public protocol TestCacheProvider: AnyObject { - static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) -} - -public class InMemoryTestCacheProvider: TestCacheProvider { - public static func makeNormalizedCache(_ completionHandler: (Result, Error>) -> ()) { - let cache = InMemoryNormalizedCache() - completionHandler(.success((cache, nil))) - } -} - -public protocol CacheDependentTesting { - var cacheType: TestCacheProvider.Type { get } - var cache: NormalizedCache! { get } -} - -extension CacheDependentTesting where Self: XCTestCase { - public func makeNormalizedCache() throws -> NormalizedCache { - var result: Result = .failure(XCTestError(.timeoutWhileWaiting)) - - let expectation = XCTestExpectation(description: "Initialized normalized cache") - - cacheType.makeNormalizedCache() { [weak self] testDependencyResult in - guard let self = self else { return } - - result = testDependencyResult.map { testDependency in - let (cache, tearDownHandler) = testDependency - - if let tearDownHandler = tearDownHandler { - self.addTeardownBlock { - do { - try tearDownHandler() - } catch { - self.record(error) - } - } - } - - return cache - } - - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) - - return try result.get() - } - - public func mergeRecordsIntoCache(_ records: RecordSet) { - _ = try! cache.merge(records: records) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift deleted file mode 100644 index 7bf8e579e..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestError.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -public struct TestError: Error, CustomDebugStringConvertible { - let message: String? - - public init(_ message: String? = nil) { - self.message = message - } - - public var debugDescription: String { - message ?? "TestError" - } - -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift deleted file mode 100644 index f675744a3..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestFileHelper.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// TestFileHelper.swift -// ApolloTests -// -// Created by Ellen Shapiro on 3/18/20. -// Copyright © 2020 Apollo GraphQL. All rights reserved. -// - -import Foundation -import Apollo - -public struct TestFileHelper { - - public static func testParentFolder(for file: StaticString = #file) -> URL { - let fileAsString = file.withUTF8Buffer { - String(decoding: $0, as: UTF8.self) - } - let url = URL(fileURLWithPath: fileAsString) - return url.deletingLastPathComponent() - } - - public static func uploadServerFolder(from file: StaticString = #file) -> URL { - self.testParentFolder(for: file) - .deletingLastPathComponent() // test root - .deletingLastPathComponent() // source root - .appendingPathComponent("SimpleUploadServer") - } - - public static func uploadsFolder(from file: StaticString = #file) -> URL { - self.uploadServerFolder(from: file) - .appendingPathComponent("uploads") - } - - public static func fileURLForFile(named name: String, extension fileExtension: String) -> URL { - return self.testParentFolder() - .appendingPathComponent("Resources") - .appendingPathComponent(name) - .appendingPathExtension(fileExtension) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift deleted file mode 100644 index 083346d4f..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift +++ /dev/null @@ -1,144 +0,0 @@ -import Foundation -import XCTest - -/// A test helper object that manages creation and deletion of files in a temporary directory -/// that ensures test isolation. -/// -/// **Creating a `TestIsolatedFileManager` sets the current working directory for the -/// current process to it's `directoryURL`.** After the test finishes, the current working -/// directory is reset to its previous value. -/// -/// All files and directories created by this class will be automatically deleted upon test -/// completion prior to the test case's `tearDown()` function being called. -/// -/// You can create a file manager from within a specific unit test with the -/// `testIsolatedFileManager()` function on `XCTestCase`. -public class TestIsolatedFileManager { - - public let directoryURL: URL - public let fileManager: FileManager - private let previousWorkingDirectory: String - - /// The paths for the files written to by the ``ApolloFileManager``. - public private(set) var writtenFiles: Set = [] - - fileprivate init(directoryURL: URL, fileManager: FileManager) throws { - self.directoryURL = directoryURL - self.fileManager = fileManager - - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) - previousWorkingDirectory = fileManager.currentDirectoryPath - fileManager.changeCurrentDirectoryPath(directoryURL.path) - } - - func cleanUp() throws { - fileManager.changeCurrentDirectoryPath(previousWorkingDirectory) - try fileManager.removeItem(at: directoryURL) - } - - /// Creates a file in the test directory. - /// - /// - Parameters: - /// - data: File content - /// - filename: Target name of the file. This should not include any path information - /// - /// - Returns: - /// - The full path of the created file. - @discardableResult - public func createFile( - containing data: Data, - named fileName: String, - inDirectory subDirectory: String? = nil - ) throws -> String { - let fileDirectoryURL: URL - if let subDirectory { - fileDirectoryURL = directoryURL.appendingPathComponent(subDirectory, isDirectory: true) - try fileManager.createDirectory(at: fileDirectoryURL, withIntermediateDirectories: true) - } else { - fileDirectoryURL = directoryURL - } - - let filePath: String = fileDirectoryURL - .resolvingSymlinksInPath() - .appendingPathComponent(fileName, isDirectory: false).path - - guard fileManager.createFile(atPath: filePath, contents: data) else { - throw Error.cannotCreateFile(at: filePath) - } - - writtenFiles.insert(filePath) - return filePath - } - - @discardableResult - public func createFile( - body: @autoclosure () -> String, - named fileName: String, - inDirectory directory: String? = nil - ) throws -> String { - let bodyString = body() - guard let data = bodyString.data(using: .utf8) else { - throw Error.cannotEncodeFileData(from: bodyString) - } - - return try createFile( - containing: data, - named: fileName, - inDirectory: directory - ) - } - - public enum Error: Swift.Error { - case cannotCreateFile(at: String) - case cannotEncodeFileData(from: String) - - public var errorDescription: String { - switch self { - case .cannotCreateFile(let path): - return "Cannot create file at \(path)" - case .cannotEncodeFileData(let body): - return "Cannot encode provided body string into UTF-8 data. Body:\n\(body)" - } - } - } - -} - -public extension XCTestCase { - - /// Creates a `TestIsolatedFileManager` for the current test. - /// - /// **Creating a `TestIsolatedFileManager` sets the current working directory for the - /// current process to it's `directoryURL`.** After the test finishes, the current working - /// directory is reset to its previous value. - func testIsolatedFileManager( - with fileManager: FileManager = .default - ) throws -> TestIsolatedFileManager { - let manager = try TestIsolatedFileManager( - directoryURL: computeTestTempDirectoryURL(), - fileManager: fileManager - ) - - addTeardownBlock { - try manager.cleanUp() - } - - return manager - } - - private func computeTestTempDirectoryURL() -> URL { - let directoryURL: URL - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, *) { - directoryURL = URL(filePath: NSTemporaryDirectory(), directoryHint: .isDirectory) - } else { - directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - } - - return directoryURL - .appendingPathComponent("ApolloTests") - .appendingPathComponent(name - .trimmingCharacters(in: CharacterSet(charactersIn: "-[]")) - .replacingOccurrences(of: " ", with: "_") - ) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift deleted file mode 100644 index 437ae83e1..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestObserver.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Apollo -import XCTest - -public class TestObserver: NSObject, XCTestObservation { - - private let onFinish: (XCTestCase) -> Void - - @Atomic private var isStarted: Bool = false - let stopAfterEachTest: Bool - - public init( - startOnInit: Bool = true, - stopAfterEachTest: Bool = true, - onFinish: @escaping ((XCTestCase) -> Void) - ) { - self.stopAfterEachTest = stopAfterEachTest - self.onFinish = onFinish - super.init() - - if startOnInit { start() } - } - - public func start() { - guard !isStarted else { return } - $isStarted.mutate { - XCTestObservationCenter.shared.addTestObserver(self) - $0 = true - } - } - - public func stop() { - guard isStarted else { return } - $isStarted.mutate { - XCTestObservationCenter.shared.removeTestObserver(self) - $0 = false - } - } - - public func testCaseDidFinish(_ testCase: XCTestCase) { - onFinish(testCase) - if stopAfterEachTest { stop() } - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift deleted file mode 100644 index 4b1a88605..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/TestURLs.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -/// URLs used in testing -public enum TestURL { - case mockServer - case mockPort8080 - - public var url: URL { - let urlString: String - switch self { - case .mockServer: - urlString = "http://localhost/dummy_url" - case .mockPort8080: - urlString = "http://localhost:8080/graphql" - } - - return URL(string: urlString)! - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift deleted file mode 100644 index 258ecf6be..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTAssertHelpers.swift +++ /dev/null @@ -1,159 +0,0 @@ -import XCTest -import Apollo - -public func XCTAssertEqual(_ expression1: @autoclosure () throws -> [T : U]?, _ expression2: @autoclosure () throws -> [T : U]?, file: StaticString = #filePath, line: UInt = #line) rethrows { - let optionalValue1 = try expression1() - let optionalValue2 = try expression2() - - let message = { - "(\"\(String(describing: optionalValue1))\") is not equal to (\"\(String(describing: optionalValue2))\")" - } - - switch (optionalValue1, optionalValue2) { - case (.none, .none): - break - case let (value1 as NSDictionary, value2 as NSDictionary): - XCTAssertEqual(value1, value2, message(), file: file, line: line) - default: - XCTFail(message(), file: file, line: line) - } -} - -public func XCTAssertEqualUnordered(_ expression1: @autoclosure () throws -> C1, _ expression2: @autoclosure () throws -> C2, file: StaticString = #filePath, line: UInt = #line) rethrows where Element: Hashable, C1.Element == Element, C2.Element == Element { - let collection1 = try expression1() - let collection2 = try expression2() - - // Convert to sets to ignore ordering and only check whether all elements are accounted for, - // but also check count to detect duplicates. - XCTAssertEqual(collection1.count, collection2.count, file: file, line: line) - XCTAssertEqual(Set(collection1), Set(collection2), file: file, line: line) -} - -public func XCTAssertMatch(_ valueExpression: @autoclosure () throws -> Pattern.Base, _ patternExpression: @autoclosure () throws -> Pattern, file: StaticString = #filePath, line: UInt = #line) rethrows { - let value = try valueExpression() - let pattern = try patternExpression() - - let message = { - "(\"\(value)\") does not match (\"\(pattern)\")" - } - - if case pattern = value { return } - - XCTFail(message(), file: file, line: line) -} - -// We need overloaded versions instead of relying on default arguments -// due to https://bugs.swift.org/browse/SR-1534 - -public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { - try XCTAssertSuccessResult(expression(), file: file, line: line, {_ in }) -} - -public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ successHandler: (_ value: Success) throws -> Void) rethrows { - let result = try expression() - - switch result { - case .success(let value): - try successHandler(value) - case .failure(let error): - XCTFail("Expected success, but result was an error: \(String(describing: error))", file: file, line: line) - } -} - -public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { - try XCTAssertFailureResult(expression(), file: file, line: line, {_ in }) -} - -public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: Error) throws -> Void) rethrows { - let result = try expression() - - switch result { - case .success(let success): - XCTFail("Expected failure, but result was successful: \(String(describing: success))", file: file, line: line) - case .failure(let error): - try errorHandler(error) - } -} - -/// Checks that the condition is eventually true with a given timeout (default 1 second). -/// -/// This assertion runs the run loop for 0.01 second after each time it checks the condition until -/// the condition is true or the timeout is reached. -/// -/// - Parameters: -/// - test: An autoclosure for the condition to test for truthiness. -/// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. -/// - message: A message to send on failure. -public func XCTAssertTrueEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { - let runLoop = RunLoop.current - let timeoutDate = Date(timeIntervalSinceNow: timeout) - repeat { - if test() { - return - } - runLoop.run(until: Date(timeIntervalSinceNow: 0.01)) - } while Date().compare(timeoutDate) == .orderedAscending - - XCTFail(message, file: file, line: line) -} - -/// Checks that the condition is eventually false with a given timeout (default 1 second). -/// -/// This assertion runs the run loop for 0.01 second after each time it checks the condition until -/// the condition is false or the timeout is reached. -/// -/// - Parameters: -/// - test: An autoclosure for the condition to test for falsiness. -/// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. -/// - message: A message to send on failure. -public func XCTAssertFalseEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { - XCTAssertTrueEventually(!test(), timeout: timeout, message: message, file: file, line: line) -} - -/// Downcast an expression to a specified type. -/// -/// Generates a failure when the downcast doesn't succeed. -/// -/// - Parameters: -/// - expression: An expression to downcast to `ExpectedType`. -/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. -/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. -/// - Returns: A value of type `ExpectedType`, the result of evaluating and downcasting the given `expression`. -/// - Throws: An error when the downcast doesn't succeed. It will also rethrow any error thrown while evaluating the given expression. -public func XCTDowncast(_ expression: @autoclosure () throws -> AnyObject, to type: ExpectedType.Type, file: StaticString = #filePath, line: UInt = #line) throws -> ExpectedType { - let object = try expression() - - guard let expected = object as? ExpectedType else { - throw XCTFailure("Expected type to be \(ExpectedType.self), but found \(Swift.type(of: object))", file: file, line: line) - } - - return expected -} - -/// An error which causes the current test to cease executing and fail when it is thrown. -/// Similar to `XCTSkip`, but without marking the test as skipped. -public struct XCTFailure: Error, CustomNSError { - - public init(_ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { - XCTFail(message(), file: file, line: line) - } - - /// The domain of the error. - public static let errorDomain = XCTestErrorDomain - - /// The error code within the given domain. - public let errorCode: Int = 0 - - /// The user-info dictionary. - public let errorUserInfo: [String : Any] = [ - // Make sure the thrown error doesn't show up as a test failure, because we already record - // a more detailed failure (with the right source location) ourselves. - "XCTestErrorUserInfoKeyShouldIgnore": true - ] -} - -public extension Optional { - func xctUnwrapped(file: StaticString = #filePath, line: UInt = #line) throws -> Wrapped { - try XCTUnwrap(self, file: file, line: line) - } -} diff --git a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift b/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift deleted file mode 100644 index 38ddf06c9..000000000 --- a/Tests/apollo-ios-paginationTests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift +++ /dev/null @@ -1,77 +0,0 @@ -import XCTest - -extension XCTestExpectation { - /// Private API for accessing the number of times an expectation has been fulfilled. - public var numberOfFulfillments: Int { - value(forKey: "numberOfFulfillments") as! Int - } -} - -public extension XCTestCase { - /// Record the specified`error` as an `XCTIssue`. - func record(_ error: Error, compactDescription: String? = nil, file: StaticString = #filePath, line: UInt = #line) { - var issue = XCTIssue(type: .thrownError, compactDescription: compactDescription ?? String(describing: error)) - - issue.associatedError = error - - let location = XCTSourceCodeLocation(filePath: file, lineNumber: line) - issue.sourceCodeContext = XCTSourceCodeContext(location: location) - - record(issue) - } - - /// Invoke a throwing closure, and record any thrown errors without rethrowing. This is useful if you need to run code that may throw - /// in a place where throwing isn't allowed, like `measure` blocks. - func whileRecordingErrors(file: StaticString = #file, line: UInt = #line, _ perform: () throws -> Void) { - do { - try perform() - } catch { - // Respect XCTestErrorUserInfoKeyShouldIgnore key that is used by XCTUnwrap, XCTSkip, and our own XCTFailure. - let shouldIgnore = (((error as NSError).userInfo["XCTestErrorUserInfoKeyShouldIgnore"] as? Bool) == true) - if !shouldIgnore { - record(error, file: file, line: line) - } - } - } - - /// Wrapper around `XCTContext.runActivity` to allow for future extension. - func runActivity(_ name: String, perform: (XCTActivity) throws -> Result) rethrows -> Result { - return try XCTContext.runActivity(named: name, block: perform) - } -} - -import Apollo -import ApolloAPI - -public extension XCTestCase { - /// Make an `AsyncResultObserver` for receiving results of the specified GraphQL operation. - func makeResultObserver(for operation: Operation, file: StaticString = #filePath, line: UInt = #line) -> AsyncResultObserver, Error> { - return AsyncResultObserver(testCase: self, file: file, line: line) - } -} - -public protocol StoreLoading { - static var defaultWaitTimeout: TimeInterval { get } - var store: ApolloStore! { get } -} - -public extension StoreLoading { - static var defaultWaitTimeout: TimeInterval { 1.0 } -} - -extension StoreLoading where Self: XCTestCase { - public func loadFromStore( - operation: Operation, - file: StaticString = #filePath, - line: UInt = #line, - resultHandler: @escaping AsyncResultObserver, Error>.ResultHandler - ) { - let resultObserver = makeResultObserver(for: operation, file: file, line: line) - - let expectation = resultObserver.expectation(description: "Loaded query from store", file: file, line: line, resultHandler: resultHandler) - - store.load(operation, resultHandler: resultObserver.handler) - - wait(for: [expectation], timeout: Self.defaultWaitTimeout) - } -} diff --git a/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift b/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift deleted file mode 100644 index 24c022ca9..000000000 --- a/Tests/apollo-ios-paginationTests/apollo_ios_paginationTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import apollo_ios_pagination - -final class apollo_ios_paginationTests: XCTestCase { - -}