* fix #203 NetworkTransport race, with main-isolated variables (#206) Co-authored-by: wti <wti@users.noreply.github.com> * feat/auth: Authentication implementation (#205) * feat: added authentication * feat(auth): improvement to error handling * feat(auth): updated errors and tests in OAuthClientRegistrar * feat(auth): updated ci conformance package version * feat(auth): bumped node version * feat(auth): updated ci conformance step * feat(auth): improvement to Linux support * chore: added docc plugin * chore: added spi yml * chore: added documentation step to ci * chore: increased timeout for test from 5 to 10m --------- Co-authored-by: Paal Øye-Strømme <paal.o.eye@gmail.com> Co-authored-by: wti <wti@users.noreply.github.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
216 lines
8.6 KiB
Swift
216 lines
8.6 KiB
Swift
@preconcurrency import Foundation
|
|
import Testing
|
|
|
|
@testable import MCP
|
|
|
|
#if canImport(FoundationNetworking)
|
|
import FoundationNetworking
|
|
#endif
|
|
|
|
#if swift(>=6.1)
|
|
|
|
enum MockResponses {
|
|
typealias Route = @Sendable (URLRequest) async throws -> (HTTPURLResponse, Data)
|
|
|
|
static func ephemeralConfiguration() -> URLSessionConfiguration {
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.protocolClasses = [MockURLProtocol.self]
|
|
return config
|
|
}
|
|
|
|
static func mockError(_ message: String) -> NSError {
|
|
NSError(
|
|
domain: "MockURLProtocolError", code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: message]
|
|
)
|
|
}
|
|
|
|
static func jsonRPCResult(id: Int) -> Data {
|
|
#"{"jsonrpc":"2.0","result":{"ok":true},"id":\#(id)}"#.data(using: .utf8)!
|
|
}
|
|
|
|
// MARK: - Route Builders
|
|
|
|
static func jsonSuccess(body: Data) -> Route {
|
|
{ request in
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, body)
|
|
}
|
|
}
|
|
|
|
static func bearerChallenge(
|
|
statusCode: Int = 401,
|
|
resourceMetadataURL: URL? = nil,
|
|
scope: String? = nil,
|
|
error: String? = nil,
|
|
errorDescription: String? = nil
|
|
) -> Route {
|
|
{ request in
|
|
var params: [String] = []
|
|
if let url = resourceMetadataURL {
|
|
params.append("resource_metadata=\"\(url.absoluteString)\"")
|
|
}
|
|
if let scope { params.append("scope=\"\(scope)\"") }
|
|
if let error { params.append("error=\"\(error)\"") }
|
|
if let errorDescription { params.append("error_description=\"\(errorDescription)\"") }
|
|
let headerValue = "Bearer \(params.joined(separator: ", "))"
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1",
|
|
headerFields: ["WWW-Authenticate": headerValue])!
|
|
return (response, Data())
|
|
}
|
|
}
|
|
|
|
static func resourceMetadata(
|
|
authorizationServers: [String],
|
|
scopesSupported: [String]? = nil,
|
|
resource: String? = nil
|
|
) -> Route {
|
|
{ request in
|
|
var dict: [String: Any] = ["authorization_servers": authorizationServers]
|
|
if let scopes = scopesSupported { dict["scopes_supported"] = scopes }
|
|
if let resource { dict["resource"] = resource }
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func asMetadata(
|
|
issuer: String,
|
|
tokenEndpoint: String,
|
|
authorizationEndpoint: String? = nil,
|
|
registrationEndpoint: String? = nil,
|
|
codeChallengeMethodsSupported: [String]? = nil,
|
|
tokenEndpointAuthMethodsSupported: [String]? = nil,
|
|
clientIDMetadataDocumentSupported: Bool? = nil
|
|
) -> Route {
|
|
{ request in
|
|
var dict: [String: Any] = ["issuer": issuer, "token_endpoint": tokenEndpoint]
|
|
if let v = authorizationEndpoint { dict["authorization_endpoint"] = v }
|
|
if let v = registrationEndpoint { dict["registration_endpoint"] = v }
|
|
if let v = codeChallengeMethodsSupported {
|
|
dict["code_challenge_methods_supported"] = v
|
|
}
|
|
if let v = tokenEndpointAuthMethodsSupported {
|
|
dict["token_endpoint_auth_methods_supported"] = v
|
|
}
|
|
if let v = clientIDMetadataDocumentSupported {
|
|
dict["client_id_metadata_document_supported"] = v
|
|
}
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func tokenSuccess(
|
|
accessToken: String,
|
|
expiresIn: Int = 3600,
|
|
scope: String? = nil,
|
|
refreshToken: String? = nil
|
|
) -> Route {
|
|
{ request in
|
|
var dict: [String: Any] = [
|
|
"access_token": accessToken, "token_type": "Bearer", "expires_in": expiresIn,
|
|
]
|
|
if let scope { dict["scope"] = scope }
|
|
if let refreshToken { dict["refresh_token"] = refreshToken }
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func tokenResponse(
|
|
accessToken: String,
|
|
tokenType: String,
|
|
expiresIn: Int = 3600
|
|
) -> Route {
|
|
{ request in
|
|
let dict: [String: Any] = [
|
|
"access_token": accessToken, "token_type": tokenType, "expires_in": expiresIn,
|
|
]
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func tokenError(
|
|
statusCode: Int = 400,
|
|
error: String,
|
|
errorDescription: String? = nil,
|
|
extraFields: [String: String] = [:]
|
|
) -> Route {
|
|
{ request in
|
|
var dict: [String: Any] = ["error": error]
|
|
if let errorDescription { dict["error_description"] = errorDescription }
|
|
for (key, value) in extraFields { dict[key] = value }
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func httpError(statusCode: Int) -> Route {
|
|
{ request in
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1",
|
|
headerFields: nil)!
|
|
return (response, Data())
|
|
}
|
|
}
|
|
|
|
static func registrationSuccess(
|
|
clientID: String,
|
|
clientSecret: String? = nil
|
|
) -> Route {
|
|
{ request in
|
|
var dict: [String: Any] = ["client_id": clientID]
|
|
if let clientSecret { dict["client_secret"] = clientSecret }
|
|
let data = try JSONSerialization.data(withJSONObject: dict)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: 201, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, data)
|
|
}
|
|
}
|
|
|
|
static func redirect(to location: String, statusCode: Int = 302) -> Route {
|
|
{ request in
|
|
let response = HTTPURLResponse(
|
|
url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1",
|
|
headerFields: ["Location": location])!
|
|
return (response, Data())
|
|
}
|
|
}
|
|
|
|
// MARK: - Routing Handler
|
|
|
|
static func routingHandler(
|
|
routes: [URL: Route]
|
|
) -> @Sendable (URLRequest) async throws -> (HTTPURLResponse, Data) {
|
|
{ request in
|
|
guard let url = request.url else { throw mockError("Missing request URL") }
|
|
guard let handler = routes[url] else {
|
|
throw mockError("Unexpected URL: \(url.absoluteString)")
|
|
}
|
|
return try await handler(request)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // swift(>=6.1)
|