Implemented the User Agent middleware (#6)

This PR contains the work done to implement the `UserAgentMiddleware` middleware that includes user agent information into a header of the requests sent by the `Client` type, as defined in the [Discogs documentation](https://www.discogs.com/developers/#page:home,header:home-general-information). For this purpose, the `CamelCaseValidationRule`, `SemanticVersionValidationRule` and `URLValidationRule` types were implemented and integrated into the existing `ValidateInputUseCase` type.

Reviewed-on: #6
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #6.
This commit is contained in:
2025-10-13 00:54:17 +00:00
committed by Javier Cicchelli
parent 24d703b967
commit 791ebf4f78
1988 changed files with 2882 additions and 2010 deletions
@@ -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]
}