diff --git a/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift b/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift index f1428b078..ac89ee175 100644 --- a/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift +++ b/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift @@ -10,33 +10,30 @@ import Foundation /// Builds http connector suitable for communications with ProcessOut API. final class ProcessOutHttpConnectorBuilder { - func with(configuration: HttpConnectorRequestMapperConfiguration) -> Self { - self.configuration = configuration - return self - } + /// Connector configuration. + var configuration: HttpConnectorRequestMapperConfiguration? - func with(sessionConfiguration: URLSessionConfiguration) -> Self { - self.sessionConfiguration = sessionConfiguration - return self - } + /// Retry strategy to use for failing requests. + var retryStrategy: RetryStrategy? = .exponential(maximumRetries: 3, interval: 0.1, rate: 3) - func with(retryStrategy: RetryStrategy?) -> Self { - self.retryStrategy = retryStrategy - return self - } + /// Logger. + var logger: POLogger? - func with(deviceMetadataProvider: DeviceMetadataProvider) -> Self { - self.deviceMetadataProvider = deviceMetadataProvider - return self - } + /// Device metadata provider. + var deviceMetadataProvider: DeviceMetadataProvider? - func with(logger: POLogger) -> Self { - self.logger = logger - return self - } + /// Session configuration. + lazy var sessionConfiguration: URLSessionConfiguration = { + let configuration = URLSessionConfiguration.default + configuration.urlCache = nil + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.waitsForConnectivity = true + configuration.timeoutIntervalForRequest = Constants.requestTimeout + return configuration + }() func build() -> HttpConnector { - guard let configuration, let logger else { + guard let configuration, let logger, let deviceMetadataProvider else { fatalError("Unable to create connector without required parameters set.") } let requestMapper = DefaultHttpConnectorRequestMapper( @@ -66,30 +63,6 @@ final class ProcessOutHttpConnectorBuilder { // MARK: - Private Properties - /// Connector configuration. - private var configuration: HttpConnectorRequestMapperConfiguration? - - /// Retry strategy to use for failing requests. - private var retryStrategy: RetryStrategy? = .exponential(maximumRetries: 3, interval: 0.1, rate: 3) - - /// Logger. - private var logger: POLogger? - - /// Session configuration. - private lazy var sessionConfiguration: URLSessionConfiguration = { - let configuration = URLSessionConfiguration.default - configuration.urlCache = nil - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - configuration.waitsForConnectivity = true - configuration.timeoutIntervalForRequest = Constants.requestTimeout - return configuration - }() - - /// Device metadata provider. - private lazy var deviceMetadataProvider: DeviceMetadataProvider = { - DefaultDeviceMetadataProvider(screen: .main, bundle: .main) - }() - private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = Constants.dateFormat @@ -111,3 +84,31 @@ final class ProcessOutHttpConnectorBuilder { return encoder }() } + +extension ProcessOutHttpConnectorBuilder { + + func with(configuration: HttpConnectorRequestMapperConfiguration) -> Self { + self.configuration = configuration + return self + } + + func with(sessionConfiguration: URLSessionConfiguration) -> Self { + self.sessionConfiguration = sessionConfiguration + return self + } + + func with(retryStrategy: RetryStrategy?) -> Self { + self.retryStrategy = retryStrategy + return self + } + + func with(deviceMetadataProvider: DeviceMetadataProvider) -> Self { + self.deviceMetadataProvider = deviceMetadataProvider + return self + } + + func with(logger: POLogger) -> Self { + self.logger = logger + return self + } +} diff --git a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift index 2e0ddd61d..c950b8520 100644 --- a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift +++ b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift @@ -11,13 +11,17 @@ import Foundation public typealias ProcessOutApiConfiguration = ProcessOutConfiguration /// Defines configuration parameters that are used to create API singleton. In order to create instance -/// of this structure one should use ``ProcessOutConfiguration/production(projectId:isDebug:)`` +/// of this structure one should use ``ProcessOutConfiguration/production(projectId:appVersion:isDebug:)`` /// method. public struct ProcessOutConfiguration { /// Project id. public let projectId: String + /// Host application version. Providing this value helps ProcessOut to troubleshoot potential + /// issues. + public let appVersion: String? + /// Boolean value that indicates whether SDK should operate in debug mode. At this moment it /// only affects logging level. /// - NOTE: Debug logs may contain sensitive data. @@ -39,13 +43,14 @@ public struct ProcessOutConfiguration { extension ProcessOutConfiguration { /// Creates production configuration. - public static func production(projectId: String, isDebug: Bool = false) -> Self { + public static func production(projectId: String, appVersion: String? = nil, isDebug: Bool = false) -> Self { // swiftlint:disable force_unwrapping let apiBaseUrl = URL(string: "https://api.processout.com")! let checkoutBaseUrl = URL(string: "https://checkout.processout.com")! // swiftlint:enable force_unwrapping return ProcessOutConfiguration( projectId: projectId, + appVersion: appVersion, isDebug: isDebug, privateKey: nil, apiBaseUrl: apiBaseUrl, @@ -58,6 +63,7 @@ extension ProcessOutConfiguration { public static func test(projectId: String, privateKey: String?, apiBaseUrl: URL, checkoutBaseUrl: URL) -> Self { ProcessOutConfiguration( projectId: projectId, + appVersion: nil, isDebug: true, privateKey: privateKey, apiBaseUrl: apiBaseUrl, diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index db2c4acf0..d712553b7 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -105,7 +105,7 @@ public final class ProcessOut { static let serviceLoggerCategory = "Service" static let repositoryLoggerCategory = "Repository" static let connectorLoggerCategory = "Connector" - static let systemLoggerSubsystem = "com.processout.processout-ios" + static let bundleIdentifier = "com.processout.processout-ios" } // MARK: - Private Properties @@ -114,15 +114,25 @@ public final class ProcessOut { private lazy var repositoryLogger = createLogger(for: Constants.repositoryLoggerCategory) private lazy var httpConnector: HttpConnector = { - let connectorConfiguration = HttpConnectorRequestMapperConfiguration( + let configuration = HttpConnectorRequestMapperConfiguration( baseUrl: configuration.apiBaseUrl, projectId: configuration.projectId, privateKey: configuration.privateKey, - version: ProcessOut.version + version: ProcessOut.version, + appVersion: configuration.appVersion ) + let keychain = Keychain(service: Constants.bundleIdentifier) + let deviceMetadataProvider = DefaultDeviceMetadataProvider( + screen: .main, device: .current, bundle: .main, keychain: keychain + ) + // Connector logs are not sent to backend to avoid recursion. This + // may be not ideal because we may loose important events, such + // as decoding failures so approach may be reconsidered in future. + let logger = createLogger(for: Constants.connectorLoggerCategory, includeRemoteDestination: false) let connector = ProcessOutHttpConnectorBuilder() - .with(configuration: connectorConfiguration) + .with(configuration: configuration) .with(logger: logger) + .with(deviceMetadataProvider: deviceMetadataProvider) .build() return connector }() @@ -144,12 +154,18 @@ public final class ProcessOut { self.configuration = configuration } - private func createLogger(for category: String) -> POLogger { + private func createLogger(for category: String, includeRemoteDestination: Bool = true) -> POLogger { let destinations: [LoggerDestination] = [ - SystemLoggerDestination(subsystem: Constants.systemLoggerSubsystem, category: category) + SystemLoggerDestination(subsystem: Constants.bundleIdentifier) ] + // todo(andrii-vysotskyi): uncomment code bellow when backend will support accepting SDK logs. + // if includeRemoteDestination { + // let repository = HttpLogsRepository(connector: httpConnector) + // let service = DefaultLogsService(repository: repository, minimumLevel: .error) + // destinations.append(service) + // } let minimumLevel: LogLevel = configuration.isDebug ? .debug : .info - return POLogger(destinations: destinations, minimumLevel: minimumLevel) + return POLogger(destinations: destinations, category: category, minimumLevel: minimumLevel) } private func prewarm() { diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift index 4c6327aaa..16971db5a 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift @@ -6,7 +6,6 @@ // import Foundation -import UIKit.UIDevice final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { @@ -38,14 +37,7 @@ final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { if let encodedBody = try encodedRequestBody(request) { sessionRequest.httpBody = encodedBody } - let defaultHeaders = [ - "Idempotency-Key": request.id, - "User-Agent": userAgent, - "Accept-Language": Strings.preferredLocalization, - "Content-Type": "application/json", - "Authorization": try authorization(request: request) - ] - defaultHeaders.forEach { field, value in + defaultHeaders(for: request).forEach { field, value in sessionRequest.setValue(value, forHTTPHeaderField: field) } request.headers.forEach { field, value in @@ -61,18 +53,7 @@ final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { private let deviceMetadataProvider: DeviceMetadataProvider private let logger: POLogger - private var userAgent: String { - let components = [ - UIDevice.current.systemName, - "Version", - UIDevice.current.systemVersion, - "ProcessOut iOS-Bindings", - configuration.version - ] - return components.joined(separator: "/") - } - - // MARK: - Private Methods + // MARK: - Request Body Encoding private func encodedRequestBody(_ request: HttpConnectorRequest) throws -> Data? { let decoratedBody: Encodable? @@ -92,18 +73,48 @@ final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { } } - private func authorization(request: HttpConnectorRequest) throws -> String { + // MARK: - Request Headers + + private func authorization(request: HttpConnectorRequest) -> String { var value = configuration.projectId + ":" if request.requiresPrivateKey { if let privateKey = configuration.privateKey { value += privateKey } else { - logger.info("Private key is required by '\(request.id)' request but not set") - throw HttpConnectorFailure.internal + preconditionFailure("Private key is required by '\(request.id)' request but not set") } } return "Basic " + Data(value.utf8).base64EncodedString() } + + private func defaultHeaders(for request: HttpConnectorRequest) -> [String: String] { + let deviceMetadata = deviceMetadataProvider.deviceMetadata + let headers = [ + "Idempotency-Key": request.id, + "User-Agent": userAgent(deviceMetadata: deviceMetadata), + "Accept-Language": Strings.preferredLocalization, + "Content-Type": "application/json", + "Authorization": authorization(request: request), + "Installation-Id": deviceMetadata.installationId, + "Device-Id": deviceMetadata.id, + "Device-System-Name": deviceMetadata.channel, + "Device-System-Version": deviceMetadata.systemVersion, + "Product-Version": configuration.version, + "Host-Application-Version": configuration.appVersion + ] + return headers.compactMapValues { $0 } + } + + private func userAgent(deviceMetadata: DeviceMetadata) -> String { + let components = [ + deviceMetadata.channel, + "Version", + deviceMetadata.systemVersion, + "ProcessOut iOS-Bindings", + configuration.version + ] + return components.joined(separator: "/") + } } /// Helps avoid using `JSONSerialization` to encode additional device metadata in request body. diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapperConfiguration.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapperConfiguration.swift index 07181c94f..bd420384d 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapperConfiguration.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapperConfiguration.swift @@ -20,4 +20,7 @@ struct HttpConnectorRequestMapperConfiguration { /// SDK version. let version: String + + /// Host application version. + let appVersion: String? } diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift index 87569ed5d..3b558bd56 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift @@ -71,7 +71,7 @@ final class UrlSessionHttpConnector: HttpConnector { _ valueType: Value.Type, from data: Data?, response: URLResponse?, error: Error?, requestId: String ) throws -> Value { if let error { - logger.debug("Request \(requestId) did fail with error: '\(error.localizedDescription)'.") + logger.info("Request \(requestId) did fail with error: '\(error.localizedDescription)'.") throw convertToFailure(urlError: error) } guard let data, let response = response as? HTTPURLResponse else { diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift index 9f4974793..062c63c1c 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift @@ -9,27 +9,47 @@ import UIKit final class DefaultDeviceMetadataProvider: DeviceMetadataProvider { - init(screen: UIScreen, bundle: Bundle) { + init(screen: UIScreen, device: UIDevice, bundle: Bundle, keychain: Keychain) { self.screen = screen + self.device = device self.bundle = bundle - timeZone = .autoupdatingCurrent + self.keychain = keychain } // MARK: - DeviceMetadataProvider var deviceMetadata: DeviceMetadata { DeviceMetadata( + id: .init(value: deviceId), + installationId: .init(value: device.identifierForVendor?.uuidString), + systemVersion: .init(value: device.systemVersion), appLanguage: bundle.preferredLocalizations.first!, // swiftlint:disable:this force_unwrapping appScreenWidth: Int(screen.nativeBounds.width), // Specified in pixels appScreenHeight: Int(screen.nativeBounds.height), - appTimeZoneOffset: timeZone.secondsFromGMT() / 60, - channel: "ios" + appTimeZoneOffset: TimeZone.current.secondsFromGMT() / 60, + channel: device.systemName.lowercased() ) } + // MARK: - Private Nested Types + + private enum Constants { + static let keychainDeviceId = "DeviceId" + } + // MARK: - Private Properties private let screen: UIScreen + private let device: UIDevice private let bundle: Bundle - private let timeZone: TimeZone + private let keychain: Keychain + + private lazy var deviceId: String? = { + if let deviceId = keychain.genericPassword(forAccount: Constants.keychainDeviceId) { + return deviceId + } + let deviceId = UUID().uuidString + keychain.add(genericPassword: deviceId, account: Constants.keychainDeviceId) + return deviceId + }() } diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift index 989c14a6c..1897a7059 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift @@ -9,6 +9,18 @@ import Foundation struct DeviceMetadata: Encodable { + /// Current device identifier. + @POImmutableExcludedCodable + var id: String? + + /// Installation identifier. Value changes if host application is reinstalled. + @POImmutableExcludedCodable + var installationId: String? + + /// Device system version. + @POImmutableExcludedCodable + var systemVersion: String + /// Default app language. let appLanguage: String @@ -21,6 +33,6 @@ struct DeviceMetadata: Encodable { /// Time zone offset in minutes. let appTimeZoneOffset: Int - /// Device channel. + /// Device channel. Holds device system name. let channel: String } diff --git a/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift b/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift new file mode 100644 index 000000000..e83a65b6b --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift @@ -0,0 +1,40 @@ +// +// Keychain.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Foundation +import Security + +final class Keychain { + + init(service: String) { + queryBuilder = KeychainQueryBuilder(service: service) + } + + @discardableResult + func add(genericPassword: String, account: String) -> Bool { + var builder = queryBuilder + builder.valueData = Data(genericPassword.utf8) + builder.account = account + return SecItemAdd(builder.build() as CFDictionary, nil) == errSecSuccess + } + + func genericPassword(forAccount account: String) -> String? { + var builder = queryBuilder + builder.account = account + builder.shouldReturnData = true + var result: AnyObject? + let status = SecItemCopyMatching(builder.build() as CFDictionary, &result) + guard status == errSecSuccess, let data = result as? Data else { + return nil + } + return String(decoding: data, as: UTF8.self) + } + + // MARK: - Private Properties + + private let queryBuilder: KeychainQueryBuilder +} diff --git a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift new file mode 100644 index 000000000..6bffcd397 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift @@ -0,0 +1,19 @@ +// +// KeychainItemAccessibility.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Security + +struct KeychainItemAccessibility: RawRepresentable { + + let rawValue: CFString + + /// The data in the keychain item cannot be accessed after a restart until + /// the device has been unlocked once by the user. + static let accessibleAfterFirstUnlockThisDeviceOnly = KeychainItemAccessibility( + rawValue: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + ) +} diff --git a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift new file mode 100644 index 000000000..40566e251 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift @@ -0,0 +1,16 @@ +// +// KeychainItemClass.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Security + +struct KeychainItemClass: RawRepresentable { + + let rawValue: CFString + + /// Generic password item. + static let genericPassword = KeychainItemClass(rawValue: kSecClassGenericPassword) +} diff --git a/Sources/ProcessOut/Sources/Core/Keychain/KeychainQueryBuilder.swift b/Sources/ProcessOut/Sources/Core/Keychain/KeychainQueryBuilder.swift new file mode 100644 index 000000000..55641b783 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/Keychain/KeychainQueryBuilder.swift @@ -0,0 +1,42 @@ +// +// KeychainQueryBuilder.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Foundation +import Security + +struct KeychainQueryBuilder { + + /// Item's class. + var itemClass: KeychainItemClass = .genericPassword + + /// Indicates when the keychain item is accessible. + var accessibility: KeychainItemAccessibility = .accessibleAfterFirstUnlockThisDeviceOnly + + /// String indicating the item's service. + var service: String? + + /// String indicating the item's account name. + var account: String? + + /// Item's data. + var valueData: Data? + + /// Indicating whether or not to return item data. + var shouldReturnData: Bool? + + func build() -> CFDictionary { + var query: [CFString: Any] = [ + kSecClass: itemClass.rawValue, + kSecAttrAccessible: accessibility.rawValue + ] + query[kSecAttrService] = service + query[kSecAttrAccount] = account + query[kSecValueData] = valueData + query[kSecReturnData] = shouldReturnData + return query as CFDictionary + } +} diff --git a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift index 1a4390dc5..33a2a46f6 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift @@ -10,21 +10,29 @@ import os final class SystemLoggerDestination: LoggerDestination { - func log(entry: LogEntry) { - let logType = convertToLogType(entry.level) - let message = entry.message.interpolation.value - os_log("[%{public}@:%{public}ld] %{public}@", log: logger, type: logType, entry.file, entry.line, message) + init(subsystem: String) { + self.subsystem = subsystem + lock = NSLock() + logs = [:] } - // MARK: - + func log(event: LogEvent) { + os_log( + "%{public}@ %{public}@", + log: osLog(category: event.category), + type: convertToLogType(event.level), + attributesDescription(event: event), + event.message + ) + } - private let logger: OSLog + // MARK: - Private Properties - init(subsystem: String, category: String) { - logger = OSLog(subsystem: subsystem, category: category) - } + private let subsystem: String + private let lock: NSLock + private var logs: [String: OSLog] - // MARK: - + // MARK: - Private Methods private func convertToLogType(_ level: LogLevel) -> OSLogType { switch level { @@ -38,4 +46,29 @@ final class SystemLoggerDestination: LoggerDestination { return .fault } } + + private func osLog(category: String) -> OSLog { + let log = lock.withLock { + if let log = logs[category] { + return log + } + let log = OSLog(subsystem: subsystem, category: category) + logs[category] = log + return log + } + return log + } + + private func attributesDescription(event: LogEvent) -> String { + struct Attribute { + let key, value: String + } + var attributes: [Attribute] = [ + Attribute(key: event.file, value: event.line.description) + ] + event.additionalAttributes.forEach { key, value in + attributes.append(Attribute(key: key, value: value)) + } + return attributes.map { "[" + $0.key + ":" + $0.value + "]" } .joined() + } } diff --git a/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift index aba16dfc3..b98ca8fb6 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift @@ -7,6 +7,6 @@ protocol LoggerDestination { - /// Logs given message. - func log(entry: LogEntry) + /// Logs given event. + func log(event: LogEvent) } diff --git a/Sources/ProcessOut/Sources/Core/Logger/Models/LogEntry.swift b/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift similarity index 60% rename from Sources/ProcessOut/Sources/Core/Logger/Models/LogEntry.swift rename to Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift index 109e5c19f..a877748ad 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Models/LogEntry.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift @@ -1,5 +1,5 @@ // -// LogEntry.swift +// LogEvent.swift // ProcessOut // // Created by Andrii Vysotskyi on 25.10.2022. @@ -7,13 +7,16 @@ import Foundation -struct LogEntry { +struct LogEvent { /// Logging level. let level: LogLevel /// Actual log message. - let message: LogMessage + let message: String + + /// The string that categorizes event. + let category: String /// Date associated with message. let timestamp: Date @@ -23,4 +26,7 @@ struct LogEntry { /// Line number. let line: Int + + /// Additional attributes. + let additionalAttributes: [String: String] } diff --git a/Sources/ProcessOut/Sources/Core/Logger/Models/LogLevel.swift b/Sources/ProcessOut/Sources/Core/Logger/Models/LogLevel.swift index 9d2acf726..779f2503b 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Models/LogLevel.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Models/LogLevel.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 25.10.2022. // -enum LogLevel: Int { +enum LogLevel: Int, Comparable { /// The debug log level. Use this level to capture information that may be useful during development or while /// troubleshooting a specific problem. @@ -21,4 +21,10 @@ enum LogLevel: Int { /// The fault log level. Use this level only to capture system-level or multi-process information when reporting /// system errors. case fault + + // MARK: - Comparable + + static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { + lhs.rawValue < rhs.rawValue + } } diff --git a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift index bf98cc37f..ba40f9f01 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift @@ -9,52 +9,89 @@ import Foundation /// An object for writing interpolated string messages to the processout logging system. @_spi(PO) -public final class POLogger { +public struct POLogger { - init(destinations: [LoggerDestination] = [], minimumLevel: LogLevel = .debug) { + init(destinations: [LoggerDestination] = [], category: String, minimumLevel: LogLevel = .debug) { self.destinations = destinations + self.category = category self.minimumLevel = minimumLevel + self.attributes = [:] + lock = NSLock() } - /// Records a message at the specified log level. Use this method when you need to adjust the log level - /// dynamically for a given message. - /// - /// - Parameters: - /// - level: The log level at which to store the message. This value determines the severity of the message and - /// whether the system persists it to disk. You may specify a constant or variable for this parameter. - /// - message: the message you want to add to the logs. - func log(level: LogLevel, _ message: LogMessage, file: String = #file, line: Int = #line) { - guard level.rawValue >= minimumLevel.rawValue else { - return + /// Add, change, or remove a logging attribute. + subscript(attributeKey attributeKey: String) -> String? { + get { + lock.withLock { attributes[attributeKey] } + } + set { + lock.withLock { attributes[attributeKey] = newValue } } - // swiftlint:disable:next legacy_objc_type - let fileName = NSString(string: NSString(string: file).deletingPathExtension).lastPathComponent - let entry = LogEntry(level: level, message: message, timestamp: Date(), file: fileName, line: line) - destinations.forEach { $0.log(entry: entry) } } + let category: String + /// Logs a message at the `debug` level. - func debug(_ message: LogMessage, file: String = #file, line: Int = #line) { - log(level: .debug, message, file: file, line: line) + func debug(_ message: LogMessage, attributes: [String: String] = [:], file: String = #file, line: Int = #line) { + log(level: .debug, message, attributes: attributes, file: file, line: line) } /// Logs a message at the `info` level. - func info(_ message: LogMessage, file: String = #file, line: Int = #line) { - log(level: .info, message, file: file, line: line) + func info(_ message: LogMessage, attributes: [String: String] = [:], file: String = #file, line: Int = #line) { + log(level: .info, message, attributes: attributes, file: file, line: line) } /// Logs a message at the `error` level. - func error(_ message: LogMessage, file: String = #file, line: Int = #line) { - log(level: .error, message, file: file, line: line) + func error(_ message: LogMessage, attributes: [String: String] = [:], file: String = #file, line: Int = #line) { + log(level: .error, message, attributes: attributes, file: file, line: line) } /// Logs a message at the `fault` level. - func fault(_ message: LogMessage, file: String = #file, line: Int = #line) { - log(level: .fault, message, file: file, line: line) + func fault(_ message: LogMessage, attributes: [String: String] = [:], file: String = #file, line: Int = #line) { + log(level: .fault, message, attributes: attributes, file: file, line: line) } - // MARK: - + // MARK: - Private Properties private let destinations: [LoggerDestination] private let minimumLevel: LogLevel + private let lock: NSLock + private var attributes: [String: String] + + // MARK: - Private Methods + + /// Records a message at the specified log level. Use this method when you need to adjust the log level + /// dynamically for a given message. + /// + /// - Parameters: + /// - level: The log level at which to store the message. This value determines the severity of the message and + /// whether the system persists it to disk. You may specify a constant or variable for this parameter. + /// - message: the message you want to add to the logs. + /// - attributes: additional attributes to log alongside primary logger attributes. + private func log( + level: LogLevel, + _ message: LogMessage, + attributes additionalAttributes: [String: String] = [:], + file: String = #file, + line: Int = #line + ) { + guard level >= minimumLevel else { + return + } + var attributes = lock.withLock { self.attributes } + additionalAttributes.forEach { key, value in + attributes[key] = value + } + let entry = LogEvent( + level: level, + message: message.interpolation.value, + category: category, + timestamp: Date(), + // swiftlint:disable:next legacy_objc_type + file: NSString(string: NSString(string: file).deletingPathExtension).lastPathComponent, + line: line, + additionalAttributes: attributes + ) + destinations.forEach { $0.log(event: entry) } + } } diff --git a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index d81a4eab8..5922a159a 100644 --- a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift +++ b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift @@ -121,6 +121,10 @@ extension InvoicesRepository { } } +@available(iOS 13.0, *) +extension LogsRepository { +} + @available(iOS 13.0, *) extension POCardsService { diff --git a/Sources/ProcessOut/Sources/Repositories/Logs/HttpLogsRepository.swift b/Sources/ProcessOut/Sources/Repositories/Logs/HttpLogsRepository.swift new file mode 100644 index 000000000..781a2a9d0 --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Logs/HttpLogsRepository.swift @@ -0,0 +1,26 @@ +// +// HttpLogsRepository.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Foundation + +final class HttpLogsRepository: LogsRepository { + + init(connector: HttpConnector) { + self.connector = connector + } + + // MARK: - LogsRepository + + func send(request: LogRequest) { + let httpRequest = HttpConnectorRequest.post(path: "/logs", body: request) + connector.execute(request: httpRequest) { _ in } + } + + // MARK: - Private Properties + + private let connector: HttpConnector +} diff --git a/Sources/ProcessOut/Sources/Repositories/Logs/LogRequest.swift b/Sources/ProcessOut/Sources/Repositories/Logs/LogRequest.swift new file mode 100644 index 000000000..5746f84b3 --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Logs/LogRequest.swift @@ -0,0 +1,26 @@ +// +// LogRequest.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Foundation + +struct LogRequest: Encodable { + + /// Log level. + let level: String + + /// Event timestamp. + let date: Date + + /// Actual log message. + let message: String + + /// Event type. Could be module name, category etc. + let eventType: String + + /// Arbitrary attributes. + let attributes: [String: String] +} diff --git a/Sources/ProcessOut/Sources/Repositories/Logs/LogsRepository.swift b/Sources/ProcessOut/Sources/Repositories/Logs/LogsRepository.swift new file mode 100644 index 000000000..06e6f38d0 --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Logs/LogsRepository.swift @@ -0,0 +1,12 @@ +// +// LogsRepository.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +protocol LogsRepository: PORepository { + + /// Sends given log event. + func send(request: LogRequest) +} diff --git a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift index b8d5919a5..1d0332eb6 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift @@ -81,7 +81,7 @@ final class DefaultThreeDSService: ThreeDSService { } self.complete(with: response, completion: completion) case let .failure(failure): - logger.error("Failed to create authentication request: \(failure)") + logger.info("Failed to create authentication request: \(failure)") completion(.failure(failure)) } } @@ -105,7 +105,7 @@ final class DefaultThreeDSService: ThreeDSService { : Constants.challengeFailureEncodedResponse completion(.success(Constants.tokenPrefix + encodedResponse)) case let .failure(failure): - logger.error("Failed to handle challenge: \(failure)") + logger.info("Failed to handle challenge: \(failure)") completion(.failure(failure)) } } @@ -136,7 +136,7 @@ final class DefaultThreeDSService: ThreeDSService { } self.complete(with: response, completion: completion) case let .failure(failure): - self.logger.error("Failed to handle url fingeprint: \(failure)") + self.logger.info("Failed to handle url fingeprint: \(failure)") completion(.failure(failure)) } } diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift index 5c215481c..74f5be668 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift @@ -19,9 +19,7 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> URL { guard var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true) else { - let message = "Can't create components from base url." - logger.error("\(message)") - fatalError(message) + preconditionFailure("Failed to create components from base url.") } let pathComponents: [String] if let tokenId = request.tokenId, let customerId = request.customerId { @@ -34,9 +32,7 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods URLQueryItem(name: "additional_data[" + data.key + "]", value: data.value) } guard let url = components.url else { - let message = "Failed to create APM redirection URL." - logger.error("\(message)") - fatalError(message) + preconditionFailure("Failed to create APM redirection URL.") } return url } @@ -45,13 +41,13 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems else { let message = "Invalid or malformed Alternative Payment Mehod URL response provided." - throw POFailure(message: message, code: .internal(.mobile), underlyingError: nil) + throw POFailure(message: message, code: .generic(.mobile), underlyingError: nil) } if let errorCode = queryItems.queryItemValue(name: "error_code") { throw POFailure(code: createFailureCode(rawValue: errorCode)) } guard let gatewayToken = queryItems.queryItemValue(name: "token") else { - let message = "Invalid or malformed Alternative Payment Mehod URL response provided." + let message = "Mandatory gateway 'token' query item is not set in URL." throw POFailure(message: message, code: .internal(.mobile), underlyingError: nil) } guard let customerId = queryItems.queryItemValue(name: "customer_id"), diff --git a/Sources/ProcessOut/Sources/Services/Logs/DefaultLogsService.swift b/Sources/ProcessOut/Sources/Services/Logs/DefaultLogsService.swift new file mode 100644 index 000000000..967428939 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/Logs/DefaultLogsService.swift @@ -0,0 +1,60 @@ +// +// DefaultLogsService.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 31.05.2023. +// + +import Foundation + +/// This service is thread safe. +final class DefaultLogsService: POService, LoggerDestination { + + init(repository: LogsRepository, minimumLevel: LogLevel) { + self.repository = repository + self.minimumLevel = minimumLevel + } + + // MARK: - LoggerDestination + + func log(event: LogEvent) { + guard event.level.rawValue >= minimumLevel.rawValue else { + return + } + var attributes = [ + Constants.attributeFile: event.file, Constants.attributeLine: event.line.description + ] + event.additionalAttributes.forEach { key, value in + attributes[key] = value + } + let request = LogRequest( + level: string(from: event.level), + date: event.timestamp, + message: event.message, + eventType: event.category, + attributes: attributes + ) + repository.send(request: request) + } + + // MARK: - Private Nested Types + + private enum Constants { + static let attributeFile = "File" + static let attributeLine = "Line" + } + + // MARK: - Private Properties + + private let repository: LogsRepository + private let minimumLevel: LogLevel + + // MARK: - Private Methods + + private func string(from level: LogLevel) -> String { + let strings: [LogLevel: String] = [ + .debug: "debug", .info: "info", .error: "error", .fault: "critical" + ] + return strings[level]! // swiftlint:disable:this force_unwrapping + } +} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift index 172a5f35c..e5dece0d3 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift @@ -73,6 +73,8 @@ public final class PONativeAlternativePaymentMethodViewControllerBuilder { // sw preconditionFailure("Gateway configuration id and invoice id must be set.") } let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation + var logger = api.logger + logger[attributeKey: "InvoiceId"] = invoiceId let interactor = DefaultNativeAlternativePaymentMethodInteractor( invoicesService: api.invoices, imagesRepository: api.images, @@ -82,13 +84,13 @@ public final class PONativeAlternativePaymentMethodViewControllerBuilder { // sw waitsPaymentConfirmation: configuration.waitsPaymentConfirmation, paymentConfirmationTimeout: configuration.paymentConfirmationTimeout ), - logger: api.logger, + logger: logger, delegate: delegate ) let viewModel = DefaultNativeAlternativePaymentMethodViewModel( interactor: interactor, configuration: configuration, completion: completion ) - return NativeAlternativePaymentMethodViewController(viewModel: viewModel, style: style, logger: api.logger) + return NativeAlternativePaymentMethodViewController(viewModel: viewModel, style: style, logger: logger) } // MARK: - Private Properties diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/DefaultNativeAlternativePaymentMethodInteractor.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/DefaultNativeAlternativePaymentMethodInteractor.swift index b1726c53f..8fd7496fb 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/DefaultNativeAlternativePaymentMethodInteractor.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/DefaultNativeAlternativePaymentMethodInteractor.swift @@ -38,7 +38,9 @@ final class DefaultNativeAlternativePaymentMethodInteractor: guard case .idle = state else { return } - logger.info("Starting payment using configuration: \(String(describing: configuration))") + logger.info( + "Starting native alternative payment", attributes: ["GatewayId": configuration.gatewayConfigurationId] + ) send(event: .willStart) state = .starting let request = PONativeAlternativePaymentMethodTransactionDetailsRequest( @@ -53,7 +55,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: } } case .failure(let failure): - self?.logger.error("Failed to start payment: \(failure)") + self?.logger.info("Failed to start payment: \(failure)") self?.setFailureStateUnchecked(failure: failure) } } @@ -94,7 +96,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: guard case let .started(startedState) = state, startedState.isSubmitAllowed else { return } - logger.info("Will submit '\(configuration.invoiceId)' payment parameters") + logger.info("Will submit payment parameters") send(event: .willSubmitParameters) do { let values = try validated(values: startedState.values, for: startedState.parameters) @@ -143,7 +145,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: } func cancel() { - logger.debug("Will attempt to cancel payment \(configuration.invoiceId)") + logger.debug("Will attempt to cancel payment.") switch state { case .started: setFailureStateUnchecked(failure: POFailure(code: .cancelled)) @@ -166,7 +168,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: private let invoicesService: POInvoicesService private let imagesRepository: ImagesRepository private let configuration: NativeAlternativePaymentMethodInteractorConfiguration - private let logger: POLogger + private var logger: POLogger private weak var delegate: PONativeAlternativePaymentMethodDelegate? private lazy var phoneNumberFormatter: PhoneNumberFormatter = { @@ -186,7 +188,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: case .customerInput, nil: break case .pendingCapture: - logger.debug("No more parameters to submit for '\(configuration.invoiceId), waiting for capture") + logger.debug("No more parameters to submit, waiting for capture") let actionMessage = details.parameterValues?.customerActionMessage ?? details.gateway.customerActionMessage trySetAwaitingCaptureStateUnchecked( gatewayLogo: gatewayLogo, expectedActionMessage: actionMessage, actionImage: nil @@ -210,7 +212,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: ) state = .started(startedState) send(event: .didStart) - logger.info("Did start \(configuration.invoiceId) payment, waiting for parameters") + logger.info("Did start payment, waiting for parameters") } private func trySetAwaitingCaptureStateUnchecked( @@ -232,7 +234,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: case .success: self?.setCapturedState() case .failure(let failure): - self?.logger.error("Did fail to capture invoice \(request.invoiceId): \(failure)") + self?.logger.info("Did fail to capture invoice: \(failure)") self?.setFailureStateUnchecked(failure: failure) } } @@ -242,7 +244,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: actionImage: actionImage ) state = .awaitingCapture(awaitingCaptureState) - logger.info("Waiting for invoice \(configuration.invoiceId) capture confirmation") + logger.info("Waiting for invoice capture confirmation") } private func setCapturedState() { @@ -259,7 +261,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: } private func setCapturedStateUnchecked(gatewayLogo: UIImage?) { - logger.info("Did receive invoice '\(configuration.invoiceId)' capture confirmation") + logger.info("Did receive invoice capture confirmation") if configuration.waitsPaymentConfirmation { state = .captured(.init(gatewayLogo: gatewayLogo)) send(event: .didCompletePayment) @@ -272,7 +274,7 @@ final class DefaultNativeAlternativePaymentMethodInteractor: private func restoreStartedStateAfterSubmissionFailureIfPossible( _ failure: POFailure, replaceErrorMessages: Bool = false ) { - logger.error("Did fail to submit parameters: \(failure)") + logger.info("Did fail to submit parameters: \(failure)") let startedState: State.Started switch state { case let .submitting(state), let .started(state): diff --git a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift index ed18e64fa..af6844aa0 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift @@ -10,6 +10,15 @@ struct StubDeviceMetadataProvider: DeviceMetadataProvider { var deviceMetadata: DeviceMetadata { - DeviceMetadata(appLanguage: "en", appScreenWidth: 1, appScreenHeight: 2, appTimeZoneOffset: 3, channel: "test") + DeviceMetadata( + id: .init(value: ""), + installationId: .init(value: nil), + systemVersion: .init(value: ""), + appLanguage: "en", + appScreenWidth: 1, + appScreenHeight: 2, + appTimeZoneOffset: 3, + channel: "test" + ) } } diff --git a/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift new file mode 100644 index 000000000..c318c2477 --- /dev/null +++ b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift @@ -0,0 +1,14 @@ +// +// POLogger+Extensions.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 26.06.2023. +// + +@testable @_spi(PO) import ProcessOut + +extension POLogger { + + /// Stub logger. + static var stub = POLogger(category: "") +} diff --git a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift index ee59fa6b7..9cc826c22 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift @@ -16,7 +16,11 @@ final class DefaultHttpConnectorRequestMapperTests: XCTestCase { func test_urlRequest_whenBaseUrlIsMalformed_fails() throws { // Given let configuration = HttpConnectorRequestMapperConfiguration( - baseUrl: URL(string: "http://example.com:-80")!, projectId: "", privateKey: nil, version: "" + baseUrl: URL(string: "http://example.com:-80")!, + projectId: "", + privateKey: nil, + version: "", + appVersion: nil ) let sut = createMapper(configuration: configuration) let request = HttpConnectorRequest.get(path: "") @@ -139,7 +143,7 @@ final class DefaultHttpConnectorRequestMapperTests: XCTestCase { // Then let userAgent = urlRequest.value(forHTTPHeaderField: "user-agent") - let userAgentRegex = /^iOS\/Version\/.*\/ProcessOut iOS-Bindings\/1\.2\.3$/ + let userAgentRegex = /^test\/Version\/.*\/ProcessOut iOS-Bindings\/1\.2\.3$/ XCTAssertNotNil(userAgent?.firstMatch(of: userAgentRegex)) } @@ -169,18 +173,6 @@ final class DefaultHttpConnectorRequestMapperTests: XCTestCase { XCTAssertEqual(authorization, "Basic PElEPjo8S0VZPg==") } - func test_urlRequest_whenPrivateKeyIsRequiredButNotSet_fails() throws { - // Given - let configuration = HttpConnectorRequestMapperConfiguration( - baseUrl: Constants.baseUrl, projectId: "", privateKey: nil, version: "" - ) - let sut = createMapper(configuration: configuration) - let request = HttpConnectorRequest.get(path: "", requiresPrivateKey: true) - - // Then - XCTAssertThrowsError(try sut.urlRequest(from: request)) - } - func test_urlRequest_addsDefaultHeaders() throws { // Given let sut = createMapper(configuration: defaultConfiguration) @@ -251,12 +243,12 @@ final class DefaultHttpConnectorRequestMapperTests: XCTestCase { configuration: configuration, encoder: encoder, deviceMetadataProvider: StubDeviceMetadataProvider(), - logger: POLogger() + logger: .stub ) return mapper } private var defaultConfiguration: HttpConnectorRequestMapperConfiguration { - .init(baseUrl: Constants.baseUrl, projectId: "", privateKey: "", version: "1.2.3") + .init(baseUrl: Constants.baseUrl, projectId: "", privateKey: "", version: "1.2.3", appVersion: "4.5.6") } } diff --git a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift index f58029bfd..079af9246 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift @@ -20,7 +20,7 @@ final class UrlSessionHttpConnectorTests: XCTestCase { sessionConfiguration: sessionConfiguration, requestMapper: requestMapper, decoder: JSONDecoder(), - logger: POLogger() + logger: .stub ) } diff --git a/Tests/ProcessOutTests/Sources/Unit/Repositories/Cards/HttpCardsRepositoryTests.swift b/Tests/ProcessOutTests/Sources/Unit/Repositories/Cards/HttpCardsRepositoryTests.swift index e4415424a..05c3853ac 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Repositories/Cards/HttpCardsRepositoryTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Repositories/Cards/HttpCardsRepositoryTests.swift @@ -13,18 +13,21 @@ final class HttpCardsRepositoryTests: XCTestCase { override func setUp() { super.setUp() - // todo(andrii-vysotskyi): use mocks or stubs for failure mapper and device metadata provider + // todo(andrii-vysotskyi): use mocks or stubs for failure mapper let sessionConfiguration = URLSessionConfiguration.ephemeral sessionConfiguration.protocolClasses = [MockUrlProtocol.self] - let logger = POLogger() + let connectorConfiguration = HttpConnectorRequestMapperConfiguration( + baseUrl: Constants.baseUrl, projectId: "", privateKey: nil, version: "", appVersion: "" + ) let connector = ProcessOutHttpConnectorBuilder() - .with(configuration: .init(baseUrl: Constants.baseUrl, projectId: "", privateKey: nil, version: "")) + .with(configuration: connectorConfiguration) .with(retryStrategy: nil) .with(sessionConfiguration: sessionConfiguration) - .with(logger: logger) + .with(logger: .stub) + .with(deviceMetadataProvider: StubDeviceMetadataProvider()) .build() sut = HttpCardsRepository( - connector: connector, failureMapper: DefaultHttpConnectorFailureMapper(logger: logger) + connector: connector, failureMapper: DefaultHttpConnectorFailureMapper(logger: .stub) ) } diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift index fab237845..54dcb79e1 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift @@ -16,7 +16,7 @@ final class DefaultThreeDSServiceTests: XCTestCase { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys sut = DefaultThreeDSService( - decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys], logger: POLogger() + decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys], logger: POLogger.stub ) delegate = Mock3DSService() } diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift index c894088b5..683ae4b39 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift @@ -14,8 +14,7 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { override func setUp() { super.setUp() let baseUrl = URL(string: "https://example.com")! - let logger = POLogger(destinations: []) - sut = DefaultAlternativePaymentMethodsService(projectId: "proj_test", baseUrl: baseUrl, logger: logger) + sut = DefaultAlternativePaymentMethodsService(projectId: "proj_test", baseUrl: baseUrl, logger: .stub) } func test_alternativePaymentMethodUrl_withAdditionalData_succeeds() throws {