Implemented the UserAgentMiddleware type in the library target.
This commit is contained in:
@@ -43,6 +43,7 @@ public struct AuthMiddleware {
|
||||
/// - Parameters:
|
||||
/// - method: A representation of an authentication method to use to authenticate requests.
|
||||
/// - transport: A representation of a transport option to send credentials in requests.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
public init(
|
||||
method: AuthMethod = .none,
|
||||
transport: AuthTransport
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import class OpenAPIRuntime.HTTPBody
|
||||
|
||||
import protocol OpenAPIRuntime.ClientMiddleware
|
||||
|
||||
import struct Foundation.URL
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
import struct HTTPTypes.HTTPResponse
|
||||
|
||||
/// A middleware that attaches the user agent header into the requests to the service.
|
||||
///
|
||||
/// Please refer to the [Discogs documentation](https://www.discogs.com/developers/#page:home,header:home-general-information) for further information.
|
||||
public struct UserAgentMiddleware {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A formatted value for the user agent header.
|
||||
let agentField: HTTPField
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this middleware.
|
||||
/// - Parameter product: A product from which the user agent will be generated from.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
public init(product: Product) throws {
|
||||
let agentName = ValidateInputUseCase(rules: .notNil, .notEmpty, .camelCase)
|
||||
let agentVersion = ValidateInputUseCase(rules: .notNil, .notEmpty, .semanticVersion)
|
||||
let agentURL = ValidateInputUseCase(rules: .notNil, .notEmpty, .url)
|
||||
|
||||
try agentName(product.name)
|
||||
try agentVersion(product.version)
|
||||
try agentURL(product.url)
|
||||
|
||||
self.agentField = .init(
|
||||
name: .userAgent,
|
||||
value: .init(format: .Format.userAgent, product.name, product.version, product.url)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ClientMiddleware
|
||||
|
||||
extension UserAgentMiddleware: ClientMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func intercept(
|
||||
_ request: HTTPRequest,
|
||||
body: HTTPBody?,
|
||||
baseURL: URL,
|
||||
operationID: String,
|
||||
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
|
||||
) async throws -> (HTTPResponse, HTTPBody?) {
|
||||
return try await next(
|
||||
.init(
|
||||
method: request.method,
|
||||
scheme: request.scheme,
|
||||
authority: request.authority,
|
||||
path: request.path,
|
||||
headerFields: userAgentHeader(request.headerFields)
|
||||
),
|
||||
body,
|
||||
baseURL
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension UserAgentMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Adds a user agent header to the existing header fields.
|
||||
/// - Parameter fields: A set of header fields to update.
|
||||
/// - Returns: An updated set of header fields including the user agent header.
|
||||
func userAgentHeader(_ fields: HTTPFields) -> HTTPFields {
|
||||
var fields = fields
|
||||
|
||||
fields.append(agentField)
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Format {
|
||||
/// A format for the user agent header.
|
||||
static let userAgent = "%@/%@ +%@"
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
import struct Foundation.URL
|
||||
import struct Foundation.URLComponents
|
||||
import struct Foundation.URLQueryItem
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
import struct HTTPTypes.HTTPResponse
|
||||
@@ -28,6 +27,7 @@ import Testing
|
||||
struct AuthMiddlewareTests {
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `initialize`(
|
||||
@@ -273,11 +273,11 @@ private extension AuthMiddlewareTests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the error throwing (if justified) during the initialization of the middleware.
|
||||
/// Asserts the error throwing (if justified) during the initialization of a middleware.
|
||||
/// - Parameters:
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - authTransport: A representation of an authentication transport.
|
||||
/// - error: An expected error of type ``InputValidationError`` during the initialization of the middleware.
|
||||
/// - error: An expected error of type ``InputValidationError`` during the initialization of a middleware.
|
||||
func assertInitThrows(
|
||||
authMethod: AuthMethod,
|
||||
authTransport: AuthTransport,
|
||||
@@ -331,8 +331,8 @@ private extension AuthMiddlewareTests {
|
||||
try await middleware.intercept(
|
||||
request,
|
||||
body: nil,
|
||||
baseURL: .baseURL,
|
||||
operationID: .operationId
|
||||
baseURL: .Sample.baseURL,
|
||||
operationID: .Sample.operationId
|
||||
) { request, _, _ in
|
||||
// THEN
|
||||
switch (authMethod, authTransport) {
|
||||
@@ -430,31 +430,6 @@ private extension AuthMiddlewareTests {
|
||||
|
||||
}
|
||||
|
||||
private extension HTTPRequest {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes a HTTP request conveniently.
|
||||
/// - Parameters:
|
||||
/// - method: A request method.
|
||||
/// - path: A value of the “:path” pseudo header field.
|
||||
/// - headerFields: A dictionary of request header fields.
|
||||
init(
|
||||
method: HTTPRequest.Method = .get,
|
||||
path: String?,
|
||||
headerFields: HTTPFields = [:]
|
||||
) {
|
||||
self.init(
|
||||
method: method,
|
||||
scheme: nil,
|
||||
authority: nil,
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
@@ -464,7 +439,7 @@ private extension Input {
|
||||
.user(token: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStT"),
|
||||
.none
|
||||
]
|
||||
/// A list of authentication methods to tests for the initialization throw test cases.
|
||||
/// A list of authentication methods to use in the initialization throw test cases.
|
||||
static let authMethodsThrows: [AuthMethod] = authMethods + [
|
||||
.consumer(key: .empty, secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiI", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
@@ -487,13 +462,3 @@ private extension Output {
|
||||
/// A list of expected boolean flags coming from the should authenticate test cases.
|
||||
static let authMethodsShouldAuthenticate: [Bool] = [true, true, false]
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// An operation ID sample.
|
||||
static let operationId = "SomeOperationId"
|
||||
}
|
||||
|
||||
private extension URL {
|
||||
/// A base URL sample.
|
||||
static let baseURL = URL(string: "https://sample.domain.com")!
|
||||
}
|
||||
|
||||
@@ -12,14 +12,215 @@
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import DiscogsService
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import DiscogsService
|
||||
|
||||
@Suite("User Agent Middleware", .tags(.middleware))
|
||||
struct UserAgentMiddlewareTests {
|
||||
|
||||
// @Test func <#test function name#>() async throws {
|
||||
// // Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
// }
|
||||
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `initialize`(
|
||||
product: Product
|
||||
) throws {
|
||||
try assertInit(product: product)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.userAgentsThrows,
|
||||
Output.userAgentsThrows
|
||||
))
|
||||
func `initialize throws`(
|
||||
product: Product,
|
||||
expect error: InputValidationError?
|
||||
) {
|
||||
assertInitThrows(
|
||||
product: product,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("initialize", arguments: Input.userAgents)
|
||||
func initialize(
|
||||
product: Product
|
||||
) throws {
|
||||
try assertInit(product: product)
|
||||
}
|
||||
|
||||
@Test("initialize throws", arguments: zip(
|
||||
Input.userAgentsThrows,
|
||||
Output.userAgentsThrows
|
||||
))
|
||||
func initializeThrows(
|
||||
product: Product,
|
||||
expect error: InputValidationError?
|
||||
) {
|
||||
assertInitThrows(
|
||||
product: product,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Functions tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `intercept with user agent on headers`(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(product: product)
|
||||
}
|
||||
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `intercept with user agent on headers when headers are populated`(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
product: product,
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("intercept with user agent on headers", arguments: Input.userAgents)
|
||||
func intercept_withUserAgentOnHeaders(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(product: product)
|
||||
}
|
||||
|
||||
@Test("intercept with user agent on headers when headers are populated", arguments: Input.userAgents)
|
||||
func intercept_withUserAgentOnHeaders_whenHeadersPopulated(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
product: product,
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension UserAgentMiddlewareTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Asserts the initialization of the middleware , especially the assignments of its properties.
|
||||
/// - Parameter product: A product to initialize a middleware.
|
||||
/// - Throws: an error of type ``InputValidationError`` in case of an unexpected error occurs while running test cases.
|
||||
func assertInit(
|
||||
product: Product
|
||||
) throws {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let middleware = try UserAgentMiddleware(product: product)
|
||||
|
||||
// THEN
|
||||
#expect(middleware.agentField == .init(
|
||||
name: .userAgent,
|
||||
value: "\(product.name)/\(product.version) +\(product.url)"
|
||||
))
|
||||
}
|
||||
|
||||
/// Asserts the error throwing (if justified) during the initialization of the middleware.
|
||||
/// - Parameters:
|
||||
/// - product: A product to initialize a middleware.
|
||||
/// - error: An expected error of type ``InputValidationError`` during the initialization of a middleware.
|
||||
func assertInitThrows(
|
||||
product: Product,
|
||||
expects error: InputValidationError?
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
// THEN
|
||||
if let error {
|
||||
#expect(throws: error) {
|
||||
try UserAgentMiddleware(product: product)
|
||||
}
|
||||
} else {
|
||||
#expect(throws: Never.self) {
|
||||
try UserAgentMiddleware(product: product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the interception of a request to add the user agent in its header.
|
||||
/// - Parameters:
|
||||
/// - product: A product to initialize a middleware.
|
||||
/// - path: A URI path for a request.
|
||||
/// - headerFields: A set of header fields for a request.
|
||||
func assertIntercept(
|
||||
product: Product,
|
||||
path: String? = nil,
|
||||
headerFields: HTTPFields = [:]
|
||||
) async throws {
|
||||
// GIVEN
|
||||
let middleware = try UserAgentMiddleware(product: product)
|
||||
let request = HTTPRequest(
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
|
||||
// WHEN
|
||||
_ = try await confirmation { confirmation in
|
||||
try await middleware.intercept(
|
||||
request,
|
||||
body: nil,
|
||||
baseURL: .Sample.baseURL,
|
||||
operationID: .Sample.operationId
|
||||
) { request, _, _ in
|
||||
// THEN
|
||||
#expect(request.path == path)
|
||||
#expect(request.headerFields != headerFields)
|
||||
#expect(request.headerFields.count == headerFields.count + 1)
|
||||
#expect(request.headerFields.contains(where: { $0.name == .userAgent }))
|
||||
|
||||
confirmation()
|
||||
|
||||
return (.init(status: .ok) , nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
/// A list of products to successfully initialize user agent middleware instances.
|
||||
static let userAgents: [Product] = [
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeOther4pp", version: "1.2.3-b1", url: "https://some-other.app"),
|
||||
.init(name: "Yet4notherApp", version: "0.8.8+alpha", url: "https://yet.another.app")
|
||||
]
|
||||
/// A list of products to use in the initialization throw test cases.
|
||||
static let userAgentsThrows: [Product] = userAgents + [
|
||||
.init(name: "Some App", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "Some-App", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: .empty, version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "v0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: .empty, url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: .empty),
|
||||
.init(name: "Some App", version: "v0.0.1", url: "www.some.app"),
|
||||
.init(name: "SomeApp", version: "v0.0.1", url: "www.some.app"),
|
||||
.init(name: "Some App", version: "0.0.1", url: "www.some.app"),
|
||||
]
|
||||
}
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected input validation errors (if thrown) coming from the initialization throw test cases.
|
||||
static let userAgentsThrows: [InputValidationError?] = [nil, nil, nil, .inputNotCamelCase, .inputNotCamelCase, .inputIsEmpty, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputIsEmpty, .inputNotURL, .inputNotURL, .inputIsEmpty, .inputNotCamelCase, .inputNotSemanticVersion, .inputNotCamelCase]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
|
||||
extension HTTPRequest {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes a HTTP request conveniently.
|
||||
/// - Parameters:
|
||||
/// - method: A request method.
|
||||
/// - path: A value of the “:path” pseudo header field.
|
||||
/// - headerFields: A dictionary of request header fields.
|
||||
init(
|
||||
method: HTTPRequest.Method = .get,
|
||||
path: String?,
|
||||
headerFields: HTTPFields = [:]
|
||||
) {
|
||||
self.init(
|
||||
method: method,
|
||||
scheme: nil,
|
||||
authority: nil,
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
extension String {
|
||||
/// A namespace assigned for string samples on test cases.
|
||||
enum Sample {
|
||||
/// An operation ID sample.
|
||||
static let operationId = "SomeOperationId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
/// A namespace assigned for URL samples on test cases.
|
||||
enum Sample {
|
||||
/// A base URL sample.
|
||||
static let baseURL = URL(string: "https://sample.domain.com")!
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user