Skip to content

Commit

Permalink
chore: Add authentication tests (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: danthorpe <[email protected]>
  • Loading branch information
danthorpe and danthorpe authored Oct 12, 2023
1 parent f2a3e4d commit 93ee749
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ struct Authentication<Delegate: AuthenticationDelegate>: NetworkingModifier {
var credentials: Credentials
do {
credentials = try await delegate.fetch(for: request)
} catch let error as AuthenticationError {
continuation.finish(
throwing: error
)
return
} catch {
continuation.finish(
throwing: AuthenticationError.fetchCredentialsFailed(request, Credentials.method, error)
Expand Down Expand Up @@ -59,6 +64,8 @@ struct Authentication<Delegate: AuthenticationDelegate>: NetworkingModifier {
do {
credentials = try await delegate.refresh(unauthorized: credentials, from: response)
return credentials.apply(to: response.request)
} catch let error as AuthenticationError {
throw error
} catch {
throw AuthenticationError.refreshCredentialsFailed(response, Credentials.method, error)
}
Expand Down
75 changes: 42 additions & 33 deletions Sources/TestSupport/Mocked.swift
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
import Networking

extension NetworkingComponent {
public func mocked(
_ request: HTTPRequestData,
stub: StubbedResponseStream
) -> some NetworkingComponent {
modified(Mocked(request: request, with: stub))
}

public func mocked(
_ block: @escaping @Sendable (NetworkingComponent, HTTPRequestData) -> ResponseStream<HTTPResponseData>
) -> some NetworkingComponent {
modified(CustomMocked(block: block))
}
public func mocked(
_ stub: StubbedResponseStream,
check: @escaping (HTTPRequestData) -> Bool
) -> some NetworkingComponent {
modified(Mocked(mock: check, with: stub))
}

public func mocked(
_ request: HTTPRequestData,
stub: StubbedResponseStream
) -> some NetworkingComponent {
mocked(stub) { $0 ~= request }
}
}

struct Mocked: NetworkingModifier {
let mock: HTTPRequestData
let stub: StubbedResponseStream
let mock: (HTTPRequestData) -> Bool
let stub: StubbedResponseStream

@NetworkEnvironment(\.instrument) var instrument
@NetworkEnvironment(\.instrument) var instrument

init(request: HTTPRequestData, with stubbedResponse: StubbedResponseStream) {
self.mock = request
self.stub = stubbedResponse
}
init(mock: @escaping (HTTPRequestData) -> Bool, with stubbedResponse: StubbedResponseStream) {
self.mock = mock
self.stub = stubbedResponse
}

func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream<HTTPResponseData> {
guard request ~= mock else {
return upstream.send(request)
}
return ResponseStream { continuation in
Task {
await instrument?.measureElapsedTime("Mocked")
await stub(request).redirect(into: continuation)
}
}
func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream<HTTPResponseData> {
guard mock(request) else {
return upstream.send(request)
}
return ResponseStream { continuation in
Task {
await instrument?.measureElapsedTime("Mocked")
await stub(request).redirect(into: continuation)
}
}
}
}

extension NetworkingComponent {
public func mocked(
_ block: @escaping @Sendable (NetworkingComponent, HTTPRequestData) -> ResponseStream<HTTPResponseData>
) -> some NetworkingComponent {
modified(CustomMocked(block: block))
}
}

struct CustomMocked: NetworkingModifier {
let block: @Sendable (NetworkingComponent, HTTPRequestData) -> ResponseStream<HTTPResponseData>
let block: @Sendable (NetworkingComponent, HTTPRequestData) -> ResponseStream<HTTPResponseData>

func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream<HTTPResponseData> {
block(upstream, request)
}
func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream<HTTPResponseData> {
block(upstream, request)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import AssertionExtras
import ConcurrencyExtras
import Dependencies
import Foundation
import HTTPTypes
import Networking
import TestSupport
import XCTest

final class AuthenticationTests: XCTestCase {

override func invokeTest() {
withDependencies {
$0.shortID = .incrementing
$0.continuousClock = TestClock()
} operation: {
super.invokeTest()
}
}

func test__authentication() async throws {
let reporter = TestReporter()
let delegate = TestAuthenticationDelegate(
fetch: { _ in
BearerCredentials(token: "token")
}
)

let bearerAuthentication = BearerAuthentication(delegate: delegate)

var request = HTTPRequestData(authority: "example.com")
request.authenticationMethod = .bearer
let copy = request

let network = TerminalNetworkingComponent()
.mocked(.ok(), check: { _ in true })
.reported(by: reporter)
.authenticated(with: bearerAuthentication)

try await withMainSerialExecutor {
try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in

for _ in 0..<4 {
group.addTask {
return try await network.data(copy)
}
}

var responses: [HTTPResponseData] = []
for try await response in group {
responses.append(response)
}
XCTAssertTrue(responses.allSatisfy {
$0.request.headerFields[.authorization] == "Bearer token"
})
}

let reportedRequests = await reporter.requests
XCTAssertEqual(reportedRequests.count, 4)
XCTAssertTrue(reportedRequests.allSatisfy {
$0.headerFields[.authorization] == "Bearer token"
})

XCTAssertEqual(delegate.fetchCount, 1)
}
}

func test__authentication__when_delegate_throws_error_on_fetch() async throws {
struct CustomError: Error, Hashable { }

let delegate = TestAuthenticationDelegate<BearerCredentials>(
fetch: { _ in
throw CustomError()
}
)

let bearerAuthentication = BearerAuthentication(delegate: delegate)

var request = HTTPRequestData(authority: "example.com")
request.authenticationMethod = .bearer

let network = TerminalNetworkingComponent()
.authenticated(with: bearerAuthentication)

await XCTAssertThrowsError(
try await network.data(request),
matches: AuthenticationError.fetchCredentialsFailed(request, .bearer, CustomError())
)
}

func test__authentication__refresh_token() async throws {

var isUnauthorized = true
let reporter = TestReporter()
let delegate = TestAuthenticationDelegate(
fetch: { _ in
BearerCredentials(token: "token")
},
refresh: { _, _ in
BearerCredentials(token: "refreshed token")
}
)

let bearerAuthentication = BearerAuthentication(delegate: delegate)

var request = HTTPRequestData(authority: "example.com")
request.authenticationMethod = .bearer

let network = TerminalNetworkingComponent()
.mocked(.ok(), check: { _ in true })
.mocked(.status(.unauthorized), check: { _ in
defer { isUnauthorized.toggle() }
return isUnauthorized
})
.reported(by: reporter)
.authenticated(with: bearerAuthentication)

try await network.data(request)

let reportedRequests = await reporter.requests
XCTAssertEqual(reportedRequests.count, 2)
XCTAssertTrue(reportedRequests[0].headerFields[.authorization] == "Bearer token")
XCTAssertTrue(reportedRequests[1].headerFields[.authorization] == "Bearer refreshed token")
XCTAssertEqual(delegate.fetchCount, 1)
XCTAssertEqual(delegate.refreshCount, 1)
}
}

0 comments on commit 93ee749

Please sign in to comment.