mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
BIT-59: Add initial networking layer (#9)
This commit is contained in:
parent
090f1a07fd
commit
adf0a24d73
@ -12,6 +12,8 @@
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
32
Networking/.swiftpm/Networking.xctestplan
Normal file
32
Networking/.swiftpm/Networking.xctestplan
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "904F040F-90D0-48AE-8E03-A700F055631B",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"codeCoverage" : {
|
||||
"targets" : [
|
||||
{
|
||||
"containerPath" : "container:",
|
||||
"identifier" : "Networking",
|
||||
"name" : "Networking"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:",
|
||||
"identifier" : "NetworkingTests",
|
||||
"name" : "NetworkingTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
7
Networking/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
Networking/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Networking"
|
||||
BuildableName = "Networking"
|
||||
BlueprintName = "Networking"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "NetworkingTests"
|
||||
BuildableName = "NetworkingTests"
|
||||
BlueprintName = "NetworkingTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:.swiftpm/Networking.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "NetworkingTests"
|
||||
BuildableName = "NetworkingTests"
|
||||
BlueprintName = "NetworkingTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Networking"
|
||||
BuildableName = "Networking"
|
||||
BlueprintName = "Networking"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
23
Networking/Package.swift
Normal file
23
Networking/Package.swift
Normal file
@ -0,0 +1,23 @@
|
||||
// swift-tools-version: 5.8
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Networking",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "Networking",
|
||||
targets: ["Networking"]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Networking"),
|
||||
.testTarget(
|
||||
name: "NetworkingTests",
|
||||
dependencies: ["Networking"]
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
import OSLog
|
||||
|
||||
extension Logger {
|
||||
/// Logger instance for networking logs.
|
||||
static let networking = Logger(subsystem: subsystem, category: "Networking")
|
||||
|
||||
/// The OSLog subsystem passed along with logs to the logging system to identify logs from this
|
||||
/// library.
|
||||
private static var subsystem = Bundle(for: HTTPService.self).bundleIdentifier!
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
/// Conforms `URLSession` to the `HTTPClient` protocol.
|
||||
///
|
||||
extension URLSession: HTTPClient {
|
||||
public func send(_ request: HTTPRequest) async throws -> HTTPResponse {
|
||||
var urlRequest = URLRequest(url: request.url)
|
||||
urlRequest.httpMethod = request.method.rawValue
|
||||
urlRequest.httpBody = request.body
|
||||
|
||||
for (field, value) in request.headers {
|
||||
urlRequest.addValue(value, forHTTPHeaderField: field)
|
||||
}
|
||||
|
||||
let (data, urlResponse) = try await data(for: urlRequest)
|
||||
|
||||
return try HTTPResponse(data: data, response: urlResponse, request: request)
|
||||
}
|
||||
}
|
||||
10
Networking/Sources/Networking/HTTPClient.swift
Normal file
10
Networking/Sources/Networking/HTTPClient.swift
Normal file
@ -0,0 +1,10 @@
|
||||
/// A protocol for a networking client that performs HTTP requests.
|
||||
///
|
||||
public protocol HTTPClient {
|
||||
/// Sends a `HTTPRequest` over the network, returning a `HTTPResponse`.
|
||||
///
|
||||
/// - Parameter request: The `HTTPRequest` to send.
|
||||
/// - Returns: A `HTTPResponse` containing the data that was returned from the network request.
|
||||
///
|
||||
func send(_ request: HTTPRequest) async throws -> HTTPResponse
|
||||
}
|
||||
48
Networking/Sources/Networking/HTTPLogger.swift
Normal file
48
Networking/Sources/Networking/HTTPLogger.swift
Normal file
@ -0,0 +1,48 @@
|
||||
import OSLog
|
||||
|
||||
/// An object that handles logging HTTP requests and responses.
|
||||
///
|
||||
class HTTPLogger {
|
||||
/// Logs the details of a `HTTPRequest`.
|
||||
///
|
||||
/// - Parameter httpRequest: The `HTTPRequest` to log the details of.
|
||||
///
|
||||
func logRequest(_ httpRequest: HTTPRequest) {
|
||||
let formattedBody = formattedBody(httpRequest.body)
|
||||
Logger.networking.info("""
|
||||
Request \(httpRequest.requestID): \(httpRequest.method.rawValue) \(httpRequest.url)
|
||||
Body: \(formattedBody)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
/// Logs the details of a `HTTPResponse`.
|
||||
///
|
||||
/// - Parameter httpResponse: The `HTTPResponse` to log the details of.
|
||||
///
|
||||
func logResponse(_ httpResponse: HTTPResponse) {
|
||||
let formattedBody = formattedBody(httpResponse.body)
|
||||
Logger.networking.info("""
|
||||
Response \(httpResponse.requestID): \(httpResponse.url) \(httpResponse.statusCode)
|
||||
Body: \(formattedBody)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// Formats the data in the body of a request or response for logging.
|
||||
///
|
||||
/// - Parameter data: The data from the body of a request or response to format.
|
||||
/// - Returns: A string containing the formatted body data.
|
||||
///
|
||||
private func formattedBody(_ data: Data?) -> String {
|
||||
guard let data, !data.isEmpty else { return "(empty)" }
|
||||
|
||||
if let dataString = String(data: data, encoding: .utf8) {
|
||||
return dataString
|
||||
}
|
||||
|
||||
return data.debugDescription
|
||||
}
|
||||
}
|
||||
23
Networking/Sources/Networking/HTTPMethod.swift
Normal file
23
Networking/Sources/Networking/HTTPMethod.swift
Normal file
@ -0,0 +1,23 @@
|
||||
/// A type representing the HTTP method.
|
||||
///
|
||||
public struct HTTPMethod: Equatable {
|
||||
/// The string value of the method.
|
||||
let rawValue: String
|
||||
}
|
||||
|
||||
public extension HTTPMethod {
|
||||
/// The `GET` method.
|
||||
static let get = HTTPMethod(rawValue: "GET")
|
||||
|
||||
/// The `POST` method.
|
||||
static let post = HTTPMethod(rawValue: "POST")
|
||||
|
||||
/// The `PUT` method.
|
||||
static let put = HTTPMethod(rawValue: "PUT")
|
||||
|
||||
/// The `DELETE` method.
|
||||
static let delete = HTTPMethod(rawValue: "DELETE")
|
||||
|
||||
/// The `PATCH` method.
|
||||
static let patch = HTTPMethod(rawValue: "PATCH")
|
||||
}
|
||||
83
Networking/Sources/Networking/HTTPRequest.swift
Normal file
83
Networking/Sources/Networking/HTTPRequest.swift
Normal file
@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
|
||||
/// A data model containing the details of an HTTP request to be performed.
|
||||
///
|
||||
public struct HTTPRequest: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// Data to be sent in the body of the request.
|
||||
public let body: Data?
|
||||
|
||||
/// Headers to be included in the request.
|
||||
public let headers: [String: String]
|
||||
|
||||
/// The HTTP method of the request.
|
||||
public let method: HTTPMethod
|
||||
|
||||
/// A unique identifier for the request.
|
||||
public let requestID: UUID
|
||||
|
||||
/// The URL for the request.
|
||||
public let url: URL
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `HTTPRequest`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL for the request.
|
||||
/// - method: The HTTP method of the request.
|
||||
/// - headers: Headers to be included in the request.
|
||||
/// - body: Data to be sent in the body of the request.
|
||||
/// - requestID: A unique identifier for the request.
|
||||
///
|
||||
public init(
|
||||
url: URL,
|
||||
method: HTTPMethod = .get,
|
||||
headers: [String: String] = [:],
|
||||
body: Data? = nil,
|
||||
requestID: UUID = UUID()
|
||||
) {
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
self.method = method
|
||||
self.requestID = requestID
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
public extension HTTPRequest {
|
||||
/// Initialize a `HTTPRequest` from a `Request` instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: The `Request` instance used to initialize the `HTTPRequest`.
|
||||
/// - baseURL: The base URL that will be prepended to the `Request`'s path to construct the
|
||||
/// request URL.
|
||||
///
|
||||
init<R: Request>(request: R, baseURL: URL) throws {
|
||||
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
urlComponents.queryItems = !request.query.isEmpty ? request.query : nil
|
||||
|
||||
if urlComponents.path.hasSuffix("/") {
|
||||
urlComponents.path.removeLast()
|
||||
}
|
||||
|
||||
guard let url = urlComponents.url?.appendingPathComponent(request.path) else {
|
||||
fatalError("🛑 Request `resolve` failed: reason unknown.")
|
||||
}
|
||||
|
||||
var headers = request.headers
|
||||
if let additionalHeaders = request.body?.additionalHeaders {
|
||||
for header in additionalHeaders {
|
||||
headers[header.key] = header.value
|
||||
}
|
||||
}
|
||||
|
||||
try self.init(
|
||||
url: url,
|
||||
method: request.method,
|
||||
headers: headers,
|
||||
body: request.body?.encode()
|
||||
)
|
||||
}
|
||||
}
|
||||
64
Networking/Sources/Networking/HTTPResponse.swift
Normal file
64
Networking/Sources/Networking/HTTPResponse.swift
Normal file
@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
|
||||
/// A data model containing the details of an HTTP response that's been received.
|
||||
///
|
||||
public struct HTTPResponse: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// Data received in the body of the response.
|
||||
public let body: Data
|
||||
|
||||
/// Headers received from response.
|
||||
public let headers: [String: String]
|
||||
|
||||
/// The response's status code.
|
||||
public let statusCode: Int
|
||||
|
||||
/// A unique identifier for the request associated with this response.
|
||||
public let requestID: UUID
|
||||
|
||||
/// The URL from which the response was created.
|
||||
public let url: URL
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `HTTPResponse`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL from which the response was created.
|
||||
/// - statusCode: The response's status code.
|
||||
/// - headers: Headers received from response.
|
||||
/// - body: Data received in the body of the response.
|
||||
/// - requestID: A unique identifier for the request associated with this response.
|
||||
///
|
||||
public init(url: URL, statusCode: Int, headers: [String: String], body: Data, requestID: UUID) {
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
self.statusCode = statusCode
|
||||
self.requestID = requestID
|
||||
self.url = url
|
||||
}
|
||||
|
||||
/// Initialize a `HTTPResponse` with data and a `URLResponse`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: Data received in the body of the response.
|
||||
/// - response: A `URLResponse` object containing the details of the response.
|
||||
/// - request: The `HTTPRequest` associated with this response.
|
||||
///
|
||||
init(data: Data, response: URLResponse, request: HTTPRequest) throws {
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw HTTPResponseError.invalidResponse(response)
|
||||
}
|
||||
|
||||
guard let responseURL = response.url else {
|
||||
throw HTTPResponseError.noURL
|
||||
}
|
||||
|
||||
url = responseURL
|
||||
statusCode = response.statusCode
|
||||
body = data
|
||||
headers = response.allHeaderFields as? [String: String] ?? [:]
|
||||
requestID = request.requestID
|
||||
}
|
||||
}
|
||||
10
Networking/Sources/Networking/HTTPResponseError.swift
Normal file
10
Networking/Sources/Networking/HTTPResponseError.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
/// Errors thrown by `HTTPResponse`.
|
||||
///
|
||||
enum HTTPResponseError: Error, Equatable {
|
||||
/// The `URLResponse` was unable to be converted to a `HTTPURLResponse`.
|
||||
case invalidResponse(URLResponse)
|
||||
/// The `URLResponse` didn't contain a URL.
|
||||
case noURL
|
||||
}
|
||||
51
Networking/Sources/Networking/HTTPService.swift
Normal file
51
Networking/Sources/Networking/HTTPService.swift
Normal file
@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
|
||||
/// A networking service that can be used to perform HTTP requests.
|
||||
///
|
||||
public class HTTPService {
|
||||
// MARK: Properties
|
||||
|
||||
/// The URL against which requests are resolved.
|
||||
let baseURL: URL
|
||||
|
||||
/// The underlying `HTTPClient` that performs the network request.
|
||||
let client: HTTPClient
|
||||
|
||||
/// A logger used to log HTTP request and responses.
|
||||
let logger = HTTPLogger()
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `HTTPService`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - baseURL: The URL against which requests are resolved.
|
||||
/// - client: The underlying `HTTPClient` that performs the network request.
|
||||
///
|
||||
public init(
|
||||
baseURL: URL,
|
||||
client: HTTPClient = URLSession.shared
|
||||
) {
|
||||
self.baseURL = baseURL
|
||||
self.client = client
|
||||
}
|
||||
|
||||
// MARK: Request Performing
|
||||
|
||||
/// Performs a network request.
|
||||
///
|
||||
/// - Parameter request: The request to perform.
|
||||
/// - Returns: The response received for the request.
|
||||
///
|
||||
public func send<R: Request>(
|
||||
_ request: R
|
||||
) async throws -> R.Response where R.Response: Response {
|
||||
let httpRequest = try HTTPRequest(request: request, baseURL: baseURL)
|
||||
logger.logRequest(httpRequest)
|
||||
|
||||
let httpResponse = try await client.send(httpRequest)
|
||||
logger.logResponse(httpResponse)
|
||||
|
||||
return try R.Response(response: httpResponse)
|
||||
}
|
||||
}
|
||||
43
Networking/Sources/Networking/Request.swift
Normal file
43
Networking/Sources/Networking/Request.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
/// A protocol for an instance that describes an HTTP request.
|
||||
///
|
||||
public protocol Request {
|
||||
/// The response type associated with this request.
|
||||
associatedtype Response
|
||||
/// The body type associated with this request. This could be `Data` or another type conforming
|
||||
/// to `RequestBody` that could be converted to `Data` to include in the body of the request.
|
||||
associatedtype Body: RequestBody
|
||||
|
||||
/// The HTTP method for the request.
|
||||
var method: HTTPMethod { get }
|
||||
|
||||
/// The body of the request.
|
||||
var body: Body? { get }
|
||||
|
||||
/// The URL path for this request that will be appended to the base URL.
|
||||
var path: String { get }
|
||||
|
||||
/// A dictionary of HTTP headers to be sent in the request.
|
||||
var headers: [String: String] { get }
|
||||
|
||||
/// A list of URL query items for the request.
|
||||
var query: [URLQueryItem] { get }
|
||||
}
|
||||
|
||||
/// This extension provides default values for the `Request` methods, which can be overridden in a
|
||||
/// type conforming to the `Request` protocol.
|
||||
///
|
||||
public extension Request {
|
||||
/// The HTTP method for the request.
|
||||
var method: HTTPMethod { .get }
|
||||
|
||||
/// The body of the request.
|
||||
var body: Data? { nil }
|
||||
|
||||
/// A dictionary of HTTP headers to be sent in the request.
|
||||
var headers: [String: String] { [:] }
|
||||
|
||||
/// A list of URL query items for the request.
|
||||
var query: [URLQueryItem] { [] }
|
||||
}
|
||||
43
Networking/Sources/Networking/RequestBody.swift
Normal file
43
Networking/Sources/Networking/RequestBody.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
/// A protocol for an instance containing the data for the body of a request.
|
||||
///
|
||||
public protocol RequestBody {
|
||||
/// Additional headers to append to the request headers.
|
||||
var additionalHeaders: [String: String] { get }
|
||||
|
||||
/// Encodes the data to be included in the body of the request.
|
||||
///
|
||||
/// - Returns: The encoded data to include in the body of the request.
|
||||
///
|
||||
func encode() throws -> Data
|
||||
}
|
||||
|
||||
/// A protocol for a `RequestBody` that can be encoded to JSON for the body of a request.
|
||||
///
|
||||
public protocol JSONRequestBody: RequestBody, Encodable {
|
||||
/// The `JSONEncoder` used to encode the object to include in the body of the request.
|
||||
static var encoder: JSONEncoder { get }
|
||||
}
|
||||
|
||||
public extension JSONRequestBody {
|
||||
/// Additional headers to append to the request headers.
|
||||
var additionalHeaders: [String: String] {
|
||||
["Content-Type": "application/json"]
|
||||
}
|
||||
|
||||
/// Encodes the data to be included in the body of the request.
|
||||
///
|
||||
/// - Returns: The encoded data to include in the body of the request.
|
||||
///
|
||||
func encode() throws -> Data {
|
||||
try Self.encoder.encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conforms `Data` to `RequestBody`.
|
||||
///
|
||||
extension Data: RequestBody {
|
||||
public var additionalHeaders: [String: String] { [:] }
|
||||
public func encode() throws -> Data { self }
|
||||
}
|
||||
37
Networking/Sources/Networking/Response.swift
Normal file
37
Networking/Sources/Networking/Response.swift
Normal file
@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
|
||||
/// A protocol for an instance that describes a HTTP response.
|
||||
///
|
||||
public protocol Response {
|
||||
/// Initialize a `Response` from a `HTTPResponse`.
|
||||
///
|
||||
/// Typically, this is where the raw `HTTPResponse` would be decoded into an app model.
|
||||
///
|
||||
/// - Parameter response: The `HTTPResponse` used to initialize the `Response`.
|
||||
///
|
||||
init(response: HTTPResponse) throws
|
||||
}
|
||||
|
||||
/// A protocol for a `Response` containing JSON.
|
||||
///
|
||||
public protocol JSONResponse: Response, Codable {
|
||||
/// A JSON decoder used to decode this response.
|
||||
static var decoder: JSONDecoder { get }
|
||||
}
|
||||
|
||||
public extension JSONResponse {
|
||||
/// Initialize a `JSONResponse` from a `HTTPResponse`.
|
||||
///
|
||||
/// - Parameter response: The `HTTPResponse` used to initialize the `Response.
|
||||
///
|
||||
init(response: HTTPResponse) throws {
|
||||
self = try Self.decoder.decode(Self.self, from: response.body)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: Response where Element: JSONResponse {}
|
||||
extension Array: JSONResponse where Element: JSONResponse {
|
||||
public static var decoder: JSONDecoder {
|
||||
Element.decoder
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class URLSessionHTTPClientTests: XCTestCase {
|
||||
var subject: URLSession!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.protocolClasses = [MockURLProtocol.self]
|
||||
|
||||
subject = URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
subject = nil
|
||||
URLProtocolMocking.reset()
|
||||
}
|
||||
|
||||
/// `send(_:)` performs the request and returns the response for a 200 status request.
|
||||
func testSendSuccess200() async throws {
|
||||
let urlResponse = HTTPURLResponse(
|
||||
url: URL(string: "https://example.com")!,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: ["Content-Type": "application/json"]
|
||||
)!
|
||||
|
||||
URLProtocolMocking.mock(
|
||||
HTTPRequest.default.url,
|
||||
with: .success((urlResponse, "response data".data(using: .utf8)!))
|
||||
)
|
||||
|
||||
let httpResponse = try await subject.send(.default)
|
||||
|
||||
XCTAssertEqual(
|
||||
try String(data: XCTUnwrap(httpResponse.body), encoding: .utf8),
|
||||
"response data"
|
||||
)
|
||||
XCTAssertEqual(httpResponse.headers, ["Content-Type": "application/json"])
|
||||
XCTAssertEqual(httpResponse.statusCode, 200)
|
||||
XCTAssertEqual(httpResponse.url, URL(string: "https://example.com")!)
|
||||
}
|
||||
|
||||
/// `send(_:)` performs the request and returns the response for a 500 status request.
|
||||
func testSendSuccess500() async throws {
|
||||
let urlResponse = HTTPURLResponse(
|
||||
url: URL(string: "https://example.com")!,
|
||||
statusCode: 500,
|
||||
httpVersion: nil,
|
||||
headerFields: nil
|
||||
)!
|
||||
|
||||
URLProtocolMocking.mock(
|
||||
HTTPRequest.default.url,
|
||||
with: .success((urlResponse, Data()))
|
||||
)
|
||||
|
||||
let httpResponse = try await subject.send(.default)
|
||||
|
||||
XCTAssertEqual(httpResponse.body, Data())
|
||||
XCTAssertEqual(httpResponse.headers, [:])
|
||||
XCTAssertEqual(httpResponse.statusCode, 500)
|
||||
XCTAssertEqual(httpResponse.url, URL(string: "https://example.com")!)
|
||||
}
|
||||
|
||||
/// `send(_:)` performs the request and throws an error if one occurs.
|
||||
func testSendError() async throws {
|
||||
URLProtocolMocking.mock(
|
||||
HTTPRequest.default.url,
|
||||
with: .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
|
||||
)
|
||||
|
||||
do {
|
||||
_ = try await subject.send(.default)
|
||||
XCTFail("Expected send(_:) to throw an error.")
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
XCTAssertEqual(nsError.domain, NSURLErrorDomain)
|
||||
XCTAssertEqual(nsError.code, NSURLErrorTimedOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Networking/Tests/NetworkingTests/HTTPRequestTests.swift
Normal file
67
Networking/Tests/NetworkingTests/HTTPRequestTests.swift
Normal file
@ -0,0 +1,67 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class HTTPRequestTests: XCTestCase {
|
||||
struct TestRequest: Request {
|
||||
typealias Response = String // swiftlint:disable:this nesting
|
||||
let body: Data? = "body data".data(using: .utf8)
|
||||
let headers = ["Content-Type": "application/json"]
|
||||
let method = HTTPMethod.get
|
||||
let path = "/test"
|
||||
let query = [URLQueryItem(name: "foo", value: "bar")]
|
||||
}
|
||||
|
||||
/// The initializer provides default values.
|
||||
func testInitDefaultValues() {
|
||||
let subject = HTTPRequest(url: URL(string: "https://example.com")!)
|
||||
|
||||
XCTAssertNil(subject.body)
|
||||
XCTAssertEqual(subject.headers, [:])
|
||||
XCTAssertEqual(subject.method, .get)
|
||||
XCTAssertEqual(subject.url, URL(string: "https://example.com")!)
|
||||
}
|
||||
|
||||
/// The initializer sets the item's properties.
|
||||
func testInit() throws {
|
||||
let subject = HTTPRequest(
|
||||
url: URL(string: "https://example.com/json")!,
|
||||
method: .post,
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "🔒",
|
||||
],
|
||||
body: "top secret".data(using: .utf8)!
|
||||
)
|
||||
|
||||
try XCTAssertEqual(
|
||||
String(data: XCTUnwrap(subject.body), encoding: .utf8),
|
||||
"top secret"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
subject.headers,
|
||||
[
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "🔒",
|
||||
]
|
||||
)
|
||||
XCTAssertEqual(subject.method, .post)
|
||||
XCTAssertEqual(subject.url, URL(string: "https://example.com/json")!)
|
||||
}
|
||||
|
||||
/// `init(request:baseURL)` builds a `HTTPRequest` from a `Request` object.
|
||||
func testInitRequest() throws {
|
||||
let subject = try HTTPRequest(
|
||||
request: TestRequest(),
|
||||
baseURL: URL(string: "https://example.com/")!
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
try String(data: XCTUnwrap(subject.body), encoding: .utf8),
|
||||
"body data"
|
||||
)
|
||||
XCTAssertEqual(subject.headers, ["Content-Type": "application/json"])
|
||||
XCTAssertEqual(subject.method, .get)
|
||||
XCTAssertEqual(subject.url, URL(string: "https://example.com/test?foo=bar")!)
|
||||
}
|
||||
}
|
||||
68
Networking/Tests/NetworkingTests/HTTPResponseTests.swift
Normal file
68
Networking/Tests/NetworkingTests/HTTPResponseTests.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class HTTPResponseTests: XCTestCase {
|
||||
/// The initializer sets the item's properties.
|
||||
func testInit() {
|
||||
let subject = HTTPResponse(
|
||||
url: URL(string: "https://example.com")!,
|
||||
statusCode: 200,
|
||||
headers: [:],
|
||||
body: Data(),
|
||||
requestID: UUID()
|
||||
)
|
||||
|
||||
XCTAssertEqual(subject.body, Data())
|
||||
XCTAssertEqual(subject.headers, [:])
|
||||
XCTAssertEqual(subject.statusCode, 200)
|
||||
XCTAssertEqual(subject.url, URL(string: "https://example.com")!)
|
||||
}
|
||||
|
||||
/// The initializer sets the item's properties with a `HTTPURLResponse`.
|
||||
func testInitResponse() throws {
|
||||
let subject = try HTTPResponse(
|
||||
data: "response body".data(using: .utf8)!,
|
||||
response: XCTUnwrap(HTTPURLResponse(
|
||||
url: URL(string: "https://example.com")!,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: ["Content-Type": "application/json"]
|
||||
)),
|
||||
request: .default
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
try String(data: XCTUnwrap(subject.body), encoding: .utf8),
|
||||
"response body"
|
||||
)
|
||||
XCTAssertEqual(subject.headers, ["Content-Type": "application/json"])
|
||||
XCTAssertEqual(subject.statusCode, 200)
|
||||
XCTAssertEqual(subject.url, URL(string: "https://example.com")!)
|
||||
}
|
||||
|
||||
/// Initializing an `HTTPResponse` with an `URLResponse` vs a `HTTPURLResponse` throws an error.
|
||||
func testInitWithURLResponseThrowsError() {
|
||||
let urlResponse = URLResponse(
|
||||
url: URL(string: "https://example.com")!,
|
||||
mimeType: nil,
|
||||
expectedContentLength: 0,
|
||||
textEncodingName: nil
|
||||
)
|
||||
|
||||
XCTAssertThrowsError(
|
||||
try HTTPResponse(
|
||||
data: Data(),
|
||||
response: urlResponse,
|
||||
request: .default
|
||||
),
|
||||
"Expected a HTTPResponseError.invalidResponse error to be thrown"
|
||||
) { error in
|
||||
XCTAssertTrue(error is HTTPResponseError)
|
||||
XCTAssertEqual(
|
||||
error as? HTTPResponseError,
|
||||
HTTPResponseError.invalidResponse(urlResponse)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Networking/Tests/NetworkingTests/HTTPServiceTests.swift
Normal file
50
Networking/Tests/NetworkingTests/HTTPServiceTests.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class HTTPServiceTests: XCTestCase {
|
||||
var client: MockHTTPClient!
|
||||
var subject: HTTPService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
client = MockHTTPClient()
|
||||
|
||||
subject = HTTPService(
|
||||
baseURL: URL(string: "https://example.com")!,
|
||||
client: client
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
client = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
/// `send(_:)` forwards the request to the client and returns the response.
|
||||
func testSendRequest() async throws {
|
||||
let httpResponse = HTTPResponse.success()
|
||||
client.result = .success(httpResponse)
|
||||
|
||||
let response = try await subject.send(TestRequest())
|
||||
|
||||
XCTAssertEqual(response.httpResponse, httpResponse)
|
||||
}
|
||||
|
||||
/// `send(_:)` forwards the request to the client and throws if an error occurs.
|
||||
func testSendRequestError() async {
|
||||
client.result = .failure(RequestError())
|
||||
|
||||
do {
|
||||
_ = try await subject.send(TestRequest())
|
||||
XCTFail("Expected send(_:) to throw an error")
|
||||
} catch {
|
||||
XCTAssertTrue(error is RequestError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct RequestError: Error {}
|
||||
31
Networking/Tests/NetworkingTests/RequestBodyTests.swift
Normal file
31
Networking/Tests/NetworkingTests/RequestBodyTests.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class RequestBodyTests: XCTestCase {
|
||||
struct TestRequestBodyJSON: JSONRequestBody {
|
||||
static var encoder = JSONEncoder()
|
||||
|
||||
let name = "john"
|
||||
}
|
||||
|
||||
/// `JSONRequestBody` can encode the JSON request body and provide additional headers.
|
||||
func testRequestBodyJSON() throws {
|
||||
let subject = TestRequestBodyJSON()
|
||||
|
||||
XCTAssertEqual(subject.additionalHeaders, ["Content-Type": "application/json"])
|
||||
|
||||
let encodedData = try subject.encode()
|
||||
XCTAssertEqual(String(data: encodedData, encoding: .utf8), #"{"name":"john"}"#)
|
||||
}
|
||||
|
||||
/// Test that `Data` conforms to `RequestBody` and can be used directly.
|
||||
func testRequestBodyData() throws {
|
||||
let data = try XCTUnwrap("💾".data(using: .utf8))
|
||||
|
||||
let subject: RequestBody = data
|
||||
|
||||
XCTAssertEqual(subject.additionalHeaders, [:])
|
||||
XCTAssertEqual(try subject.encode(), data)
|
||||
}
|
||||
}
|
||||
19
Networking/Tests/NetworkingTests/RequestTests.swift
Normal file
19
Networking/Tests/NetworkingTests/RequestTests.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class RequestTests: XCTestCase {
|
||||
struct DefaultRequest: Request {
|
||||
typealias Response = String // swiftlint:disable:this nesting
|
||||
var path: String = "/path"
|
||||
}
|
||||
|
||||
/// `Request` default.
|
||||
func testRequest() {
|
||||
let request = DefaultRequest()
|
||||
XCTAssertEqual(request.method, .get)
|
||||
XCTAssertNil(request.body)
|
||||
XCTAssertEqual(request.headers, [:])
|
||||
XCTAssertEqual(request.query, [])
|
||||
}
|
||||
}
|
||||
32
Networking/Tests/NetworkingTests/ResponseTests.swift
Normal file
32
Networking/Tests/NetworkingTests/ResponseTests.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Networking
|
||||
|
||||
class ResponseTests: XCTestCase {
|
||||
/// Test creating a `Response` from JSON.
|
||||
func testJSONResponse() throws {
|
||||
let httpResponse = HTTPResponse(
|
||||
url: URL(string: "http://example.com")!,
|
||||
statusCode: 200,
|
||||
headers: [:],
|
||||
body: "{ \"field\": \"value\" }".data(using: .utf8)!,
|
||||
requestID: UUID()
|
||||
)
|
||||
let response = try TestJSONResponse(response: httpResponse)
|
||||
XCTAssertEqual(response.field, "value")
|
||||
}
|
||||
|
||||
/// Test creating a `Response` from a JSON array.
|
||||
func testJSONArray() throws {
|
||||
let httpResponse = HTTPResponse(
|
||||
url: URL(string: "http://example.com")!,
|
||||
statusCode: 200,
|
||||
headers: [:],
|
||||
body: "[{ \"field\": \"value\" }]".data(using: .utf8)!,
|
||||
requestID: UUID()
|
||||
)
|
||||
let response = try [TestJSONResponse](response: httpResponse)
|
||||
XCTAssertEqual(response.count, 1)
|
||||
XCTAssertEqual(response[0].field, "value")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
@testable import Networking
|
||||
|
||||
extension HTTPRequest {
|
||||
static let `default` = HTTPRequest(url: URL(string: "https://www.example.com")!)
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
@testable import Networking
|
||||
|
||||
extension HTTPResponse {
|
||||
static func success(
|
||||
string: String = "http://example.com",
|
||||
statusCode: Int = 200,
|
||||
headers: [String: String] = [:],
|
||||
body: Data = Data()
|
||||
) -> HTTPResponse {
|
||||
HTTPResponse(
|
||||
url: URL(string: string)!,
|
||||
statusCode: statusCode,
|
||||
headers: headers,
|
||||
body: body,
|
||||
requestID: UUID()
|
||||
)
|
||||
}
|
||||
|
||||
static func failure(
|
||||
string: String = "http://example.com",
|
||||
statusCode: Int = 500,
|
||||
headers: [String: String] = [:],
|
||||
body: Data = Data()
|
||||
) -> HTTPResponse {
|
||||
HTTPResponse(
|
||||
url: URL(string: string)!,
|
||||
statusCode: statusCode,
|
||||
headers: headers,
|
||||
body: body,
|
||||
requestID: UUID()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
@testable import Networking
|
||||
|
||||
class MockHTTPClient: HTTPClient {
|
||||
var result: Result<HTTPResponse, Error>?
|
||||
|
||||
func send(_ request: HTTPRequest) async throws -> HTTPResponse {
|
||||
guard let result else {
|
||||
throw MockClientError.noResultForRequest
|
||||
}
|
||||
return try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
enum MockClientError: Error {
|
||||
case noResultForRequest
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
|
||||
/// A mock `URLProtocol` used to mock networking requests using `URLSession`.
|
||||
///
|
||||
class MockURLProtocol: URLProtocol {
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
guard let url = request.url,
|
||||
URLProtocolMocking.response(for: url) != nil
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
guard let url = request.url,
|
||||
let result = URLProtocolMocking.response(for: url)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .success((response, data)):
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
case let .failure(error):
|
||||
client?.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
override func stopLoading() {}
|
||||
}
|
||||
3
Networking/Tests/NetworkingTests/Support/TestError.swift
Normal file
3
Networking/Tests/NetworkingTests/Support/TestError.swift
Normal file
@ -0,0 +1,3 @@
|
||||
enum TestError: Error, Equatable {
|
||||
case badResponse
|
||||
}
|
||||
49
Networking/Tests/NetworkingTests/Support/TestRequest.swift
Normal file
49
Networking/Tests/NetworkingTests/Support/TestRequest.swift
Normal file
@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
@testable import Networking
|
||||
|
||||
struct TestRequest: Request {
|
||||
typealias Response = TestResponse
|
||||
let path = "/test"
|
||||
}
|
||||
|
||||
struct TestResponse: Response {
|
||||
let httpResponse: HTTPResponse
|
||||
init(response: HTTPResponse) throws {
|
||||
httpResponse = response
|
||||
}
|
||||
}
|
||||
|
||||
struct TestJSONRequest: Request {
|
||||
typealias Response = TestJSONResponse
|
||||
let path = "/test.json"
|
||||
}
|
||||
|
||||
struct TestJSONResponse: JSONResponse {
|
||||
static var decoder: JSONDecoder { JSONDecoder() }
|
||||
|
||||
var field: String
|
||||
}
|
||||
|
||||
struct TestValidatingRequest: Request {
|
||||
typealias Response = TestResponse
|
||||
let path = "/test"
|
||||
|
||||
func validate(_ response: HTTPResponse) throws -> Result<HTTPResponse, Error> {
|
||||
throw TestError.badResponse
|
||||
}
|
||||
}
|
||||
|
||||
struct TestJSONRequestBody: JSONRequestBody {
|
||||
static var encoder: JSONEncoder { JSONEncoder() }
|
||||
|
||||
let field: String
|
||||
}
|
||||
|
||||
struct TestJSONBodyRequest: Request {
|
||||
typealias Response = TestResponse
|
||||
typealias Body = TestJSONRequestBody
|
||||
let path = "/test"
|
||||
|
||||
var body: TestJSONRequestBody?
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import Foundation
|
||||
|
||||
/// An object that manages mock responses for `MockURLProtocol`.
|
||||
///
|
||||
class URLProtocolMocking {
|
||||
typealias Response = Result<(URLResponse, Data), Error>
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The singleton mocking instance. Only one object should be used at a time since only one
|
||||
/// URL protocol can be registered at a time.
|
||||
private static let shared = URLProtocolMocking()
|
||||
|
||||
// MARK: Private properties
|
||||
|
||||
/// The queue used to synchronize access to the instance across threads.
|
||||
private let queue = DispatchQueue(label: "URLProtocolMocking")
|
||||
|
||||
/// The responses currently configured for mocking, by URL.
|
||||
private var responses: [URL: Response] = [:]
|
||||
|
||||
// MARK: Static Methods
|
||||
|
||||
/// Stub out requests for the given url with a fake response.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The url which will be matched against incoming requests.
|
||||
/// - response: The mock response to return when a request is made.
|
||||
///
|
||||
static func mock(_ url: URL, with response: Response) {
|
||||
shared.mock(url, with: response)
|
||||
}
|
||||
|
||||
/// Resets all mocks on the singleton instance.
|
||||
///
|
||||
/// Use this during tearDown() to remove all previously configured mocks.
|
||||
///
|
||||
static func reset() {
|
||||
shared.reset()
|
||||
}
|
||||
|
||||
/// Returns the mocked response for the given url.
|
||||
///
|
||||
/// - Parameter url: The url of an incoming request.
|
||||
/// - Returns: A response result to use for mocking or nil if the url is not matched.
|
||||
///
|
||||
static func response(for url: URL) -> Response? {
|
||||
shared.response(for: url)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// Stub out requests for the given url with a fake response.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The url which will be matched against incoming requests.
|
||||
/// - response: The mock response to return when a request is made.
|
||||
///
|
||||
private func mock(_ url: URL, with response: Response) {
|
||||
queue.sync {
|
||||
responses[url] = response
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets all mocks.
|
||||
///
|
||||
/// Use this during tearDown() to remove all previously configured mocks.
|
||||
///
|
||||
private func reset() {
|
||||
queue.sync {
|
||||
responses.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the mocked response for the given url.
|
||||
///
|
||||
/// - Parameter url: The url of an incoming request.
|
||||
/// - Returns: A response result to use for mocking or nil if the url is not matched.
|
||||
///
|
||||
private func response(for url: URL) -> Response? {
|
||||
queue.sync {
|
||||
responses[url]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,8 @@ packages:
|
||||
Firebase:
|
||||
url: https://github.com/firebase/firebase-ios-sdk
|
||||
exactVersion: 10.13.0
|
||||
Networking:
|
||||
path: Networking
|
||||
SnapshotTesting:
|
||||
url: https://github.com/pointfreeco/swift-snapshot-testing
|
||||
exactVersion: 1.11.1
|
||||
@ -40,6 +42,7 @@ schemes:
|
||||
- BitwardenAutoFillExtensionTests
|
||||
- BitwardenShareExtensionTests
|
||||
- BitwardenSharedTests
|
||||
- package: Networking/NetworkingTests
|
||||
BitwardenActionExtension:
|
||||
build:
|
||||
targets:
|
||||
@ -258,10 +261,11 @@ targets:
|
||||
randomExecutionOrder: true
|
||||
|
||||
BitwardenShared:
|
||||
type: library.static
|
||||
type: framework
|
||||
platform: iOS
|
||||
settings:
|
||||
base:
|
||||
APPLICATION_EXTENSION_API_ONLY: true
|
||||
INFOPLIST_FILE: BitwardenShared/Application/Support/Info.plist
|
||||
sources:
|
||||
- path: BitwardenShared
|
||||
@ -269,8 +273,7 @@ targets:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- package: Firebase
|
||||
product: FirebaseCrashlytics
|
||||
- package: Networking
|
||||
BitwardenSharedTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user