Skip to content

Commit

Permalink
Add more APIs (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdbergeron authored Oct 12, 2023
1 parent 3f84ae2 commit ff7d288
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 8 deletions.
97 changes: 97 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/Stubby.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Stubby"
BuildableName = "Stubby"
BlueprintName = "Stubby"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "StubbyTests"
BuildableName = "StubbyTests"
BlueprintName = "StubbyTests"
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:Stubby.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "StubbyTests"
BuildableName = "StubbyTests"
BlueprintName = "StubbyTests"
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 = "Stubby"
BuildableName = "Stubby"
BlueprintName = "Stubby"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
110 changes: 104 additions & 6 deletions Sources/Stubby/URLSession+Stubbed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import Foundation

extension URLSession {
/// Create a `URLSession` with stubbed request handlers.
/// - Parameter responseProvider: The type of ``StubbyResponseProvider`` to use for handling requests.
/// - Parameter configuration: An `URLSessionConfiguration` object to be used by the created `URLSession`.
/// Defaults to `URLSessionConfiguration.ephemeral`.
/// - Parameter maintainExistingProtocolClasses: By default, the created `URLSession` will utilize _only_ the underlying
/// `StubbyURLProtocol` protocol class. To maintain any existing protocol classes specified by `URLSessionConfiguration.protocolClasses`,
/// set this to `true`.
/// - Parameters:
/// - responseProvider: The type of ``StubbyResponseProvider`` to use for handling requests.
/// - configuration: An `URLSessionConfiguration` object to be used by the created `URLSession`.
/// Defaults to `URLSessionConfiguration.ephemeral`.
/// - maintainExistingProtocolClasses: By default, the created `URLSession` will utilize _only_ the underlying `StubbyURLProtocol`
/// protocol class. To maintain any existing protocol classes specified by `URLSessionConfiguration.protocolClasses`, set this to `true`.
/// - Returns: A new ``URLSession`` instance.
public static func stubbed<ResponseProvider: StubbyResponseProvider>(
responseProvider: ResponseProvider.Type,
Expand All @@ -29,4 +29,102 @@ extension URLSession {
}
return URLSession(configuration: configuration)
}

/// Create a `URLSession` with stubbed request handlers.
/// - Parameters:
/// - configuration: An `URLSessionConfiguration` object to be used by the created `URLSession`.
/// Defaults to `URLSessionConfiguration.ephemeral`.
/// - maintainExistingProtocolClasses: By default, the created `URLSession` will utilize _only_ the underlying `StubbyURLProtocol`
/// protocol class. To maintain any existing protocol classes specified by `URLSessionConfiguration.protocolClasses`, set this to `true`.
/// - stubs: A list of ``Stub``s to use.
/// - Returns: A new ``URLSession`` instance.
public static func stubbed(
configuration: URLSessionConfiguration = .ephemeral,
maintainExistingProtocolClasses: Bool = false,
_ stubs: [Stub])
-> URLSession
{
for stub in stubs {
ResponseProvider.registerStub(stub)
}
return stubbed(
responseProvider: ResponseProvider.self,
configuration: configuration,
maintainExistingProtocolClasses: maintainExistingProtocolClasses)
}

/// Create a `URLSession` with stubbed request handlers.
/// - Parameters:
/// - configuration: An `URLSessionConfiguration` object to be used by the created `URLSession`.
/// Defaults to `URLSessionConfiguration.ephemeral`.
/// - maintainExistingProtocolClasses: By default, the created `URLSession` will utilize _only_ the underlying `StubbyURLProtocol`
/// protocol class. To maintain any existing protocol classes specified by `URLSessionConfiguration.protocolClasses`, set this to `true`.
/// - url: ``URL`` to stub.
/// - response: Response to stub.
/// - Returns: A new ``URLSession`` instance.
public static func stubbed(
configuration: URLSessionConfiguration = .ephemeral,
maintainExistingProtocolClasses: Bool = false,
url: URL,
response: @escaping (URLRequest) throws -> Result<StubbyResponse, Error>)
-> URLSession
{
stubbed(
configuration: configuration,
maintainExistingProtocolClasses: maintainExistingProtocolClasses,
[
.init(url: url, response: response),
])
}
}

// MARK: - Stub

public struct Stub {

// MARK: Lifecycle

public init(
url: URL,
response: @escaping (URLRequest) throws -> Result<StubbyResponse, Error>)
{
self.url = url
self.response = response
}

// MARK: Public

public let url: URL
public let response: (URLRequest) throws -> Result<StubbyResponse, Error>

}

// MARK: - ResponseProvider

private struct ResponseProvider: StubbyResponseProvider {

// MARK: Internal

static func registerStub(_ stub: Stub) {
stubs[stub.url] = stub
}

static func respondsTo(request: URLRequest) -> Bool {
true
}

static func response(for request: URLRequest) throws -> Result<StubbyResponse, Error> {
guard let url = request.url else {
throw URLError(.badURL)
}
guard let stub = stubs[url] else {
throw URLError(.unsupportedURL)
}
return try stub.response(request)
}

// MARK: Private

private static var stubs = [URL: Stub]()

}
32 changes: 32 additions & 0 deletions Stubby.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"configurations" : [
{
"id" : "13B3FAF7-8B85-4057-A8C6-94A4A86B2EC3",
"name" : "Test Scheme Action",
"options" : {

}
}
],
"defaultOptions" : {
"codeCoverage" : {
"targets" : [
{
"containerPath" : "container:",
"identifier" : "Stubby",
"name" : "Stubby"
}
]
}
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:",
"identifier" : "StubbyTests",
"name" : "StubbyTests"
}
}
],
"version" : 1
}
51 changes: 49 additions & 2 deletions Tests/StubbyTests/StubbyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ final class StubbyTests: XCTestCase {
}

func test_stubbyResponse_failsWithUnsupportedURLError() async {
let urlSession = URLSession.stubbed(responseProvider: TestResponseProvider.self)
let urlSession = URLSession.stubbed(url: .githubURL) { _ in
.failure(URLError(.unsupportedURL))
}
let request = URLRequest(url: .githubURL)
do {
_ = try await urlSession.data(for: request)
Expand All @@ -35,7 +37,11 @@ final class StubbyTests: XCTestCase {
}

func test_stubbyResponse_succeeds() async {
let urlSession = URLSession.stubbed(responseProvider: TestResponseProvider.self)
let urlSession = URLSession.stubbed(url: .repoURL) { request in
try .success(StubbyResponse(
data: XCTUnwrap("Hello, world!".data(using: .utf8)),
for: XCTUnwrap(request.url)))
}
let request = URLRequest(url: .repoURL)
do {
let (data, _) = try await urlSession.data(for: request)
Expand All @@ -45,6 +51,47 @@ final class StubbyTests: XCTestCase {
XCTFail("Unexpected error: \(error)")
}
}

func test_stubbyResponse_multipleStubs() async throws {
let githubExpectation = expectation(description: "github")
let repoExpectation = expectation(description: "repo")
let urlSession = URLSession.stubbed([
Stub(url: .githubURL) { request in
defer { githubExpectation.fulfill() }
return try .success(StubbyResponse(
data: XCTUnwrap("Github".data(using: .utf8)),
for: XCTUnwrap(request.url)))
},
Stub(url: .repoURL) { request in
defer { repoExpectation.fulfill() }
return try .success(StubbyResponse(
data: XCTUnwrap("Hello, world!".data(using: .utf8)),
for: XCTUnwrap(request.url)))
},
])
async let requests = [
urlSession.data(from: .githubURL),
urlSession.data(from: .repoURL),
]
let responses = try await requests
await fulfillment(of: [githubExpectation, repoExpectation], timeout: 1.0)
XCTAssertEqual(responses.count, 2)
let (githubData, _) = responses[0]
let (repoData, _) = responses[1]
XCTAssertEqual(String(data: githubData, encoding: .utf8), "Github")
XCTAssertEqual(String(data: repoData, encoding: .utf8), "Hello, world!")
do {
_ = try await urlSession.data(from: URL(string: "https://bradbergeron.com")!)
XCTFail("Should fail.")
} catch let error as NSError {
XCTAssertEqual(error.domain, URLError.errorDomain)
let expectedError = URLError(.unsupportedURL)
XCTAssertEqual(error.code, expectedError.errorCode)
XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}

// MARK: - URL
Expand Down

0 comments on commit ff7d288

Please sign in to comment.