3 Commits

Author SHA1 Message Date
Juraldinio c564f69b82 WiP 2022-05-30 12:55:13 +03:00
Juraldinio 52919cde7f Remove project files from git 2022-05-30 11:54:41 +03:00
Juraldinio c07fb77ce5 Refactoring huge ApplicationDelegate 2022-05-30 10:19:50 +03:00
2334 changed files with 58353 additions and 406124 deletions
-5
View File
@@ -93,9 +93,4 @@ iOSInjectionProject/
Info.plist
/PayCash.entitlements
/PayCash.xcodeproj
/ResultIPA/
*.pbxproj
*.xcscheme
*.xcodeproj
*.entitlements
/iOS/Assets/Settings.bundle
+18 -34
View File
@@ -1,6 +1,5 @@
# Execute always
before_script:
- echo $CI_PIPELINE_IID
- echo $GITLAB_USER_ID
- echo ${CI_COMMIT_REF_SLUG}
- id
@@ -10,12 +9,13 @@ variables:
stages:
- lint
- test
# - test
- build
- deploy
- notification
# Linting stage
.linting: &linting
tags:
- malinka
@@ -34,25 +34,6 @@ Lint:
- /^feature/
- merge_requests
# Testing stage
.testing: &testing
tags:
- malinka
stage: test
when: always
allow_failure: false
script:
- chmod +x ./toolchain/testing.sh
- ./toolchain/testing.sh
Test:
<<: *testing
only:
- develop
- /^bugfix/
- /^feature/
- merge_requests
# Build IPA
.BuildIPA: &BuildIPA
tags:
@@ -63,20 +44,23 @@ Test:
script:
- rm -rf ./ResultIPA
- chmod +x ./toolchain/build_iOS.sh
- ./toolchain/build_iOS.sh ${APPLICATION_DEPLOY_TYPE} ${APPLICATION_NAME} ${APPLICATION_SCHEME_NAME}
# - ./toolchain/build_iOS.sh PRODUCTION
- ./toolchain/build_iOS.sh BETA
# Build stage
build:
stage: build
<<: *BuildIPA
only:
- tags
only:
- master
- develop
- tags
- merge_requests
artifacts:
paths:
- ./ResultIPA
expire_in: 2 days
expire_in: 3 days
buildLeaf:
stage: build
@@ -101,7 +85,7 @@ buildLeaf:
IPA_PATH: ./ResultIPA
script:
- chmod +x ./toolchain/deploy_iOS.sh
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${APPLICATION_SCHEME_NAME}
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH}
needs:
- job: build
artifacts: true
@@ -111,8 +95,9 @@ deploy:
<<: *DeployIPA
when: on_success
only:
- tags
- master
- develop
- tags
needs:
- job: build
artifacts: true
@@ -137,29 +122,28 @@ deployLeaf:
script:
- chmod +x ./toolchain/slack_notification.sh
- ./toolchain/slack_notification.sh ${BUILD_RESULT}
needs:
- job: deploy
# Develop
FailureNotification:
variables:
BUILD_RESULT: "FAILURE"
when: on_failure
only:
- tags
- master
- develop
- tags
<<: *slacknotification
needs:
- job: deploy
SuccessNotification:
variables:
BUILD_RESULT: "SUCCESS"
when: on_success
only:
- tags
- master
- develop
- tags
<<: *slacknotification
needs:
- job: deploy
# Merge checks
-32
View File
@@ -1,32 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletFoundation",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletFoundation",
targets: ["WalletFoundation"]),
],
dependencies: [
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletFoundation",
dependencies: ["KeyChainAccess"],
path: "./Sources"),
.testTarget(
name: "WalletFoundationTests",
dependencies: ["WalletFoundation"],
path: "Tests"//, // Test files
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
)
]
)
@@ -1,85 +0,0 @@
//
// ApplicationSettings.swift
//
//
// Created by Juraldinio on 8/28/22.
//
import Foundation
fileprivate enum ApplicationSettingsKeys {
// TODO: - Need remove after 1.4.0 version
static let ignoreDeviceSatusKeyOld = "Settings.device_ignore"
static let ignoreCreateAccountSatusKeyOld = "Status.createAccount_ignore"
static let isNetworkLogEnabledKeyOld = "Settings.network_copy"
static let isDeviceTokenCopyEnabledKeyOld = "Settings.device_token_copy_enable"
static let apiEnvironmentKeyOld = "Settings.api_environment"
static let apiPaycashEnvironmentKeyOld = "Settings.api_paycash_environment"
static let deviceDescriptionKeyOld = "Settings.device_id"
static let deviceTokenKeyOld = "Settings.device_token"
/// Ignore device status on register flow
static let ignoreDeviceSatusKey = "Settings.application.device_ignore"
/// Ignore create account status on register flow
static let ignoreCreateAccountSatusKey = "Status.application.createAccount_ignore"
/// log all network activity
static let isNetworkLogEnabledKey = "Settings.application.network_copy"
/// Copy device token to Settings fields
static let isDeviceTokenCopyEnabledKey = "Settings.application.device_token_copy_enable"
/// Field in Settings for device description
static let deviceDescriptionKey = "Settings.application.device_id"
/// Field in Settings for device token
static let deviceTokenKey = "Settings.application.device_token"
static let apiUsernameEnvironmentKey = "Settings.application.environment.usernames"
static let apiBackendEnvironmentKey = "Settings.application.environment.backend"
static let otherEnvironmentKey = "Settings.application.environment.other"
static let smartEnvironmentKey = "Settings.application.environment.smart"
}
public enum ApplicationSettings {
public static var ignoreDeviceSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey) }
public static var ignoreCreateAccountSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey) }
public static var isNetworkLogEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey) }
public static var isDeviceTokenCopyEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey) }
public static func device(description: String) { UserDefaults.standard.set(description, forKey: ApplicationSettingsKeys.deviceDescriptionKey) }
public static func device(token: String) { UserDefaults.standard.set(token, forKey: ApplicationSettingsKeys.deviceTokenKey) }
public static var apiUsernameEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey) }
public static var apiBackendEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey) }
public static var otherEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.otherEnvironmentKey) }
public static var smartsEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.smartEnvironmentKey) }
public static func clearApiEmvironment() {
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.otherEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.smartEnvironmentKey)
// TODO: - Need remove after 1.4.0 version
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiEnvironmentKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiPaycashEnvironmentKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKeyOld)
}
}
@@ -1,26 +0,0 @@
//
// CommonKey.swift
//
//
// Created by Juraldinio on 11/25/22.
//
import Foundation
public struct CommonKey: RawRepresentable, Hashable {
public var rawValue: String
public init?(rawValue: String) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.rawValue = rawValue
}
public func with(_ suffix: String) -> Self { .init(rawValue + "." + suffix) }
public static func key(_ key: String) -> Self { .init(key) }
public static func key(_ key: String, suffix: String) -> Self { Self.key(key).with(suffix) }
}
@@ -1,191 +0,0 @@
//
// WalletKeychain.swift
//
//
// Created by Juraldinio on 11/27/22.
//
import Foundation
import LocalAuthentication
extension String {
fileprivate static let password = "PWD"
fileprivate static let biometric = "BIO"
}
final public class WalletKeychain {
public typealias Key = CommonKey
public static let instance = WalletKeychain()
// MARK: - Init
private init() { }
// MARK: - Interface
public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.password)) }
public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.biometric) ) }
public subscript(biometric key: Key) -> String? {
self.loadBiometricProtected(key: key.with(.biometric))
.map({ String(data: $0, encoding: .utf8) }) ?? nil
}
public subscript(_ key: Key, password password: String) -> String? {
get { loadPassProtected(key: key.with(.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil }
set { update(key, password: password, newValue: newValue) }
}
// MARK: - Private
private func getPwdSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .applicationPassword, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setPassProtected(key: Key, data: String, password: String) -> Bool {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
kSecUseAuthenticationContext: context
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setPassProtected(key: key, data: data, password: password)
} else {
return false
}
} else {
return false
}
}
private func loadPassProtected(key: Key, password: String) -> Data? {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue!,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
var dataTypeRef: AnyObject?
let result = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if result == noErr,
let value = dataTypeRef as? Data {
return value
}
return nil
}
// MARK: - Biometric entries
private func getBiometricSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setBiometricEntry(key: Key, data: String) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getBiometricSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setBiometricEntry(key: key, data: data)
} else {
return false
}
} else {
return false
}
}
@discardableResult
private func loadBiometricProtected(key: Key) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue as Any,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseOperationPrompt as String: "Access your data"
]
var dataTypeRef: AnyObject?
return SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == noErr ? dataTypeRef as? Data : nil
}
// MARK: -
private func update(_ key: Key, password: String, newValue: String?) {
if let value = newValue {
setPassProtected(key: key.with(.password), data: value, password: password)
setBiometricEntry(key: key.with(.biometric), data: value)
} else {
removeProtected(key: key.with(.password))
removeProtected(key: key.with(.biometric))
}
}
@discardableResult
private func removeProtected(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue
]
return SecItemDelete(query as CFDictionary) == noErr
}
private func checkProtectedExist(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
let status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
case errSecSuccess, errSecInteractionNotAllowed: return true
case errSecItemNotFound: return false
default: return false
}
}
}
@@ -1,54 +0,0 @@
//
// Settings.swift
//
//
// Created by Juraldinio on 11/29/22.
//
import Foundation
final public class Settings {
public static let shared = Settings()
private init() {}
public func contains(_ key: CommonKey) -> Bool { defaults.value(forKey: key.rawValue) != nil }
public subscript<T>(_ key: CommonKey) -> T? {
get { defaults.value(forKey: key.rawValue) as? T }
set { set(value: newValue, for: key) }
}
public subscript<T: Codable>(_ key: CommonKey) -> [T]? {
get {
let decoder = JSONDecoder()
guard let data = defaults.value(forKey: key.rawValue) as? Data,
let value = try? decoder.decode([T].self, from: data) else { return nil }
return value
}
set {
let encoder = JSONEncoder()
guard let value = newValue,
let data = try? encoder.encode(value) else { return }
set(value: data, for: key)
}
}
public subscript(_ key: CommonKey) -> Bool {
get { defaults.bool(forKey: key.rawValue) }
set { set(value: newValue, for: key) }
}
public subscript(_ key: CommonKey) -> Int {
get { (defaults.value(forKey: key.rawValue) as? Int) ?? 0 }
set { set(value: newValue, for: key) }
}
private func set(value: Any?, for key: CommonKey) {
defaults.setValue(value, forKey: key.rawValue)
defaults.synchronize()
}
private var defaults: UserDefaults { UserDefaults.standard }
}
@@ -1,55 +0,0 @@
//
// DeviceToken.swift
//
//
// Created by NUT.Tech on 02.08.2022.
//
import Foundation
import DeviceCheck
import UIKit
public enum DeviceCheckTokenError: Error {
case notSupported
case generation
}
public struct DeviceCheckToken {
public let token: String
public let isSimulator: Bool
public static func generate() async -> Result<DeviceCheckToken, DeviceCheckTokenError> {
let token: String
let isSimulator: Bool
#if targetEnvironment(simulator)
isSimulator = true
token = tokenConstant
#else
let device = DCDevice.current
guard device.isSupported else { return .failure(.notSupported) }
guard let data = try? await device.generateToken() else { return .failure(.generation) }
isSimulator = false
token = data.base64EncodedString()
#endif
if ApplicationSettings.isDeviceTokenCopyEnabled {
ApplicationSettings.device(token: token)
}
return .success(DeviceCheckToken(token: token, isSimulator: isSimulator))
}
}
#if targetEnvironment(simulator)
fileprivate let tokenConstant = """
AgAAAIArgRs5gVfPAEHSra0ejd0EUNk0+me89vLfv5ZingpyOOkgXXXyjPzYTzWmWSu+BYqcD47byirLZ++3dJccpF99hWppT7G5xAuU+y56WpSYsATWySfuxbSSMT9JSoOWz4QtiDmmVmUHbzCfHbTP3Tr3hsG+86KBBaoSqHtDRy+dtKlV32kDRbuuniBy3nseZsZoCggAAKTXDFKWHDZ55Ya1Cp+8s+dGOyi4C08v0P/8rbQHcjjkOphpLUqKaBZCykAaf5Ue1c1ul57OKeoyaDy9ShXGvwIKIcZrvZBBds3wwEFQuZBNPTG1ZvpIZ3npXscHWaKRd228V/bboEKarukYi3+lsxafZj+R8laD0Ex3nP3WZaIU4990oerdiu6wbdELlaNyrVF6SRJcZQhhflfnnyXHMqw6c41Hk3toMHsUd9wzdzcYmZB1PI5rsgSfnWrxAbBy5rYpH+ZkGkhcHFJqQIO4TB8ZKU0KSjmS8mBnVDT/1VPgiNDz/qI+KWiZ1xcuEwwvaEmD+Fk6Pt9GMRsfxI+SvldTgk8REqr9dBXt69xwM2FHBHP9k4okkaMbsU1qpZPKCwsnTBrkbvLn0zVn2+tfTukLx0O+uCPRMluXn3EXDrQ0aRHFbUtgoMqOx0JP+7tj/BaOAKkc3C2GaCRfEes2YmqZkM4pMve4Qh1jAsIwYtGcDz7AeGQ61kVrc4CFj3xj2OgC+IbBi7naOZqNvr1Be1YPKt8Vpig9YF5GueY9p4DRGlOG6UX2RY4wsZTt38Lxw5uBBkWuINjRyqsiN5obPZ3xLagfzfNsFDBYgBsGrib8nURVfSgAqIgrozOl+cppRB7xoN9QRti+HAJqKNd6sqpsPkXzqnDWPBD2Jdg2WCJE2bjJhTqyJ6L3lHFguOdIPc2P6F1CGx4bn1GtegOLlFxexOjMzfU86gJOhYjkVGHt8GD96ohRl75/fsv1reCI16pWt2x8p+Bbh4kko8wP3FtXiun+i6gPDMBhE30Ye5ATsIUIHFZjHOA8UfntaEyCSAngQebQ0X4UERcue+GKY4RqVfPhuVqJa3RHt35Ci/2g7VWllNs1NKYsPofAgTNO2n/kwGrnnIL+gQPNsO6XjnqDOjjT/eDfZX88eK4k6+8+MnW3+l8IAZOIh5JT++JZvBadrNZPV7G/2ME6G1FCIAZ4icCidbMzpj4sGc8dlJOg/B3457/Vt8CHLulhajQIsXQuqGDotgzirELTVQ+Z2eh3a8W/Pu+g8iNIUL1MCEzHg7RefM6tUescNDH6uPEuEeXKmrsoVbHdUvhuLIVbQGHMCjuGbmxmczcfIcRAPWkjwuVQOKBpoaD/Zep2gM71inpz6056bmvUuTMM9MV6sM87Fqrv6TvIRT/ch/i7Flmv56ERn/NGryafdcvDIu+JMs5U3yvb3STTSZmbh8RXVGIJjOZ1FJYLREUxxK7eEGM2JLCD+CxR7LuLRuN4AtpI4GIhFrbdVdDkywqNpvEY6aGEOFnD9NP6neBuHRhK/AzqpDE83uFf+1JiPPY7aHYVoQhCxkPs8ex0qJnjHaveKiWfSmaZ6JfY/vVByzJNr5XD6ZSQlJQJ3+xjRb+blTR0XcZ5BHI9ovQQAmGQljWpGPnD5CZQ7ah5kVoK1SbPqtxY6J5zQUTjtTpSe3l6By/nopXH6HSQXJGotOzb+eMOOHFhDC9ypq3urHY+Q1jXB18eR/xkXEIlZQsPBmwCLhoNltD57faLzlqgiwinHjqslntnvfsMkoIpQnWFwLYKh0biW9KM5ZWv7CMIxcc+Wjl1FVnyUrMzhz/IIW6WshcNmZWFSMpaKzozxIyFQZ/IjZHPrE1SI33PwGn/Tro8ZRSf9mpKJbA3uidrygVz6WQlJJwR3ujdgZ9aJ9WZQWVehVYj5mMg5P5MB/BG1G/TQNTLn72Root26hSB+8WMCAo/EEY3L5Qox+JabfDV+kBsHjFtDfyo7ghp/AOvkVzudzzk+F3ruc1bIDJlSfOQ5WRTkjmQGhKDIfDvLIi4Mt8bQTzZ3KPkLaU+hwuFE7m9c/MgzmLWK24oDsZ5nZ4oBeUNc+lThUuz7qnWENE1sIXij3zFljsoH+HOPb+zSt8m7iwszxMZ6T84LvqxqWVGEkI4Il2s41ti5QH74nOzTaci+5dYRVWOnY/hAwI51HE1sb3Pe+NsSHcfgtDW0E4Xx+Wx0uLdqbTlriXRnUIOoi9PVNR6XOdJ4nDOxyiOMMhnUooQ7lRqm0hCxw9nAEw5+PvYWWFXxPaupUfeVsjH+9dXW6bzosCGzTbVpHcDjPWie70r+Nma+oOwA4ARKHmGsbKcoO43xos6sqfbZTCCP9BIPQnZ8XbUen9G7eMs9ESigKoynKKVGNmsBXK4lU8xM/qLXXudViMdSPOZ6mghjJCNK0yA1v9l/ipZRHiTPFOttELH6Ip+fKDtfqdeEqCiPrSnVtWzehUKOUhlNJtexkOZcb2Dtq4L7JlJ0GkJg80vCvEYvArM2JpPqKDVr8hCBNC87u6zk9T3E+L2dfL30aiNVAGTl44Qw0pPerIr1a6m9Jkj690Pi3OI7UAgWaoQjxYm2my7DZMqtkL6CrT0NW9KnihXw701ngJysdKcZ0JkMDT2LzP+2Nj10WIOwkLxASexSQgSoyGk7yTYLUAyvwN1rhRtspXaiyOcfyzDwgTIU9Sn/jMbC6fv7GPReCsiFR8Xa6VCj37eFPXgBiOpAYtj/zMz/3S/io3LTqs7QG1M14CX31xSxu21tASOzaRhbd2RB2QCHXgpqv4593psE5EPjbRZt5DN2toQ4XJJ1A1/EcyDkEJ8+1gu34aVrqC6ejm/07/MQ7ISmUuPrJyCaPIW+PbkxF0VpYU5lJ9HP+LD7WggPwi8NVz8zFWNtyTM6aUuTNL69sBHpWlYeqCwpkJ+EcJFuaTnT27N4pFvwA==
"""
#endif
@@ -1,346 +0,0 @@
//
// DeviceUUID.swift
//
//
// Created Nut.Tech on 02.08.2022.
//
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
import KeyChainAccess
/// Class allow retrive UUID with different life cycle.
final public class DeviceUUID {
private enum Constants {
enum Keys {
static let installationUUIDKey = "installationUUIDKey"
static let deviceUUIDKey = "deviceUUIDKey"
static let devicesUUIDsKey = "devicesUUIDsKey"
static let devicesUUIDsToggleKey = "devicesUUIDsToggleKey"
static let devicesUUIDs = "devicesUUIDs"
}
enum Locals {
static let DevicesUUIDsDidChangeNotification = NSNotification.Name(rawValue: "DevicesUUIDsDidChangeNotification")
}
}
/// Shared instance for class
static public var shared = DeviceUUID()
/// Changes each time the app gets launched (persistent to session).
lazy private(set) public var session: String = {
self.generateUuid()
}()
/// Changes each time the app gets installed (persistent to installation).
lazy private(set) public var installation: String = {
self.getValue(forKey: Constants.Keys.installationUUIDKey,
defaultValue: nil,
keychain: false,
synchronizable: false)
}()
/// Changes each time all the apps of the same vendor are uninstalled (this works exactly as identifierForVendor).
lazy private(set) public var vendor: String? = {
#if os(iOS)
UIDevice.current
.identifierForVendor?
.uuidString
.lowercased()
.replacingOccurrences(of: "-", with: "")
#elseif os(macOS)
return nil
#endif
}()
/// Changes only on system reset, this is the best replacement to the good old udid (persistent to device)
lazy private(set) public var device: String = {
self.getValue(forKey: Constants.Keys.deviceUUIDKey,
defaultValue: nil,
synchronizable: false)
}()
/// List of all uuidForDevice of the same user.
/// In this way it's possible manage guest accounts across multiple devices easily
lazy private(set) public var devices: [String] = {
let devicesString = self.getValue(forKey: Constants.Keys.devicesUUIDsKey,
defaultValue: self.device)
return devicesString.components(separatedBy: "|")
}()
/// Changes each time (no persistent), but allows to keep in memory more temporary uuids.
public func uuid(forKey key: String) -> String {
guard let uuid = self.uuids[key] else {
let value = self.generateUuid()
self.uuids[key] = value
return value
}
return uuid
}
/// Changes each time (no persistent)
public func generateUuid() -> String {
let uuidRef = CFUUIDCreate(nil)
let uuidStringRef = CFUUIDCreateString(nil, uuidRef)
return ((uuidStringRef as? String) ?? "")
.lowercased()
.replacingOccurrences(of: "-", with: "")
}
// MARK: - Private
private var uuids = [String: String]()
private var isCloudAvailable = false
// MARK: - Init
init() {
self.initCloudUUIDsDevices()
}
// MARK: - Internal
func getValue(forKey key: String,
defaultValue: String? = nil,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil,
synchronizable: Bool = true) -> String {
if let newValue = Self.getValue(forKey: key,
userDefaults:
userDefaults,
keychain: keychain,
service: service,
accessGroup: accessGroup) {
return newValue
} else {
let value = defaultValue ?? self.generateUuid()
Self.setValue(value,
forKey: key,
userDefaults: userDefaults,
keychain: keychain,
service: service,
accessGroup: accessGroup,
synchronizable: synchronizable)
return value
}
}
public func uuidForDeviceMigratingValue(forKey key: String,
service: String? = nil,
accessGroup: String? = nil,
commitMigration: Bool) -> String? {
if let uuidToMigrate = Self.getValue(forKey: key,
service: service,
accessGroup: accessGroup) {
return self.uuid(forDeviceMigratingValue: uuidToMigrate, commitMigration: commitMigration)
}
return nil
}
func updateUUIDDevice(value: String) {
self.device = value
Self.setValue(value,
forKey: Constants.Keys.deviceUUIDKey,
synchronizable: false)
}
func uuid(forDeviceMigratingValue value: String, commitMigration: Bool) -> String? {
if self.isValidUUID(value) {
let oldValue = self.device
let newValue = value
guard oldValue != newValue else { return oldValue }
if commitMigration {
self.updateUUIDDevice(value: newValue)
let deviceSet = NSMutableOrderedSet(array: self.devices)
deviceSet.add(newValue)
deviceSet.remove(oldValue)
if let uuidsArray = deviceSet.array as? [String] {
self.updateUUIDsDevices(with: uuidsArray)
}
self.syncCloudUUIDsDevices()
return self.device
} else {
return oldValue
}
} else {
let exception = NSException(name: NSExceptionName(rawValue: "Invalid uuid to migrate"),
reason: "uuid value should be a string of 32 or 36 characters.")
exception.raise()
return nil
}
}
private func initCloudUUIDsDevices() {
self.isCloudAvailable = false
guard FileManager.default.ubiquityIdentityToken != nil else { return }
self.isCloudAvailable = true
NotificationCenter.default.addObserver(self,
selector: #selector(self.changesCloudUUIDsDevicesNotification),
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: nil)
self.syncCloudUUIDsDevices()
}
private func syncCloudUUIDsDevices() {
if self.isCloudAvailable {
let iCloud = NSUbiquitousKeyValueStore.default
//if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud
for uuidOfUserDevice in self.devices {
let uuidOfUserDeviceAsKey = "\(Constants.Keys.deviceUUIDKey)_\(uuidOfUserDevice)"
if iCloud.string(forKey: uuidOfUserDeviceAsKey) != uuidOfUserDevice {
iCloud.set(uuidOfUserDevice, forKey: uuidOfUserDeviceAsKey)
}
}
//toggle a boolean value to force notification on other devices, useful for debug
let uuidsOfUserDevicesToggler = !iCloud.bool(forKey: Constants.Keys.devicesUUIDsToggleKey)
iCloud.set(uuidsOfUserDevicesToggler, forKey: Constants.Keys.devicesUUIDsToggleKey)
iCloud.synchronize()
}
}
private func updateUUIDsDevices(with value: [String]) {
self.devices = value
Self.setValue(value.joined(separator: "|"),
forKey: Constants.Keys.devicesUUIDsKey)
}
private func isValidUUID(_ value: String) -> Bool {
let pattern = "^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"
guard let regExp = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
return false
}
let uuidValueRange = NSRange(location: 0, length: value.count)
let matchRange = regExp.rangeOfFirstMatch(in: value, options: [], range: uuidValueRange)
var matchValue: String?
if !NSEqualRanges(matchRange, NSRange(location: NSNotFound, length: 0)) {
matchValue = (value as NSString).substring(with: matchRange)
return matchValue == value ? true : false
} else {
return false
}
}
@objc
private func changesCloudUUIDsDevicesNotification(_ notification: Notification?) {
if self.isCloudAvailable {
let uuidsSet = NSMutableOrderedSet(array: self.devices)
let uuidsCount = uuidsSet.count
let iCloud = NSUbiquitousKeyValueStore.default
let iCloudDict = iCloud.dictionaryRepresentation as NSDictionary
iCloudDict.enumerateKeysAndObjects { key, obj, stop in
let uuidKey = key as? NSString
if uuidKey?.range(of: Constants.Keys.deviceUUIDKey).location == 0 {
if let uuidValue = obj as? String {
if uuidKey?.range(of: uuidValue).location != NSNotFound,
self.isValidUUID(uuidValue) {
uuidsSet.add(uuidValue)
} else {
print("invalid uuid")
}
}
}
}
if uuidsSet.count > uuidsCount,
let uuidsArray = uuidsSet.array as? [String] {
self.updateUUIDsDevices(with: uuidsArray)
let userInfo = [Constants.Keys.devicesUUIDs: self.devices]
NotificationCenter.default.post(name: Constants.Locals.DevicesUUIDsDidChangeNotification,
object: self,
userInfo: userInfo)
}
}
}
// MARK: - Static
static private func getValue(forKey key: String,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil) -> String? {
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup)
var value = try? keychainStore.getString(key)
if userDefaults,
!value.isExist {
value = UserDefaults.standard.string(forKey: key)
}
return value
}
@discardableResult
static private func setValue(_ value: String?,
forKey key: String,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil,
synchronizable: Bool = true) -> Error? {
if let value = value,
userDefaults {
UserDefaults.standard.set(value, forKey: key)
UserDefaults.standard.synchronize()
}
if let value = value, keychain {
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup).synchronizable(synchronizable)
do {
try keychainStore.set(value, key: key)
} catch {
return error
}
}
return nil
}
static private func makeKeychain(service: String? = nil, accessGroup: String? = nil) -> Keychain {
if let service = service {
if let accessGroup = accessGroup {
return Keychain(service: service, accessGroup: accessGroup)
} else {
return Keychain(service: service)
}
} else if let accessGroup = accessGroup {
return Keychain(accessGroup: accessGroup)
}
return Keychain()
}
}
@@ -1,16 +0,0 @@
//
// Array+Extension.swift
//
//
// Created by Juraldinio on 9/15/22.
//
import Foundation
public extension Array {
subscript(safe index: Index) -> Element? {
return (self.startIndex..<self.endIndex) ~= index ? self[index] : nil
}
}
@@ -1,15 +0,0 @@
//
// Data+Extension.swift
//
//
// Created by Juraldinio on 12/6/22.
//
import Foundation
public extension Data {
func jsonDecoded<T: Decodable>(type: T.Type) -> T? { try? JSONDecoder().decode(type, from: self) }
func jsonDecoded<T: Decodable>(type: T.Type) -> [T]? { try? JSONDecoder().decode([T].self, from: self) }
}
@@ -1,15 +0,0 @@
//
// Dictionary+Extension.swift
//
//
// Created by NUT.Tech on 27.01.2023.
//
import Foundation
public extension Dictionary where Key == String, Value == Any {
func jsonSerialized(options: JSONSerialization.WritingOptions = []) -> Data? {
try? JSONSerialization.data(withJSONObject: self, options: options)
}
}
@@ -1,16 +0,0 @@
//
// Encodable+Extension.swift
//
//
// Created by Juraldinio on 12/6/22.
//
import Foundation
public extension Encodable {
func jsonData() -> Data? {
let encoder = JSONEncoder()
return try? encoder.encode(self)
}
}
@@ -1,32 +0,0 @@
//
// Optional+Extension.swift
//
//
// Created by Juraldinio on 08.08.2022.
//
import Foundation
public extension Optional {
var isExist: Bool {
if case .some = self {
return true
}
return false
}
func orCreate(_ creation: @autoclosure () -> Wrapped) -> Wrapped {
switch self {
case let .some(value): return value
case .none: return creation()
}
}
func orTypedCreate<Element: RawRepresentable>(_ creation: @autoclosure () -> Element) -> Element where Element.RawValue == Wrapped {
switch self {
case let .some(value): return Element(rawValue: value) ?? creation()
case .none: return creation()
}
}
}
@@ -1,43 +0,0 @@
//
// Publisher+Extension.swift
//
//
// Created by Juraldinio on 12/20/22.
//
import Foundation
import Combine
public extension Publisher {
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
/// The first time the upstream publisher emits an element, the previous element will be `nil`.
///
/// let range = (1...5)
/// cancellable = range.publisher
/// .withPrevious()
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
/// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
///
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
.compactMap { $0 }
.eraseToAnyPublisher()
}
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional.
/// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`.
///
/// let range = (1...5)
/// cancellable = range.publisher
/// .withPrevious(0)
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
/// // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ".
///
/// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time.
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
}
}
@@ -1,24 +0,0 @@
//
// File.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
public extension String {
func trimCompact() -> String {
let value = self
.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
let regex = try! NSRegularExpression(pattern: "[ ]{2,}", options: .caseInsensitive)
return regex.stringByReplacingMatches(in: value,
options: [],
range: NSRange(0..<value.utf16.count),
withTemplate: " ")
}
}
@@ -1,43 +0,0 @@
//
// Either.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
public enum Either<T: Decodable, U: Decodable>: Decodable {
case firstType(T)
case secondType(U)
public func unwrap() -> Any {
switch self {
case .firstType(let objectOfTypeT): return objectOfTypeT
case .secondType(let objectOfTypeU): return objectOfTypeU
}
}
public func map<V>(firstTypeTransform: (T) -> V, secondTypeTransform: (U) -> V) -> V {
switch self {
case .firstType(let value):
return firstTypeTransform(value)
case .secondType(let value):
return secondTypeTransform(value)
}
}
public init(from decoder: Decoder) throws {
if let value = try? T(from: decoder) {
self = .firstType(value)
} else if let value = try? U(from: decoder) {
self = .secondType(value)
} else {
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"Cannot decode \(T.self) or \(U.self)")
throw DecodingError.dataCorrupted(context)
}
}
}
@@ -1,53 +0,0 @@
//
// Operations.swift
// Jura
//
// Created by Jura on 8/14/19.
// Copyright © 2019 Jura. All rights reserved.
//
import Foundation
precedencegroup MonadicPrecedence {
associativity: left
higherThan: BitwiseShiftPrecedence
}
infix operator >>- : MonadicPrecedence
@inline(__always)
@discardableResult
public func >>-<T, U>(a: T?, f: (T) throws -> U?) rethrows -> U? {
switch a {
case .some(let x):
return try f(x)
case .none:
return nil
}
}
// MARK: <<< / >>>
precedencegroup FunctionApplicationPrecedenceLeft {
lowerThan: AssignmentPrecedence
associativity: left
}
infix operator >>> : FunctionApplicationPrecedenceLeft
@inline(__always)
public func >>><T, U>(x: T, f: (T) throws -> U) rethrows -> U {
return try f(x)
}
precedencegroup FunctionApplicationPrecedenceRight {
lowerThan: AssignmentPrecedence
associativity: right
}
infix operator <<< : FunctionApplicationPrecedenceRight
@inline(__always)
public func <<<<T, U>(f: (T) throws -> U, x: T) rethrows -> U {
return try f(x)
}
@@ -1,37 +0,0 @@
//
// ArrayTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
@testable
import WalletFoundation
final class ArrayTests: XCTestCase {
func testOuter() {
let test = [0, 1, 2]
XCTAssertNil(test[safe: 5])
XCTAssertNil(test[safe: -1])
}
func testInner() {
let test = [1, 3, 5]
var value = test[safe: 1]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 3)
value = test[safe: 2]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 5)
value = test[safe: 0]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 1)
}
}
@@ -1,58 +0,0 @@
//
// OptionalsTests.swift
//
//
// Created by Juraldinio on 11/8/22.
//
import Foundation
import XCTest
@testable
import WalletFoundation
final class OptionalsTests: XCTestCase {
func testIsExists() {
var value: Int?
XCTAssertFalse(value.isExist)
value = 42
XCTAssertTrue(value.isExist)
}
func testOrCreate() {
var value: String?
XCTAssertEqual(value.orCreate("Hello"), "Hello")
value = "world"
XCTAssertEqual(value.orCreate("Hello"), "world")
}
func testOrTypedCreate() {
enum TestCases: String {
case first
case second
}
var value: String?
// Create new value because nil
XCTAssertEqual(value.orTypedCreate(TestCases.first), .first)
XCTAssertNotEqual(value.orTypedCreate(TestCases.first), .second)
value = "Hello"
// Create new value because not matched
XCTAssertEqual(value.orTypedCreate(TestCases.second), .second)
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .first)
value = "first"
// Not create new value and just match
XCTAssertEqual(value.orTypedCreate(TestCases.second), .first)
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .second)
}
}
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
-35
View File
@@ -1,35 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletKit",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletKit",
targets: ["WalletKit"]),
],
dependencies: [
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess"),
.package(name: "WalletFoundation", path: "../WalletFoundation"),
.package(name: "WalletNetwork", path: "../WalletNetwork")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletKit",
dependencies: ["eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"],
path: "./Sources"),
.testTarget(
name: "WalletKitTests",
dependencies: ["WalletKit"],
path: "Tests"//, // Test files
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
)
]
)
@@ -1,53 +0,0 @@
//
// DeepLink.swift
//
//
// Created by Juraldinio on 8/9/22.
//
import Foundation
public struct DeepLink {
public typealias DataParams = [AnyHashable: Any]
public typealias InfoParams = [String: Any]
public let action: DeepLinkAction
let data: Data?
public let info: [String: Any]?
public let url: URL?
public static func create(params: DataParams?) -> DeepLink? {
guard let params = params,
let rawAction = params["action"] as? String,
let action = DeepLinkAction(rawValue: rawAction),
let data = (params["params"] as? String)?.data(using: .utf8)
?? (try? JSONSerialization.data(withJSONObject: params["params"] as? [String: Any], options: [])) else {
return nil
}
return DeepLink(action: action, data: data, info: nil, url: URL(string: params["r"] as? String ?? ""))
}
public static func create(info: DataParams?) -> DeepLink? {
guard let info = info as? [String: Any],
let rawAction = info["action"] as? String,
let action = DeepLinkAction(rawValue: rawAction) else {
return nil
}
return DeepLink(action: action, data: nil, info: info, url: nil)
}
public func get<T: Decodable>(_ model: T.Type) -> T? {
guard let data = self.data else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}
public func dictionary() -> [String: Any] {
guard let data = self.data else { return [:] }
return (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:]
}
}
@@ -1,147 +0,0 @@
//
// Village.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import WalletFoundation
import WalletNetwork
public enum VillageError: Error {
case passwordNotMatch
}
final public class Village {
private enum Constants {
static let passwordKey = CommonKey("Account.Service.password")
}
private static var villages = [Village]()
private let environment: NetworkEnvironment
private var houses = [VillageHouse]()
private var foreigner: Foreigner? {
didSet {
guard let foreigner = self.foreigner else { return }
ApplicationSettings.device(description: "\(foreigner)")
}
}
// MARK: - Init
private init(environment: NetworkEnvironment) {
self.environment = environment
}
// MARK: - Public
/// Create instance of Village.
public static func get(with environment: NetworkEnvironment) -> Village {
if let village = Self.villages.first(where: { $0.environment.isEquals(other: environment) }) {
return village
}
let village = Village(environment: environment)
Self.villages.append(village)
return village
}
/// Get device instance.
public func device(force: Bool = false) async throws -> Device {
if force { self.foreigner = nil }
if let foreigner = self.foreigner { return foreigner }
if !force,
let foreigner = Foreigner.restore() {
do {
try await foreigner.retrievStatus(using: self.environment)
self.foreigner = foreigner
return foreigner
} catch { }
}
do {
let foreigner = try await Foreigner.create(for: DeviceUUID.shared.device, using: self.environment)
foreigner.save()
self.foreigner = foreigner
return foreigner
} catch {
throw DeviceError.create
}
}
public func attestate(device: Device) async throws -> CertifiedDevice {
try await Resident.permit(for: device, using: self.environment)
}
/// Create wallet key.
public func createWalletKey() throws -> WalletKey {
try WalletKey.create()
}
/// Create wallet case for hold wallet.
public func createWalletCase(with name: String, key: WalletKey? = nil) async throws -> WalletCase {
let walletKey: WalletKey
if let key = key {
walletKey = key
} else {
walletKey = try WalletKey.create()
}
return try await VillagerCoat.create(name: name, using: self.environment, key: walletKey)
}
/// Create bank that contain Wallets.
public func getBank(with password: String, on device: Device) -> Bank {
if let bank = self.houses.first(where: { $0.isEquals(password: password, device: device, environment: self.environment) }) {
return bank
}
if Self.password != password {
Self.password = password
}
let house = VillageHouse.create(for: password, on: device, environment: self.environment)
self.houses.append(house)
return house
}
public static func reset() {
VillageHouse.removeAllVillagers()
Self.password = nil
}
public static var isPasswordExists: Bool { WalletKeychain.instance.exist(Constants.passwordKey) }
public static func getPassword(password: String?) -> String? {
if let password {
return WalletKeychain.instance[Constants.passwordKey, password: password]
} else {
return WalletKeychain.instance[biometric: Constants.passwordKey]
}
}
// TODO: Remove this function after refactoring passwords
public static func updateCommonPassword(password: String, old: String) throws {
if WalletKeychain.instance[Constants.passwordKey, password: old] != nil {
Self.password = password
} else {
throw VillageError.passwordNotMatch
}
}
private static var password: String? {
get { WalletKeychain.instance[biometric: Constants.passwordKey] }
set { WalletKeychain.instance[Constants.passwordKey, password: newValue ?? ""] = newValue }
}
}
@@ -1,93 +0,0 @@
//
// Tractor.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
import KeyChainAccess
final class Foreigner: Device, Codable {
private enum Constant {
static let saveKey = "village.tractor.instance"
static let service = CodingUserInfoKey(rawValue: "service")!
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
case uuid
case id
}
// MARK: - Init
private init(uuid: String, id: String, isTrusted: Bool) {
self.uuid = uuid
self.id = id
self.isTrusted = isTrusted
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(String.self, forKey: .uuid)
self.id = try container.decode(String.self, forKey: .id)
}
// MARK: - Device
let uuid: String
let id: String
private(set) var isTrusted: Bool = false
private(set) var availableAccounts: Int?
// MARK: - CustomStringConvertible
var description: String { "uuid: \(self.uuid), id: \(self.id)" }
// MARK: - Methods
func save() {
guard let rawData = try? JSONEncoder().encode(self) else { return }
let keychain = Keychain()
try? keychain.set(rawData, key: Constant.saveKey)
}
func retrievStatus(using environment: NetworkEnvironment) async throws {
let service = DeviceService(environment: environment)
do {
let status = try await service.stateDevice(id: self.id)
self.availableAccounts = status.availableAccounts
self.isTrusted = status.isTrusted
return
} catch {
throw DeviceError.status
}
}
// MARK: - Static
static func create(for cid: String, using environment: NetworkEnvironment) async throws -> Foreigner {
let service = DeviceService(environment: environment)
do {
let device = try await service.createDevice(uuid: cid)
return Foreigner(uuid: device.uuid, id: device.id, isTrusted: device.isTrusted)
} catch {
throw DeviceError.create
}
}
static func restore() -> Foreigner? {
let keychain = Keychain()
guard let rawData = try? keychain.getData(Constant.saveKey) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(Foreigner.self, from: rawData)
}
}
@@ -1,47 +0,0 @@
//
// Resident.swift
//
//
// Created by Juraldinio on 25.08.2022.
//
import Foundation
import WalletFoundation
import WalletNetwork
struct Resident: CertifiedDevice {
let device: Device
let status: DeviceStatus
static func permit(for device: Device, using environment: NetworkEnvironment) async throws -> CertifiedDevice {
if device.isTrusted { return Resident(device: device, status: .valid) }
let result = await DeviceCheckToken.generate()
guard let deviceToken = try? result.get() else {
throw CertifiedDeviceError.token
}
let service = DeviceService(environment: environment)
do {
let status = try await service.checkDevice(id: device.id, token: deviceToken.token)
return Resident(device: device, status: status == .valid ? .valid : .invalid )
} catch let NetworkServiceError.gqlApplication(error) {
let deviceError: CertifiedDeviceError
switch error {
case "Invalid": deviceError = .invalid
case "DeviceNotFoundError": deviceError = .notFound
case "DecryptError": deviceError = .decrypt
case "UnexpectedDeviceKind": deviceError = .kind
case "UnexpectedError": deviceError = .unknown
default: deviceError = .unknown
}
throw deviceError
} catch {
throw CertifiedDeviceError.unknown
}
}
}
@@ -1,290 +0,0 @@
//
// File.swift
//
//
// Created by Juraldinio on 8/27/22.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
final class VillageHouse: Bank {
private enum Constants {
static let oldKey = "Account.Service.collection"
static let key = "Wallet.bank.service"
enum Keys {
static let current = CommonKey("Account.Service.current")
static let collection = CommonKey("Account.Service.collection")
}
}
// MARK: - Properties
private var password: String
private let device: Device
private let environment: NetworkEnvironment
private let activeSubject: CurrentValueSubject<Villager?, Never>
private let villagersSubject: CurrentValueSubject<[Villager], Never>
private var cancelables = Set<AnyCancellable>()
// MARK: - Init
private init(password: String, device: Device, environment: NetworkEnvironment) {
self.password = password
self.device = device
self.environment = environment
let collection = Self.restore()
self.villagersSubject = CurrentValueSubject(collection)
let active = Self.active(in: collection)
self.activeSubject = CurrentValueSubject(active)
self.villagersSubject
.sink { [weak self] villagers in
self?.save(villagers: villagers)
}
.store(in: &self.cancelables)
self.migrate(using: self.password)
}
// MARK: - Bank
var active: Wallet? { self.activeSubject.value }
var wallets: [Wallet] { self.villagersSubject.value }
lazy var activePublisher: AnyPublisher<Wallet?, Never> = self.activeSubject.map { $0 }.eraseToAnyPublisher()
lazy var walletsPublisher: AnyPublisher<[Wallet], Never> = self.villagersSubject.map { $0 }.eraseToAnyPublisher()
func accept(password: String) -> Bool { self.password == password }
func remove(wallet: Wallet) throws {
var villagers = self.villagersSubject.value
guard let villager = self.villager(by: wallet),
let index = villagers.firstIndex(where: { $0 == villager }) else {
throw BankError.notOwned
}
villagers.remove(at: index)
villager.clear()
self.villagersSubject.value = villagers
if let active = self.active,
villager.isEquals(other: active) {
try self.activate(wallet: nil)
}
}
func activate(wallet: Wallet?) throws {
guard let wallet else {
Settings.shared[Constants.Keys.current] = Data()
self.activeSubject.value = nil
return
}
guard let villager = self.villager(by: wallet) else {
throw BankError.notOwned
}
// We can save only wallet in accepted state.
if case .accepted = villager.state {
Settings.shared[Constants.Keys.current] = villager.jsonData()
}
self.activeSubject.value = villager
}
func add(using walletCase: WalletCase) async throws -> Wallet {
let villager = try await Villager.create(walletCase: walletCase, on: self.device, using: self.environment)
villager.updatePrivateKey(using: self.password)
let villagers = self.villagersSubject.value
self.villagersSubject.value = villagers + [villager]
return villager
}
func add(using purses: [Purse]) throws {
let wallets = purses
.filter { $0.bank?.isEquals(other: self) ?? false }
.filter { purse in
!self.wallets.contains(where: { $0.name == purse.name && $0.keyType.rawValue == purse.permission.permName })
}
.map { Villager.create(purse: $0) }
wallets
.filter { $0.key.privateKey.isExist }
.forEach { $0.updatePrivateKey(using: self.password) }
let villagers = self.villagersSubject.value
self.villagersSubject.value = villagers + wallets
}
func restore(using keys: WalletKey) async throws -> PurseHolder {
let holder = PurseHolder()
let service = AccountHyperionService(environment: self.environment)
let eosService = EOSService(environment: self.environment)
return try await holder.load(using: service, eosService: eosService, keys: keys, in: self)
}
func refreshStatus(wallet: Wallet?) async throws {
let villagers: [Villager]
if let wallet,
let villager = self.villager(by: wallet) {
villagers = [villager]
} else {
villagers = self.villagersSubject.value
}
_ = await withTaskGroup(of: Void.self) { group in
villagers.forEach { villager in
switch villager.state {
case .pending,
.creating:
group.addTask {
try? await villager.refresh(using: self.environment)
}
case .accepted,
.declined:
return
}
}
await group.waitForAll()
self.save(villagers: villagers)
}
}
func isEquals(other: Bank) -> Bool {
guard let house = other as? VillageHouse else { return false }
return self.password == house.password &&
self.device.isEquals(other: house.device) &&
self.environment.isEquals(other: house.environment)
}
func switchPassword(_ password: String, old: String) throws {
guard self.accept(password: old) else {
throw BankError.passwordNotMatch
}
self.password = password
old >>- { old in self.villagersSubject.value.forEach { $0.update(password: password, old: old) } }
}
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] {
// TODO: - After move EOS to frameworks!
/*guard password == self.password else { throw BankError.passwordNotMatch }
let villagers = keyUpdates.compactMap({ self.villager(by: $0.wallet) })
guard villagers.count != keyUpdates.count else { throw BankError.notOwned }
guard let privateKey = self.privateKey(password) else {
throw WalletError.privateKeyNotExists
}
return WalletKeyUpdate(wallet: self, oldPrivateKey: "", transitionId: "")
return try await villager.update(key: key, password: self.password, using: self.environment)*/
return []
}
// MARK: - Internal
func isEquals(password: String, device: Device, environment: NetworkEnvironment) -> Bool {
return self.password == password &&
self.device.isEquals(other: device) &&
self.environment.isEquals(other: environment)
}
// MARK: - Private
private func save(villagers: [Villager]) {
Settings.shared[Constants.Keys.collection] = villagers.jsonData()
}
private static func restore() -> [Villager] {
if let data: Data = Settings.shared[Constants.Keys.collection],
let villagers: [Villager] = data.jsonDecoded(type: Villager.self) {
return villagers
}
return []
}
private func migrate(using password: String) {
let migrateWallets: [Villager]
if let data = UserDefaults.standard.value(forKey: Constants.oldKey) as? Data,
let collection = try? JSONDecoder().decode([Villager.OldVillager].self, from: data) {
migrateWallets = collection.compactMap { $0.covert(password: password) }
} else {
migrateWallets = []
}
print(migrateWallets)
// Delete previous key
// UserDefaults.standard.set(nil, forKey: Constants.oldKey)
}
private func villager(by wallet: Wallet) -> Villager? {
guard let villager = wallet as? Villager,
self.villagersSubject.value.contains(where: { $0 == villager }) else {
return nil
}
return villager
}
// MARK: - Static
static func create(for password: String, on device: Device, environment: NetworkEnvironment) -> VillageHouse {
VillageHouse(password: password, device: device, environment: environment)
}
private static func active(in collection: [Villager]) -> Villager? {
if let data: Data = Settings.shared[Constants.Keys.current],
let villager: Villager = data.jsonDecoded(type: Villager.self),
collection.contains(where: { $0 == villager }) {
return villager
}
guard let rawValue: String = Settings.shared[Constants.Keys.current] else { return nil }
let name = rawValue.components(separatedBy: "@").first
let keyTypeString = rawValue.components(separatedBy: "@").last ?? ""
let keyType = WalletKeyType(rawValue: keyTypeString) ?? WalletKeyType.active
return collection.first(where: { $0.name == name && $0.keyType == keyType })
}
static func removeAllVillagers() {
Settings.shared[Constants.Keys.collection] = Data()
}
}
@@ -1,240 +0,0 @@
//
// Villager.swift
//
//
// Created by Juraldinio on 16.08.2022.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
extension String {
fileprivate static let privateKey = "privateKey"
}
final class Villager: Wallet, Codable, CustomStringConvertible {
struct OldVillager: Codable {
var username: String
var publicKey: String
var keyType: String
func covert(password: String) -> Villager? {
guard let keyType = WalletKeyType(rawValue: self.keyType),
let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey, password: password) else {
return nil
}
return Villager(name: self.username, key: key, keyType: keyType, state: .accepted)
}
}
private let stateSubject: CurrentValueSubject<WalletState, Never>
// MARK: - Init
private init(walletCase: WalletCase, keyType: WalletKeyType, state: WalletState) {
self.name = walletCase.name
self.key = walletCase.key
self.keyType = keyType
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
}
private init(name: String, key: WalletKey, keyType: WalletKeyType, state: WalletState) {
self.name = name
self.key = key
self.keyType = keyType
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
// Old values
case username
case publicKey
//
case name
case key
case keyType
case state
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// For save old format with username field!
let username = try container.decodeIfPresent(String.self, forKey: .username)
if let username {
self.name = username
} else {
self.name = try container.decode(String.self, forKey: .name)
}
self.keyType = try container.decode(WalletKeyType.self, forKey: .keyType)
let state = try container.decode(WalletState.self, forKey: .state)
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
// If first version we hold key on flat structure!
if let key = try? WalletKey(from: decoder) {
self.key = key
} else {
self.key = try container.decode(WalletKey.self, forKey: .key)
}
// TODO: - NEED COMPLETETASK
// self.key = try WalletKey.restore(using: self.name, type: self.keyType, publicKey: publicKey, password: "")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.key, forKey: .key)
try container.encode(self.keyType, forKey: .keyType)
try container.encode(self.stateSubject.value, forKey: .state)
}
// MARK: - Wallet
let name: String
private(set) var key: WalletKey
let keyType: WalletKeyType
var state: WalletState { self.stateSubject.value }
lazy var statePublisher: AnyPublisher<WalletState, Never> = self.stateSubject.eraseToAnyPublisher()
// MARK: - Equatable
static func == (lhs: Villager, rhs: Villager) -> Bool {
return lhs.name == rhs.name
&& lhs.key == rhs.key
&& lhs.keyType == rhs.keyType
&& lhs.state == rhs.state
&& lhs.name == rhs.name
}
// MARK: - CustomStringConvertible
var description: String { "(name: \(name), keyType: \(keyType), state: \(state), public: \(key.publicKey)" }
// MARK: - Internal
func refresh(using environment: NetworkEnvironment) async throws {
let service = AccountService(environment: environment)
let state: WalletState
switch self.state {
case .creating(let orderId),
.pending(let orderId):
do {
let order = try await service.order(with: orderId)
state = WalletState(order: order)
} catch {
throw WalletError.network(error)
}
default:
state = self.state
}
self.stateSubject.value = state
}
func update(key: WalletKey, using passrod: String) -> Wallet {
self.key = key
self.updatePrivateKey(using: passrod)
return self
}
func privateKey(_ value: String?) -> String? {
let privateKey: String?
if let password = value {
privateKey = WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password]
} else {
privateKey = WalletKeychain.instance[biometric: .key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)]
}
return privateKey
}
func updatePrivateKey(using password: String) {
guard let privateKey = self.key.privateKey else { return }
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
password: password] = privateKey
}
// TODO: - Need review
func migrate(password: String) {
let privateKey = WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: password]
WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: ""] = nil
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password] = privateKey
}
func update(password: String, old: String) {
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
password: password] = self.privateKey(old)
}
func clear() {
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: ""] = nil
}
// MARK: - Static
static func create(purse: Purse) -> Villager {
Villager(name: purse.name, key: purse.key, keyType: purse.keyType, state: .accepted)
}
static func create(walletCase: WalletCase, on device: Device, using environment: NetworkEnvironment) async throws -> Villager {
let service = AccountService(environment: environment)
do {
// return Villager(walletCase: walletCase, state: .pending)
let order = try await service.create(username: walletCase.name,
pubKey: walletCase.key.publicKey,
deviceId: device.id)
let state: WalletState
switch order.status {
case .creating: state = .creating(order.id)
case .active, .executed: state = .pending(order.id)
case .expired, .failed: state = .declined
case .completed: state = .accepted
}
return Villager(walletCase: walletCase, keyType: .owner, state: state)
} catch let NetworkServiceError.gqlApplication(error) {
if ApplicationSettings.ignoreCreateAccountSatus {
return Villager(walletCase: walletCase, keyType: .owner, state: .creating("HELLO"))
}
let walletError: WalletError
switch error {
case "invalid EOS public key": walletError = .invalidKey
case "INVALID DEVICE ID": walletError = .invalidDevice
case "UNKNOWN DEVICE": walletError = .device
case "UNCHECKED": walletError = .unchecked
case "UNTRUSTED": walletError = .untrusted
case "DEVICE LIMIT REACHED": walletError = .deviceLimit
case "DAILY LIMIT REACHED": walletError = .dailyLimit
case "UNAVAILABLE": walletError = .unavailable
case "ACCOUNT ALREADY EXISTS": walletError = .exists
default: walletError = .unavailable
}
throw walletError
} catch {
throw WalletError.unavailable
}
}
static func create(walletKey: WalletKey, using environment: NetworkEnvironment) async throws -> Villager {
// TODO: - Add logic
throw WalletError.unavailable
}
}
@@ -1,52 +0,0 @@
//
// VillagerCoat.swift
//
//
// Created by Juraldinio on 16.08.2022.
//
import Foundation
import WalletNetwork
final class VillagerCoat: WalletCase {
// MARK: - Init
private init(name: String, key: WalletKey) {
self.name = name
self.key = key
}
// MARK: - WalletCase
let name: String
private(set) var key: WalletKey
@discardableResult
func update(key: WalletKey) -> WalletKey {
self.key = key
return self.key
}
@discardableResult
func generateKey() -> WalletKey {
if let key = try? WalletKey.create() {
self.key = key
}
return self.key
}
// MARK: - Static
static func create(name: String, using environment: NetworkEnvironment, key: WalletKey) async throws -> VillagerCoat {
let walletName = name.lowercased()
let service = AccountService(environment: environment)
let isAvailable = await service.isAvailable(username: walletName)
switch isAvailable {
case .available: return VillagerCoat(name: walletName, key: key)
case .alreadyTaken: throw WalletCaseError.exists(name: walletName)
case .system: throw WalletCaseError.system
}
}
}
@@ -1,48 +0,0 @@
//
// Bank.swift
//
//
// Created by Juraldinio on 8/27/22.
//
import Foundation
import Combine
import WalletNetwork
public enum BankError: Error {
case empty
case notOwned
case passwordNotMatch
}
/// Bank that contain wallets.
public protocol Bank: AnyObject {
/// Current active wallet.
var active: Wallet? { get }
/// Publisher for observe changes.
var activePublisher: AnyPublisher<Wallet?, Never> { get }
/// Wallets in bank.
var wallets: [Wallet] { get }
/// Publisher for observe wallet changes.
var walletsPublisher: AnyPublisher<[Wallet], Never> { get }
/// Check password
func accept(password: String) -> Bool
/// Activate wallet
func activate(wallet: Wallet?) throws
/// Create new wallet in bank.
func add(using walletCase: WalletCase) async throws -> Wallet
/// Add Purse to bank with converting to Wallet
func add(using purses: [Purse]) throws
/// Restore wallet by key.
func restore(using keys: WalletKey) async throws -> PurseHolder
/// Remove wallet from bank.
func remove(wallet: Wallet) throws
/// Check wallet status
func refreshStatus(wallet: Wallet?) async throws
/// Update WalletKey for Wallet
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult]
/// Compare two banks
func isEquals(other: Bank) -> Bool
/// Switch password
func switchPassword(_ password: String, old: String) throws
}
@@ -1,40 +0,0 @@
//
// CertifiedDevice.swift
//
//
// Created by Juraldinio on 25.08.2022.
//
import Foundation
public enum CertifiedDeviceError: Error {
/// Unavailable generate token for check
case token
/// Device not fount on server
case notFound
/// Invalid token
case invalid
/// Token decription error
case decrypt
/// Unexpected device kind
case kind
/// Unknown error
case unknown
/// Already in progress
case progress
}
/// Device status
public enum DeviceStatus {
/// Valid
case valid
/// Invalid
case invalid
}
public protocol CertifiedDevice {
/// Device
var device: Device { get }
/// Device status
var status: DeviceStatus { get }
}
@@ -1,40 +0,0 @@
//
// Device.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import Combine
/// Errors for Device
public enum DeviceError: Error {
/// Failed while restore
case restore
/// Failed create device on server side
case create
/// Failed retrieve status
case status
}
public protocol Device: CustomStringConvertible {
/// Device generated Identity UUID
var uuid: String { get }
/// Server generated Identity
var id: String { get }
/// Is device trusted
var isTrusted: Bool { get }
func isEquals(other: Device) -> Bool
}
// MARK: - Equatable
extension Device {
func isEquals(other: Device) -> Bool {
return self.uuid == other.uuid &&
self.id == other.id &&
self.isTrusted == other.isTrusted
}
}
@@ -1,12 +0,0 @@
//
// ErrorDomain.swift
//
//
// Created by user on 31.10.2022.
//
import Foundation
enum ErrorDomain: Error {
case noEnvironment
}
@@ -1,101 +0,0 @@
//
// PurseHolder.swift
//
//
// Created by Juraldinio on 12/4/22.
//
import Foundation
import WalletFoundation
import WalletNetwork
import EosioSwift
import Combine
public struct Purse {
public let name: String
public let key: WalletKey
public let keyType: WalletKeyType
let permission: Permission
var bank: Bank?
}
public final class PurseHolder {
public enum PurseError: Error {
case empty
}
public private(set) var purses: [Purse] = []
private var count: Int = 0
func load(using service: AccountHyperionService, eosService: EOSService, keys: WalletKey, in bank: Bank) async throws -> PurseHolder {
let publicKey = keys.publicKey
let collection = await service.fetchNamesHyperion(publicKey: publicKey)
guard !collection.isEmpty else {
throw PurseError.empty
}
let _ = await withCheckedContinuation { continuation in
self.count = collection.count
collection.forEach { name in
DispatchQueue.global().async {
eosService.getAccount(name) { response in
self.count -= 1
switch response {
case let .success(accountInfo):
let permissions = accountInfo.permissions
.filter { permission in permission.requiredAuth.keys.contains(where: { $0.key == publicKey }) }
.sorted { $0.permName > $1.permName }
permissions.forEach { permission in
if (permission.permName == WalletKeyType.owner.rawValue) {
self.purses.append(Purse(name: name,
key: keys,
keyType: .owner,
permission: permission,
bank: bank))
}
let hasAddedOwnerKey = self.purses.contains(where: { account in
account.name == name &&
account.permission.requiredAuth.keys.first?.key == permission.requiredAuth.keys.first?.key
})
if let type = WalletKeyType(rawValue: permission.permName),
type != WalletKeyType.owner,
!hasAddedOwnerKey {
self.purses.append(Purse(name: name,
key: keys,
keyType: type,
permission: permission,
bank: bank))
}
}
default: break
}
if self.count == 0 {
continuation.resume(returning: self.purses)
}
}
}
}
}
return self
}
}
@@ -1,171 +0,0 @@
//
// Wallet.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import WalletNetwork
import Combine
/// Errors for wallet creation
public enum WalletError: Error {
/// Invalid EOS public key
case invalidKey
/// Device not found
case invalidDevice
/// Device not found
case device
/// Device does not do check phase
case unchecked
/// Device not trusted
case untrusted
/// Device limit reached
case deviceLimit
/// Daily limit reached
case dailyLimit
/// Unknown error
case unavailable
/// Network error
case network(Error)
/// Private key does not exists
case privateKeyNotExists
/// Wallet with such name already exists.
case exists
}
/// Wallet state
public enum WalletState: RawRepresentable {
/// Transaction sent to EOS.
case creating(String)
/// Creation in progress
case pending(String)
/// Created
case accepted
/// Creation failed
case declined
private enum Constants {
static let preffixCreate = "C"
static let preffixPending = "P"
}
// TODO:- remove public
public init(order: WalletOrder) {
switch order.status {
case .completed:
self = .accepted
case .creating:
self = .creating(order.id)
case .executed, .active:
self = .pending(order.id)
case .failed, .expired:
self = .declined
}
}
// MARK: - RawRepresentable
public init?(rawValue: String) {
switch rawValue {
case "accepted": self = .accepted
case "declined": self = .declined
default:
let value = String(rawValue.dropFirst())
let preffix = rawValue.first?.uppercased()
if preffix == Constants.preffixPending {
self = .pending(value)
} else {
self = .creating(value)
}
}
}
public var rawValue: String {
switch self {
case .accepted: return "accepted"
case .declined: return "declined"
case let .pending(value): return "\(Constants.preffixPending)\(value)"
case let .creating(value): return "\(Constants.preffixCreate)\(value)"
}
}
}
extension WalletState: Codable { }
public enum WalletKeyType: String, Codable, CustomStringConvertible {
case owner
case active
public var description: String { self.rawValue }
}
public struct WalletKeyUpdateResult {
public let wallet: Wallet
public let oldPrivateKey: String
public let transitionId: String
}
public struct WalletKeyUpdate {
public let wallet: Wallet
public let key: WalletKey
public init(wallet: Wallet, key: WalletKey) {
self.wallet = wallet
self.key = key
}
}
/// Wallet
public protocol Wallet: AnyObject {
/// Name
var name: String { get }
/// Keys
var key: WalletKey { get }
/// Type
var keyType: WalletKeyType { get }
/// State
var state: WalletState { get }
var statePublisher: AnyPublisher<WalletState, Never> { get }
@discardableResult
func update(key: WalletKey, using passrod: String) -> Wallet
func privateKey(_ value: String?) -> String?
/// Compare two Wallets
func isEquals(other: Wallet) -> Bool
}
public extension Wallet {
var isOnCreationState: Bool {
switch self.state {
case .declined,
.accepted:
return false
case .creating,
.pending:
return true
}
}
var isOnActiveState: Bool {
switch self.state {
case .creating,
.accepted:
return true
case .declined,
.pending:
return false
}
}
func isEquals(other: Wallet) -> Bool {
self.name == other.name &&
self.key == other.key &&
self.keyType == other.keyType &&
self.state == other.state
}
}
@@ -1,30 +0,0 @@
//
// WalletCase.swift
//
//
// Created by Juraldinio on 17.08.2022.
//
import Foundation
/// Wallet Case errors
public enum WalletCaseError: Error {
/// System error
case system
/// Wallet with name already exists
case exists(name: String)
}
/// Wallet case. This mean case that can contain wallet.
public protocol WalletCase: AnyObject {
/// Name
var name: String { get }
/// Key
var key: WalletKey { get }
/// Update key
@discardableResult
func update(key: WalletKey) -> WalletKey
/// Generate new key
@discardableResult
func generateKey() -> WalletKey
}
@@ -1,142 +0,0 @@
//
// WalletKey.swift
//
//
// Created by Juraldinio on 17.08.2022.
//
import Foundation
import eosswift
/// Errors for WalletKey
public enum WalletKeyError: Error {
/// System error
case system
/// Invalid public key
case invalidPublic
/// Invalid private key
case invalidPrivate
/// Invalid pair public and private keys
case invalidPair
///
case unlock
}
/// Represents Wallet key.
/// This keys must pass checks.
public enum WalletKey: Codable, Equatable {
/// Public key
case publicKey(String)
/// Public and Private key
case bunch(publicKey: String, privateKey: String)
public var publicKey: String {
switch self {
case let .publicKey(value): return value
case let .bunch(publicKey: value, privateKey: _): return value
}
}
public var privateKey: String? {
switch self {
case let .bunch(publicKey: _, privateKey: value): return value
default: return nil
}
}
func save() {
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
case publicKey
case privateKey
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let publicKey = try container.decode(String.self, forKey: .publicKey)
let privateKey = try container.decodeIfPresent(String.self, forKey: .privateKey)
if let privateKey {
self = .bunch(publicKey: publicKey, privateKey: privateKey)
} else {
self = .publicKey(publicKey)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .publicKey(value):
try container.encode(value, forKey: .publicKey)
case let .bunch(publicKey: publicKey, privateKey: privateKey):
try container.encode(publicKey, forKey: .publicKey)
try container.encode(privateKey, forKey: .privateKey)
}
}
// MARK: - Public static
public static func create(public: String, private: String) throws -> WalletKey {
let privateKey: EOSPrivateKey
do {
privateKey = try EOSPrivateKey(base58: `private`)
} catch {
throw WalletKeyError.invalidPrivate
}
let publicKey: EOSPublicKey
do {
publicKey = try EOSPublicKey(base58: `public`)
} catch {
throw WalletKeyError.invalidPublic
}
guard privateKey.publicKey.base58 == publicKey.base58 else {
throw WalletKeyError.invalidPair
}
return .bunch(publicKey: publicKey.base58, privateKey: privateKey.base58)
}
public static func create(public: String) throws -> WalletKey {
let publicKey: EOSPublicKey
do {
publicKey = try EOSPublicKey(base58: `public`)
} catch {
throw WalletKeyError.invalidPublic
}
return .publicKey(publicKey.base58)
}
public static func create(private: String) throws -> WalletKey {
let privateKey: EOSPrivateKey
do {
privateKey = try EOSPrivateKey(base58: `private`)
} catch {
throw WalletKeyError.invalidPrivate
}
return .bunch(publicKey: privateKey.publicKey.base58, privateKey: privateKey.base58)
}
/// Create key for wallet
public static func create() throws -> WalletKey {
guard let key = try? EOSPrivateKey() else { throw WalletKeyError.system }
return .bunch(publicKey: key.publicKey.base58, privateKey: key.base58)
}
// MARK: - Internal static
static func restore(using name: String, type: WalletKeyType, publicKey: String, password: String) throws -> WalletKey {
throw WalletKeyError.unlock
}
}
@@ -1,58 +0,0 @@
//
// DeepLinkActionTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
import WalletKit
final class DeepLinkActionTests: XCTestCase {
func testConvertFromString() {
XCTAssertEqual(DeepLinkAction.connect.rawValue, "connect_accounts")
XCTAssertEqual(DeepLinkAction.chat.rawValue, "chat")
XCTAssertEqual(DeepLinkAction.transfer.rawValue, "transfer")
XCTAssertEqual(DeepLinkAction.walletAuth.rawValue, "wallet_auth")
XCTAssertEqual(DeepLinkAction.tokenization.rawValue, "accept_tokenization")
XCTAssertEqual(DeepLinkAction.approveBuy.rawValue, "approve_buy")
XCTAssertEqual(DeepLinkAction.emission.rawValue, "emission")
XCTAssertEqual(DeepLinkAction.chatMessage.rawValue, "chat.message")
XCTAssertEqual(DeepLinkAction.transactionTransfer.rawValue, "transaction.incoming_transfer")
XCTAssertEqual(DeepLinkAction.transactionInheritance.rawValue, "transactions.incoming_inheritance")
XCTAssertEqual(DeepLinkAction.transactionEmission.rawValue, "transactions.emission")
XCTAssertEqual(DeepLinkAction.p2pDealNew.rawValue, "orders.new_deal")
XCTAssertEqual(DeepLinkAction.p2pDealComplete.rawValue, "orders.completed_deal")
XCTAssertEqual(DeepLinkAction.p2pDealCancel.rawValue, "orders.cancelled_deal")
XCTAssertEqual(DeepLinkAction.p2pDealDispute.rawValue, "orders.dispute_deal")
XCTAssertEqual(DeepLinkAction.news.rawValue, "news")
XCTAssertEqual(DeepLinkAction.competitivePrice.rawValue, "competitive_price")
}
func testConvertToString() {
XCTAssertEqual(DeepLinkAction(rawValue: "connect_accounts"), .connect)
XCTAssertEqual(DeepLinkAction(rawValue: "chat"), .chat)
XCTAssertEqual(DeepLinkAction(rawValue: "transfer"), .transfer)
XCTAssertEqual(DeepLinkAction(rawValue: "wallet_auth"), .walletAuth)
XCTAssertEqual(DeepLinkAction(rawValue: "accept_tokenization"), .tokenization)
XCTAssertEqual(DeepLinkAction(rawValue: "approve_buy"), .approveBuy)
XCTAssertEqual(DeepLinkAction(rawValue: "emission"), .emission)
XCTAssertEqual(DeepLinkAction(rawValue: "chat.message"), .chatMessage)
XCTAssertEqual(DeepLinkAction(rawValue: "transaction.incoming_transfer"), .transactionTransfer)
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.incoming_inheritance"), .transactionInheritance)
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.emission"), .transactionEmission)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.new_deal"), .p2pDealNew)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.completed_deal"), .p2pDealComplete)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.cancelled_deal"), .p2pDealCancel)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.dispute_deal"), .p2pDealDispute)
XCTAssertEqual(DeepLinkAction(rawValue: "news"), .news)
XCTAssertEqual(DeepLinkAction(rawValue: "competitive_price"), .competitivePrice)
}
func testConvertFailed() {
XCTAssertNil(DeepLinkAction(rawValue: "news1"))
}
}
@@ -1,21 +0,0 @@
//
// DeepLinkTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
@testable
import WalletKit
final class DeepLinkTests: XCTestCase {
func testCreateWithParamsFailed() {
XCTAssertNil(DeepLink.create(params: nil))
XCTAssertNil(DeepLink.create(params: [:]))
}
}
-36
View File
@@ -1,36 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletNetwork",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletNetwork",
targets: ["WalletNetwork"]),
],
dependencies: [
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
.package(name: "EosioSwift", path: "../Vendors/spm/eosio-swift-1.0.0"),
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.2")),
.package(name: "WalletFoundation", path: "../WalletFoundation"),
.package(name: "Mocker", path: "../../Vendors/spm/Mocker-3.0.1")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletNetwork",
dependencies: ["EosioSwift", "eosswift", "Alamofire", "WalletFoundation"],
path: "./Sources"),
.testTarget(
name: "WalletNetworkTests",
dependencies: ["WalletNetwork", "Mocker"],
resources: [
.process("Resources")
])
]
)
@@ -1,52 +0,0 @@
//
// AccountHyperionService.swift
//
//
// Created by Juraldinio on 12/4/22.
//
import Foundation
import Alamofire
import WalletFoundation
public struct AccountHyperionService: NetworkService {
private struct Response: Codable {
let accountNames: [String]
enum CodingKeys: String, CodingKey {
case accountNames = "account_names"
}
}
let environment: NetworkEnvironment
public init(environment: NetworkEnvironment) {
self.environment = environment
}
// Methods
public func fetchNamesHyperion(publicKey: String) async -> [String] {
guard var components = URLComponents(url: self.environment.hyperion.url(), resolvingAgainstBaseURL: false) else {
return []
}
components.path = "/v2/state/get_key_accounts"
components.queryItems = [URLQueryItem(name: "public_key", value: publicKey),
URLQueryItem(name: "skip", value: "0"),
URLQueryItem(name: "limit", value: "5000")]
guard let url = components.url else { return [] }
let response = try? await AF.request(
url,
method: .get,
headers: self.environment.usernames.httpHeaders)
.serializingDecodable(Response.self)
.value
guard let response = response else { return [] }
return response.accountNames
}
}
@@ -1,156 +0,0 @@
//
// AccountService.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import Alamofire
import WalletFoundation
public struct AccountService: NetworkService {
public enum ServiceError: Error {
case notificationEmptyAccount
case notificationEmptyToken
case notificationEmptyLanguage
}
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
// Methods
public func create(username: String, pubKey: String, deviceId: String) async throws -> WalletOrder {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let variables = AccountCreateRequest.Variables(username: username, pubKey: pubKey, deviceId: deviceId)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountCreateRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
.responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountCreateResponse>.self, decoder: decoder)
.value
switch result?.result {
case let .firstType(response): return response.order
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func order(with id: String) async throws -> WalletOrder {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let variables = AccountOrderRequest.Variables(uid: id)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountOrderRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
.responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountOrderResponse>.self, decoder: decoder)
.value
switch result?.result {
case let .firstType(response): return response.order
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public enum AvailableType {
case available
case alreadyTaken
case system
}
public func isAvailable(username: String) async -> AvailableType {
guard !username.isEmpty else { return .system }
let variables = AccountCheckNameRequest.Variables(username: username)
let response = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountCheckNameRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders)
.responseString { print("checkWalletName: \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountCheckNameResponse>.self)
.value
guard let response = response else { return .system }
switch response.result {
case .firstType: return .available
case let .secondType(type):
switch type {
case let .application(error: errorType) where errorType == "ALREADY_TAKEN":
return .alreadyTaken
default: return .system
}
}
}
public func updateNotification(token: String, accounts: [String], language: String) async throws {
if accounts.isEmpty { throw ServiceError.notificationEmptyAccount }
if token.isEmpty { throw ServiceError.notificationEmptyToken }
if language.isEmpty { throw ServiceError.notificationEmptyLanguage }
let variables = AccountNotificationTokenRequest.Variables(
deviceToken: token,
deviceType: "IOS",
eosAccounts: accounts,
langCode: language,
application: "MALINKA"
)
let result = try? await self.session.request(
self.environment.backend.url,
method: .post,
parameters: AccountNotificationTokenRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
// .responseString { print("updateNotification: \($0)") }
.serializingDecodable(GraphQLResult<AccountNotificationTokenResponse>.self)
.value
print(result ?? "")
}
}
@@ -1,50 +0,0 @@
//
// AccountNodeService.swift
//
//
// Created by Nut.Tech on 16.01.2023.
//
import Alamofire
import Foundation
public struct NodeService: NetworkService {
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
public func fetchActions(account: String,
limit: Int,
skip: Int? = nil) async throws -> [NodeAction] {
guard let url = URL(string: "https://eos.greymass.com") else { return [] } //self.environment.hyperion.url()
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [] }
components.path = "/v1/history/get_actions"
components.queryItems = [URLQueryItem(name: "account_name", value: account),
URLQueryItem(name: "offset", value: "\(-limit)")]
if let skip {
components.queryItems?.append(URLQueryItem(name: "pos", value: "\(skip)"))
}
do {
let result = try await self.session.request(
components,
method: .get
).serializingDecodable(NodeResponse.self)
.value
return result.actions
} catch {
throw error
}
}
}
@@ -1,31 +0,0 @@
//
// AccountCheckNameRequest.swift
//
//
// Created by Juraldinio on 11.08.2022.
//
import Foundation
struct AccountCheckNameRequest: Encodable {
struct Variables: Encodable {
let username: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "checkWalletName"
let query: String = #"""
query checkWalletName(
$username: String!
) {
checkWalletName(username: $username) { errors }
}
"""#.trimCompact()
}
struct AccountCheckNameResponse: GraphQLResponse {
static let node = "checkWalletName"
}
@@ -1,37 +0,0 @@
//
// AccountCreateRequest.swift
//
//
// Created by Juraldinio on 24.08.2022.
//
import Foundation
struct AccountCreateRequest: Encodable {
struct Variables: Encodable {
let username: String
let pubKey: String
let deviceId: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "CreateAccount"
let query: String = #"""
mutation CreateAccount($username: String!, $pubKey: String!, $deviceId: String!) {
createAccount(username: $username, pubKey: $pubKey, deviceId: $deviceId) {
errors, order { timeout, id, modified, status }, id
}
}
"""#.trimCompact()
}
struct AccountCreateResponse: GraphQLResponse {
static let node = "createAccount"
let id: Int
let order: WalletOrder
}
@@ -1,45 +0,0 @@
//
// AccountOrderRequest.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
struct AccountOrderRequest: Encodable {
struct Variables: Encodable {
let uid: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "accountOrder"
let query: String = #"""
query accountOrder(
$uid: String!
) {
accountOrder(uid: $uid) { timeout, id, modified, status, created }
}
"""#.trimCompact()
}
struct AccountOrderResponse: GraphQLResponse {
private enum Common: String, CodingKey {
case order = "accountOrder"
}
static let node = ""
let order: WalletOrder
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Common.self)
self.order = try container.decode(WalletOrder.self, forKey: .order)
}
}
@@ -1,31 +0,0 @@
//
// File.swift
//
//
// Created by Juraldinio on 8/28/22.
//
import Foundation
public struct WalletOrder: Codable {
public enum Status: String, Codable {
/// Order created.
case active = "ACTIVE"
/// Order expired.
case expired = "EXPIRED"
/// Sent to blockchain.
case executed = "EXECUTED"
/// Accepting EOS transaction. Already sent, but not all blocks created.
case creating = "TRX_IN_CHAIN"
/// Failed.
case failed = "FAILED"
/// Created and completed.
case completed = "COMPLETED"
}
public let timeout: Date
public let modified: Date
public let status: Status
public let id: String
}
@@ -1,15 +0,0 @@
//
// NodeAct.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeAct: Decodable {
public let data: NodeData
public let name: String
public let authorization: [NodeActAuthorization]
public let account: String
}
@@ -1,13 +0,0 @@
//
// NodeActAuthorization.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeActAuthorization: Codable {
public let actor: String
public let permission: String
}
@@ -1,22 +0,0 @@
//
// NodeAction.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeAction: Decodable {
public let accountActionSeq: Int
public let actionTrace: NodeActionTrace
public let globalActionSeq: Int
public let irreversible: Bool
enum CodingKeys: String, CodingKey {
case accountActionSeq = "account_action_seq"
case actionTrace = "action_trace"
case globalActionSeq = "global_action_seq"
case irreversible
}
}
@@ -1,24 +0,0 @@
//
// NodeActionTrace.swift
//
//
// Created by Nut.Tech on 11.01.2023.
//
import Foundation
public struct NodeActionTrace: Decodable {
public let act: NodeAct
public let blockNum: Int
public let blockTime: String
public let receiver: String?
public let trxId: String
enum CodingKeys: String, CodingKey {
case blockNum = "block_num"
case blockTime = "block_time"
case trxId = "trx_id"
case receiver
case act
}
}
@@ -1,47 +0,0 @@
//
// NodeData.swift
//
// Created by NUT.TECH on 10.01.2023.
//
import Foundation
public struct NodeData: Decodable {
public let data: [String: Any]
public init(from decoder: Decoder) throws {
// Create a decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempData = [String: Any]()
// Loop through each key
for key in container.allKeys {
if let value = try? container.decode(NodeData.self, forKey: key) {
tempData[key.stringValue] = value.data
} else if let value = try? container.decode(String.self, forKey: key) {
tempData[key.stringValue] = value
} else if let value = try? container.decode(Int.self, forKey: key) {
tempData[key.stringValue] = value
}
}
self.data = tempData
}
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
}
@@ -1,12 +0,0 @@
//
// NodeResponse.swift
//
//
// Created by Nut.Tech on 11.01.2023.
//
import Foundation
public struct NodeResponse: Decodable {
public let actions: [NodeAction]
}
@@ -1,41 +0,0 @@
//
// CommonEnvironment.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
public struct CommonEnvironment: NetworkEnvironment {
public let configuration: URLSessionConfiguration?
public let usernames: NetworkStaticAPIRecord
public let hyperion: NetworkDynamicAPIRecord
public let backend: NetworkStaticAPIRecord
public let node: NetworkDynamicAPIRecord
public init(usernames: NetworkStaticAPIRecord,
backend: NetworkStaticAPIRecord,
hyperion: NetworkDynamicAPIRecord,
node: NetworkDynamicAPIRecord) {
self.configuration = nil
self.usernames = usernames
self.backend = backend
self.hyperion = hyperion
self.node = node
}
public init(usernames: URL,
backend: URL,
hyperion: @escaping NetworkDynamicAPIRecord.Dynamic,
node: @escaping NetworkDynamicAPIRecord.Dynamic,
headers: [String: String] = [:]) {
let userRecord = NetworkStaticAPIRecord(url: usernames, headers: headers)
let backendRecord = NetworkStaticAPIRecord(url: backend, headers: headers)
let hyperionRecord = NetworkDynamicAPIRecord(url: hyperion, headers: headers)
let nodeRecord = NetworkDynamicAPIRecord(url: node, headers: headers)
self.init(usernames: userRecord, backend: backendRecord, hyperion: hyperionRecord, node: nodeRecord)
}
}
@@ -1,106 +0,0 @@
//
// GraphQLResult.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import WalletFoundation
protocol GraphQLResponse: Decodable {
static var node: String { get }
}
struct GraphQLError: Decodable {
let message: String
}
enum GraphQLResponseError: Decodable {
enum Place: CustomStringConvertible {
case node
case failed
var description: String {
switch self {
case .node: return "node"
case .failed: return "failed"
}
}
}
case system(errors: [GraphQLError])
case application(error: String)
case decode(Place)
init(from decoder: Decoder) throws {
self = .system(errors: [])
}
// MARK: -
var networkServiceError: NetworkServiceError {
switch self {
case let .system(errors: errors): return .gqlSystem(errors.map { $0.message })
case let .application(error: error): return .gqlApplication(error)
case let .decode(place): return .gqlDecode(place.description)
}
}
}
let KEY_ERRORS = "errors"
struct GraphQLResult<Resp: GraphQLResponse>: Decodable {
let result: Either<Resp, GraphQLResponseError>
private enum Common: String, CodingKey {
case data
case errors
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Common.self)
// System error catch
if let errors = try? container.decode([GraphQLError].self, forKey: .errors) {
self.result = .secondType(.system(errors: errors))
return
}
// If node is empty
guard !Resp.node.isEmpty else {
if let response = try? container.decode(Resp.self, forKey: .data) {
self.result = .firstType(response)
} else {
self.result = .secondType(.decode(.failed))
}
return
}
let dataContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .data)
guard let key = DynamicCodingKeys(stringValue: Resp.node) else {
self.result = .secondType(.decode(.node))
return
}
if let responseContainer = try? dataContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: key),
let errorKey = DynamicCodingKeys(stringValue: KEY_ERRORS),
let error = try? responseContainer.decode(String.self, forKey: errorKey) {
self.result = .secondType(.application(error: error))
} else if let response = try? dataContainer.decode(Resp.self, forKey: key) {
self.result = .firstType(response)
} else {
self.result = .secondType(.decode(.failed))
}
}
}
@@ -1,84 +0,0 @@
//
// NetworkService.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
import Alamofire
public struct NetworkStaticAPIRecord {
public typealias Headers = [String: String]
public let url: URL
public let headers: Headers?
public init(url: URL, headers: Headers?) {
self.url = url
self.headers = headers
}
var httpHeaders: HTTPHeaders? {
guard let headers = self.headers else { return nil }
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
}
}
public struct NetworkDynamicAPIRecord {
public typealias Headers = [String: String]
public typealias Dynamic = () -> URL
public let url: Dynamic
public let headers: Headers?
public init(url: @escaping Dynamic, headers: Headers?) {
self.url = url
self.headers = headers
}
var httpHeaders: HTTPHeaders? {
guard let headers = self.headers else { return nil }
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
}
}
public protocol NetworkEnvironment {
var configuration: URLSessionConfiguration? { get }
var usernames: NetworkStaticAPIRecord { get }
var backend: NetworkStaticAPIRecord { get }
var hyperion: NetworkDynamicAPIRecord { get }
var node: NetworkDynamicAPIRecord { get }
func isEquals(other: NetworkEnvironment) -> Bool
}
public protocol NetworkService {
init(environment: NetworkEnvironment)
}
public enum NetworkServiceError: Error {
case invalidUrl
case gqlSystem([String])
case gqlApplication(String)
case gqlDecode(String)
case uncatched
}
// MARK: - Equatable
extension NetworkEnvironment {
public func isEquals(other: NetworkEnvironment) -> Bool {
return self.configuration == other.configuration &&
self.usernames.url == other.usernames.url &&
self.usernames.headers == other.usernames.headers &&
self.backend.url == other.backend.url &&
self.backend.headers == other.backend.headers &&
self.hyperion.url() == other.hyperion.url() &&
self.hyperion.headers == other.hyperion.headers &&
self.node.url() == other.node.url() &&
self.node.headers == other.node.headers
}
}
@@ -1,97 +0,0 @@
//
// DeviceService.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import Alamofire
public struct DeviceService: NetworkService {
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
public func createDevice(uuid: String) async throws -> Device {
let variables = RegisterDeviceRequest.Variables(cid: uuid)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: RegisterDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print($0) }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<RegisterDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(device): return Device(uuid: uuid, id: device.uid, isTrusted: device.isTrustedNow)
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func stateDevice(id: String) async throws -> StateDevice {
let variables = StateDeviceRequest.Variables(uid: id)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: StateDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<StateDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(state): return StateDevice(availableAccounts: state.availableAccounts,
isTrusted: state.isTrustedNow)
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func checkDevice(id: String, token: String) async throws -> DeviceStatus {
let variables = CheckDeviceRequest.Variables(uid: id, token: token)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: CheckDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print($0) }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<CheckDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(status): return status.status
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
}
@@ -1,35 +0,0 @@
//
// CheckDeviceRequest.swift
//
//
// Created by Juraldinio on 24.08.2022.
//
import Foundation
struct CheckDeviceRequest: Encodable {
struct Variables: Encodable {
let uid: String
let token: String
}
// MARK: - GraphQL
let operationName = "CheckDevice"
let query: String = #"""
mutation CheckDevice($uid: String!, $token: String!) {
checkDevice(uid: $uid, token: $token) {
status
}
}
"""#.trimCompact()
let variables: Variables
}
struct CheckDeviceResponse: GraphQLResponse {
static let node = "checkDevice"
let status: DeviceStatus
}
@@ -1,36 +0,0 @@
//
// RegisterDeviceRequest.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
struct RegisterDeviceRequest: Encodable {
struct Variables: Encodable {
let cid: String
}
// MARK: - GraphQL
let operationName = "RegisterDevice"
let query: String = #"""
mutation RegisterDevice($cid: String) {
registerDevice(kind: IOS, cid: $cid) {
isTrustedNow
uid
}
}
"""#.trimCompact()
let variables: Variables
}
struct RegisterDeviceResponse: GraphQLResponse {
static let node = "registerDevice"
let isTrustedNow: Bool
let uid: String
}
@@ -1,34 +0,0 @@
//
// StateDeviceRequest.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
struct StateDeviceRequest: Encodable {
struct Variables: Encodable {
let uid: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "device"
let query: String = #"""
query device(
$uid: String!
) {
device(uid: $uid) { availableAccounts, isTrustedNow }
}
"""#.trimCompact()
}
struct StateDeviceResponse: GraphQLResponse {
static let node = "device"
let availableAccounts: Int
let isTrustedNow: Bool
}
@@ -1,18 +0,0 @@
//
// Device.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
/// Model represent device
public struct Device {
/// Device generated Identity UUID
public let uuid: String
/// Server generated Identity
public let id: String
/// Is device trusted
public let isTrusted: Bool
}
@@ -1,14 +0,0 @@
//
// DeviceStatus.swift
//
//
// Created by Juraldinio on 23.08.2022.
//
import Foundation
public enum DeviceStatus: String, Decodable {
case valid = "Valid"
case invalid = "Invalid"
case unexpected = "UnexpectedError"
}
@@ -1,13 +0,0 @@
//
// StateDevice.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
public struct StateDevice {
public let availableAccounts: Int
public let isTrusted: Bool
}
@@ -1,68 +0,0 @@
//
// EOSService.swift
//
//
// Created by Juraldinio on 31.07.2022.
//
import Foundation
import Alamofire
import WalletFoundation
import EosioSwift
import eosswift
public struct EOSService: NetworkService {
let environment: NetworkEnvironment
public init(environment: NetworkEnvironment) {
self.environment = environment
}
// Methods
public func requireNodes() async -> [String] {
let result = try? await AF.request(
self.environment.backend.url,
method: .post,
parameters: EOSNodeRequest(),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
// .responseString(completionHandler: { print(">>>~~~>>> \($0)") })
.serializingDecodable(GraphQLResult<EOSNodesResponse>.self)
.value
switch result?.result {
case let .firstType(value):
return value.nodes.map { $0.url }
default:
return []
}
}
public func requireHyperions() async -> [String] {
let result = try? await AF.request(
self.environment.backend.url,
method: .post,
parameters: EOSHyperionsRequest(),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
.serializingDecodable(GraphQLResult<EOSHyperionsResponse>.self)
.value
switch result?.result {
case let .firstType(value):
return value.hyperions.map { $0.url }
default:
return []
}
}
public func getAccount(_ account: String, completion: @escaping (EosioResult<EosioRpcAccountResponse, EosioError>) -> Void) {
EosioRpcProvider(endpoint: self.environment.node.url(), headers: self.environment.node.headers)
.getAccount(requestParameters: EosioRpcAccountRequest(accountName: account), completion: completion)
}
}
@@ -1,35 +0,0 @@
//
// EOSHyperionsRequest.swift
//
//
// Created by NUT.Tech on 01.08.2022.
//
import Foundation
struct EOSHyperionsRequest: Encodable {
// MARK: - GraphQL
let query: String = #"""
query {
getEOSHyperions {
hyperions {
url
}
}
}
"""#.trimCompact()
}
struct EOSHyperionsResponse: GraphQLResponse {
static let node = "getEOSHyperions"
struct Hyperion: Decodable {
let url: String
}
let hyperions: [Hyperion]
}
@@ -1,37 +0,0 @@
//
// EOSNodesQueryRequest.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
struct EOSNodeRequest: Encodable {
// MARK: - GraphQL
let operationName = "getEOSNodes"
let query: String = #"""
query getEOSNodes {
getEOSNodes {
nodes {
url
}
}
}
"""#.trimCompact()
let variables: [String: String]? = nil
}
struct EOSNodesResponse: GraphQLResponse {
static let node = "getEOSNodes"
struct Node: Decodable {
let url: String
}
let nodes: [Node]
}
@@ -1,311 +0,0 @@
//
// DeviceServiceTests.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
import XCTest
import WalletFoundation
@testable
import WalletNetwork
import Mocker
final class DeviceServiceTests: XCTestCase {
override func tearDown() {
Mocker.removeAll()
}
// MARK: - CreateDevice
// When receive incorrect status we must throw uncatched exception.
func testCreateDeviceFailUncatched() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 500,
data: [ .post: Data() ]
).register()
do {
_ = try await service.createDevice(uuid: uuid)
XCTFail("Require to fail")
} catch NetworkServiceError.uncatched {
XCTAssertTrue(true)
} catch {
XCTFail("Require '\(NetworkServiceError.uncatched)' exception")
}
}
// When we receive incorrect data that we can't decode.
func testCreateDeviceFailDecode() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.createDeviceDecodeFailed ]
).register()
do {
_ = try await service.createDevice(uuid: uuid)
} catch NetworkServiceError.gqlDecode(let message) {
XCTAssertEqual(message, "failed")
} catch {
XCTFail("Catch exceprion '\(error)' but require NetworkServiceError.gqlDecode")
}
}
// When we successed create device.
func testCreateDeviceSuccessTrusted() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.createDeviceTrusted ]
).register()
do {
let device = try await service.createDevice(uuid: uuid)
XCTAssertTrue(device.isTrusted)
XCTAssertEqual(device.id, "a88c4532a09024c245e9")
XCTAssertEqual(device.uuid, uuid)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Test send correct request.
func testCreateDeviceSuccessRequest() async {
let url = URL(string: "https://malinka.life/query")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
let requestExpectation = XCTestExpectation(description: "Request")
let completionExpectation = XCTestExpectation(description: "Completion")
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
XCTAssertEqual(url, mock.request.url)
XCTAssertEqual(request.method, .post)
XCTAssertNotNil(object)
let registerDevice = RegisterDeviceRequest(variables: RegisterDeviceRequest.Variables(cid: uuid))
XCTAssertNotNil(object!["query"])
object!["query"]!.map(firstTypeTransform: {
XCTAssertEqual($0, registerDevice.query)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["operationName"])
object!["operationName"]!.map(firstTypeTransform: {
XCTAssertEqual($0, registerDevice.operationName)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["variables"])
object!["variables"]!.map(firstTypeTransform: {
XCTFail("Require '[String: String]' type but receive \($0)")
}, secondTypeTransform: {
XCTAssertNotNil($0["cid"])
XCTAssertEqual($0["cid"], registerDevice.variables.cid)
})
requestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
_ = try? await service.createDevice(uuid: uuid)
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
}
// MARK: - StateDevice
// Receive null values in fields - we must throw exception.
func testQueryStateDeviceException() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateNull ]
).register()
do {
_ = try await service.stateDevice(id: id)
XCTFail("Require catch exceprion NetworkServiceError.gqlDecode")
} catch {
XCTAssertTrue(true)
}
}
// Receive untrusted status.
func testQueryStateDeviceUntrusted() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateUntrusted ]
).register()
do {
let state = try await service.stateDevice(id: id)
XCTAssertFalse(state.isTrusted)
XCTAssertEqual(state.availableAccounts, 0)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Receive trusted status.
func testQueryStateDeviceTrusted() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateTrusted ]
).register()
do {
let state = try await service.stateDevice(id: id)
XCTAssertTrue(state.isTrusted)
XCTAssertEqual(state.availableAccounts, 2)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Check sent correct request.
func testQueryStateDeviceRequest() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
let requestExpectation = XCTestExpectation(description: "Request")
let completionExpectation = XCTestExpectation(description: "Completion")
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
XCTAssertEqual(url, mock.request.url)
XCTAssertEqual(request.method, .post)
XCTAssertNotNil(object)
let stateRequest = StateDeviceRequest(variables: StateDeviceRequest.Variables(uid: id))
XCTAssertNotNil(object!["query"])
object!["query"]!.map(firstTypeTransform: {
XCTAssertEqual($0, stateRequest.query)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["operationName"])
object!["operationName"]!.map(firstTypeTransform: {
XCTAssertEqual($0, stateRequest.operationName)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["variables"])
object!["variables"]!.map(firstTypeTransform: {
XCTFail("Require '[String: String]' type but receive \($0)")
}, secondTypeTransform: {
XCTAssertNotNil($0["uid"])
XCTAssertEqual($0["uid"], stateRequest.variables.uid)
})
requestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
_ = try? await service.stateDevice(id: id)
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
}
// MARK: - CheckDevice
}
@@ -1,28 +0,0 @@
//
// DeviceServiceData.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
final class DeviceServiceData {
static let createDeviceTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessTrusted",
withExtension: "json")!)
static let createDeviceDecodeFailed = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessNull",
withExtension: "json")!)
static let queryDeviceStateNull = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessNull",
withExtension: "json")!)
static let queryDeviceStateUntrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessUntrusted",
withExtension: "json")!)
static let queryDeviceStateTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessTrusted",
withExtension: "json")!)
static let nodeActionsExample = try! Data(contentsOf: Bundle.module.url(forResource: "v1historyTestmalinka1",
withExtension: "json")!)
}
@@ -1,44 +0,0 @@
//
// NetworkEnvironmentImpl.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
import WalletNetwork
struct NetworkEnvironmentImpl: NetworkEnvironment {
let configuration: URLSessionConfiguration?
let usernames: NetworkStaticAPIRecord
let backend: NetworkStaticAPIRecord
let hyperion: NetworkDynamicAPIRecord
let node: NetworkDynamicAPIRecord
static func createStubUsernames(configuration: URLSessionConfiguration,
url: URL,
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
let record = NetworkStaticAPIRecord(url: url, headers: headers)
let dumpUrl = URL(string: "Empty")!
return NetworkEnvironmentImpl(configuration: configuration,
usernames: record,
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
node: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil))
}
static func createStubNodes(configuration: URLSessionConfiguration,
url: URL,
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
let record = NetworkDynamicAPIRecord(url: { url }, headers: headers)
let dumpUrl = URL(string: "Empty")!
return NetworkEnvironmentImpl(configuration: configuration,
usernames: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
node: record)
}
}
@@ -1,120 +0,0 @@
//
// NodeActionModelsTests.swift
//
//
// Created by Nut.Tech on 13.01.2023.
//
import WalletNetwork
import Foundation
import XCTest
final class NodeActionModelsTests: XCTestCase {
func testNodeActAuthorization() {
let data = """
{
"actor": "testmalinka1",
"permission": "owner"
}
""".data(using: .utf8)!
let nodeAct = try? JSONDecoder().decode(NodeActAuthorization.self, from: data)
XCTAssertNotNil(nodeAct, "Error parsing NodeActAuthorization object")
XCTAssertEqual(nodeAct?.actor, "testmalinka1", "Error parsing NodeActAuthorization.actor value")
XCTAssertEqual(nodeAct?.permission, "owner", "Error parsing NodeActAuthorization.owner value")
}
func testNodeData() {
let data = """
{
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
}
""".data(using: .utf8)!
let nodeData = try? JSONDecoder().decode(NodeData.self, from: data)
XCTAssertNotNil(nodeData, "Error parsing NodeData object")
guard let parsedData = nodeData?.data else {
XCTFail("Error parsing data value")
return
}
XCTAssertEqual(parsedData["from"] as? String, "testmalinka1", "Error parsing data value")
XCTAssertEqual(parsedData["memo"] as? String, "buyram:testmalinka1", "Error parsing data value")
XCTAssertEqual(parsedData["quantity"] as? String, "0.1000 EOS", "Error parsing data value")
XCTAssertEqual(parsedData["to"] as? String, "malinkawallt", "Error parsing data value")
}
func testNodeAct() {
let data = """
{
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
}
""".data(using: .utf8)!
let nodeAct = try? JSONDecoder().decode(NodeAct.self, from: data)
XCTAssertNotNil(nodeAct, "Error parsing NodeAct object")
XCTAssertNotNil(nodeAct?.data, "Error parsing NodeAct.data object")
XCTAssertNotNil(nodeAct?.authorization, "buyram:testmalinka1", file: "Error parsing NodeAct.authorization objects")
XCTAssertEqual(nodeAct?.authorization.count, 1, "Error parsing NodeAct.authorization objects")
XCTAssertEqual(nodeAct?.name, "transfer", "Error parsing NodeAct.name value")
XCTAssertEqual(nodeAct?.account, "eosio.token", "Error parsing NodeAct.account value")
}
func testNodeActionTrace() {
guard let url = Bundle.module.url(forResource: "NodeActionTrace", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeActionTrace.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.act, "Error parsing NodeActionTrace.act object")
XCTAssertEqual(result.blockNum, 288713481, "Error parsing NodeActionTrace.blockNum value")
XCTAssertEqual(result.blockTime, "2023-01-12T15:27:01.500", "Error parsing NodeActionTrace.blockTime value")
XCTAssertEqual(result.receiver, "malinkawallt", "Error parsing NodeActionTrace.receiver value")
XCTAssertEqual(result.trxId, "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c", "Error parsing NodeActionTrace.trxId value")
}
func testNodeAction() {
guard let url = Bundle.module.url(forResource: "NodeAction", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeAction.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.actionTrace, "Error parsing NodeAction.actionTrace value")
XCTAssertEqual(result.accountActionSeq, 2858, "Error parsing NodeAction.accountActionSeq value")
XCTAssertEqual(result.globalActionSeq, 357585823156, "Error parsing NodeAction.globalActionSeq value")
XCTAssertTrue(result.irreversible, "Error parsing NodeAction.irreversible value")
}
func testNodeResponse() {
guard let url = Bundle.module.url(forResource: "NodeResponse", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeResponse.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.actions, "Error parsing NodeResponse")
XCTAssertEqual(result.actions.count, 1, "Error parsing NodeResponse.actions")
}
}
@@ -1,68 +0,0 @@
//
// NodeServiceTests.swift
//
//
// Created by Nut.Tech on 16.01.2023.
//
import XCTest
@testable import WalletNetwork
import Mocker
final class NodeServiceTests: XCTestCase {
override func tearDown() {
Mocker.removeAll()
}
func testFetchNodeActions() async {
let accountName = "testmalinka3"
let offset = -100
let url = URL(string: "https://eos.greymass.com/v1/history/get_actions?account_name=\(accountName)&offset=\(offset)")!
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubNodes(configuration: configuration, url: url)
let service = NodeService(environment: env)
Mock(url: url,
ignoreQuery: true,
dataType: .json,
statusCode: 200,
data: [ .get: DeviceServiceData.nodeActionsExample ]
).register()
do {
let state = try await service.fetchActions(account: accountName,
limit: offset)
guard let firstAction = state.first else {
XCTFail("Error parsing NodeActions")
return
}
XCTAssertEqual(state.count, 100, "Incorrect number of node actions")
XCTAssertEqual(firstAction.accountActionSeq, 2759, "Error parsing NodeAction.accountActionSeq")
XCTAssertEqual(firstAction.globalActionSeq, 357563493447, "Error parsing NodeAction.globalActionSeq")
XCTAssertTrue(firstAction.irreversible, "Error parsing NodeAction.irreversible")
XCTAssertNotNil(firstAction.actionTrace, "Error parsing NodeAction.actionTrace")
XCTAssertEqual(firstAction.actionTrace.blockTime, "2023-01-09T13:55:06.000", "Error parsing NodeAction.actionTrace.blockTime")
XCTAssertEqual(firstAction.actionTrace.blockNum, 288184258, "Error parsing NodeAction.actionTrace.blockNum")
XCTAssertEqual(firstAction.actionTrace.trxId, "a4dfd44c82cf0a3be1930bf6e4ec6ea51c1b331a4e77864a0d2cae4d06e8683d", "Error parsing NodeAction.actionTrace.trxId")
XCTAssertEqual(firstAction.actionTrace.receiver, "testmalinka1", "Error parsing NodeAction.actionTrace.receiver")
let act = firstAction.actionTrace.act
XCTAssertEqual(act.account, "eosio.token", "Error parsing NodeActionTrace.act.account")
XCTAssertEqual(act.name, "transfer", "Error parsing NodeActionTrace.act.name")
XCTAssertEqual(act.authorization.count, 1, "Error parsing NodeActionTrace.act.authorization")
XCTAssertNotNil(act.data, "Error parsing NodeActionTrace.act.data")
guard let authorization = act.authorization.first else {
XCTFail("Error parsing NodeAction.actionTrace.act.authorization")
return
}
XCTAssertEqual(authorization.permission, "owner", "Error parsing NodeActionTrace.act.authorization.permission")
XCTAssertEqual(authorization.actor, "testmalinka1", "Error parsing NodeActionTrace.act.authorization.actor")
} catch {
XCTFail("Catch exceprion \(error)")
}
}
}
@@ -1 +0,0 @@
{"data": {"registerDevice": {"isTrustedNow": false, "uid": null}}}
@@ -1 +0,0 @@
{"data": {"registerDevice": {"isTrustedNow": true, "uid": "a88c4532a09024c245e9"}}}
@@ -1,51 +0,0 @@
{
"account_action_seq": 2858,
"action_trace": {
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
},
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"global_action_seq": 357585823156,
"irreversible": true
}
@@ -1,44 +0,0 @@
{
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
}
@@ -1,57 +0,0 @@
{
"actions": [
{
"account_action_seq": 2858,
"action_trace": {
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
},
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"global_action_seq": 357585823156,
"irreversible": true
}
],
"head_block_num": 288838483,
"last_irreversible_block": 288838158
}
@@ -1,8 +0,0 @@
{
"data": {
"device": {
"availableAccounts": null,
"isTrustedNow": null
}
}
}
@@ -1,8 +0,0 @@
{
"data": {
"device": {
"availableAccounts": 2,
"isTrustedNow": true
}
}
}
@@ -1,8 +0,0 @@
{
"data": {
"device": {
"availableAccounts": 0,
"isTrustedNow": false
}
}
}
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>JMalinkaWallet.ipa</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>bitcode</key>
<false/>
<key>buildNumber</key>
<string>12</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>35C003923C51D78DF01DBCBFF8DAC6666C09412D</string>
<key>dateExpires</key>
<string>8/4/22</string>
<key>type</key>
<string>iOS Distribution</string>
</dict>
<key>entitlements</key>
<dict>
<key>application-identifier</key>
<string>GENPCTDS3G.com.juraldinio.wallet</string>
<key>aps-environment</key>
<string>production</string>
<key>beta-reports-active</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:paycashonline.test-app.link</string>
<string>applinks:paycashonline-alternate.test-app.link</string>
<string>applinks:paycashonline-alternate.app.link</string>
<string>applinks:paycashonline.app.link</string>
</array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
<key>com.apple.developer.team-identifier</key>
<string>GENPCTDS3G</string>
<key>get-task-allow</key>
<false/>
</dict>
<key>name</key>
<string>JMalinkaWallet.app</string>
<key>profile</key>
<dict>
<key>UUID</key>
<string>215599e9-99cb-4fbc-832f-12be30a2bcb0</string>
<key>dateExpires</key>
<string>8/4/22</string>
<key>name</key>
<string>MalinkaWallet</string>
</dict>
<key>symbols</key>
<true/>
<key>team</key>
<dict>
<key>id</key>
<string>GENPCTDS3G</string>
<key>name</key>
<string></string>
</dict>
<key>versionNumber</key>
<string>1.0.0</string>
</dict>
</array>
</dict>
</plist>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>generateAppStoreInformation</key>
<false/>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.juraldinio.wallet</key>
<string>MalinkaWallet</string>
</dict>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>GENPCTDS3G</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "KeyChainAccess",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "KeyChainAccess",
targets: ["KeyChainAccess"]),
],
dependencies: [
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "KeyChainAccess",
dependencies: [],
path: "./Sources"),
]
)
File diff suppressed because it is too large Load Diff
-28
View File
@@ -1,28 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Mocker",
platforms: [
.macOS(.v10_15),
.iOS(.v11),
.tvOS(.v12),
.watchOS(.v6)],
products: [
.library(name: "Mocker", targets: ["Mocker"])
],
targets: [
.target(
name: "Mocker"
),
.testTarget(
name: "MockerTests",
dependencies: ["Mocker"],
resources: [
.process("Resources")
]
)
],
swiftLanguageVersions: [.v5])
-400
View File
@@ -1,400 +0,0 @@
<p align="center">
<img width="900px" src="Assets/artwork.jpg">
</p>
<p align="center">
<img src="https://api.travis-ci.org/WeTransfer/Mocker.svg?branch=master"/>
<img src="https://img.shields.io/cocoapods/v/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/cocoapods/l/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/cocoapods/p/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/badge/language-swift4.2-f48041.svg?style=flat"/>
<img src="https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat"/>
<img src="https://img.shields.io/badge/spm-compatible-4BC51D.svg?style=flat"/>
<img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat"/>
</p>
Mocker is a library written in Swift which makes it possible to mock data requests using a custom `URLProtocol`.
- [Features](#features)
- [Requirements](#requirements)
- [Usage](#usage)
- [Activating the Mocker](#activating-the-mocker)
- [Custom URLSessions](#custom-urlsessions)
- [Alamofire](#alamofire)
- [Register Mocks](#register-mocks)
- [Create your mocked data](#create-your-mocked-data)
- [JSON Requests](#json-requests)
- [File extensions](#file-extensions)
- [Custom HEAD and GET response](#custom-head-and-get-response)
- [Delayed responses](#delayed-responses)
- [Redirect responses](#redirect-responses)
- [Ignoring URLs](#ignoring-urls)
- [Mock callbacks](#mock-callbacks)
- [Unregister Mocks](#unregister-mocks)
- [Clear all registered mocks](#clear-all-registered-mocks)
- [Communication](#communication)
- [Installation](#installation)
- [Release Notes](#release-notes)
- [License](#license)
## Features
_Run all your data request unit tests offline_ 🎉
- [x] Create mocked data requests based on an URL
- [x] Create mocked data requests based on a file extension
- [x] Works with `URLSession` using a custom protocol class
- [x] Supports popular frameworks like `Alamofire`
## Usage
Unit tests are written for the `Mocker` which can help you to see how it works.
### Activating the Mocker
The mocker will automatically be activated for the default URL loading system like `URLSession.shared` after you've registered your first `Mock`.
##### Custom URLSessions
To make it work with your custom `URLSession`, the `MockingURLProtocol` needs to be registered:
```swift
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
```
##### Alamofire
Quite similar like registering on a custom `URLSession`.
```swift
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let sessionManager = Alamofire.Session(configuration: configuration)
```
### Register Mocks
##### Create your mocked data
It's recommended to create a class with all your mocked data accessible. An example of this can be found in the unit tests of this project:
```swift
public final class MockedData {
public static let botAvatarImageResponseHead: Data = try! Data(contentsOf: Bundle(for: MockedData.self).url(forResource: "Resources/Responses/bot-avatar-image-head", withExtension: "data")!)
public static let botAvatarImageFileUrl: URL = Bundle(for: MockedData.self).url(forResource: "wetransfer_bot_avater", withExtension: "png")!
public static let exampleJSON: URL = Bundle(for: MockedData.self).url(forResource: "Resources/JSON Files/example", withExtension: "json")!
}
```
##### JSON Requests
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
let mock = Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
return
}
// jsonDictionary contains your JSON sample file data
// ..
}.resume()
```
##### Empty Responses
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
var request = URLRequest(url: originalURL)
request.httpMethod = "PUT"
let mock = Mock(request: request, statusCode: 204)
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
// ....
}.resume()
```
##### Ignoring the query
Some URLs like authentication URLs contain timestamps or UUIDs in the query. To mock these you can ignore the Query for a certain URL:
``` swift
/// Would transform to "https://www.example.com/api/authentication" for example.
let originalURL = URL(string: "https://www.example.com/api/authentication?oauth_timestamp=151817037")!
let mock = Mock(url: originalURL, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
return
}
// jsonDictionary contains your JSON sample file data
// ..
}.resume()
```
##### File extensions
```swift
let imageURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.botAvatarImageFileUrl)
]).register()
URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
let botAvatarImage: UIImage = UIImage(data: data!)! // This is the image from your resources.
}.resume()
```
##### Custom HEAD and GET response
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
]).register()
URLSession.shared.dataTask(with: exampleURL) { (data, response, error) in
// data is your mocked data
}.resume()
```
##### Custom DataType
In addition to the already build in static `DataType` implementations it is possible to create custom ones that will be used as the value to the `Content-Type` header key.
```swift
let xmlURL = URL(string: "https://www.wetransfer.com/sample-xml.xml")!
Mock(fileExtensions: "png", contentType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.sampleXML)
]).register()
URLSession.shared.dataTask(with: xmlURL) { (data, response, error) in
let sampleXML: Data = data // This is the xml from your resources.
}.resume(
```
##### Delayed responses
Sometimes you want to test if the cancellation of requests is working. In that case, the mocked request should not finish immediately and you need a delay. This can be added easily:
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
var mock = Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
])
mock.delay = DispatchTimeInterval.seconds(5)
mock.register()
```
##### Redirect responses
Sometimes you want to mock short URLs or other redirect URLs. This is possible by saving the response and mocking the redirect location, which can be found inside the response:
```
Date: Tue, 10 Oct 2017 07:28:33 GMT
Location: https://wetransfer.com/redirect
```
By creating a mock for the short URL and the redirect URL, you can mock redirect and test this behavior:
```swift
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
```
##### Ignoring URLs
As the Mocker catches all URLs by default when registered, you might end up with a `fatalError` thrown in cases you don't need a mocked request. In that case, you can ignore the URL:
```swift
let ignoredURL = URL(string: "www.wetransfer.com")!
Mocker.ignore(ignoredURL)
```
However, if you need the Mocker to catch only mocked URLs and ignore every other URL, you can set the `mode` attribute to `.optin`.
```swift
Mocker.mode = .optin
```
If you want to set the original mode back, you have just to set it to `.optout`.
```swift
Mocker.mode = .optout
```
##### Mock errors
You can request a `Mock` to return an error, allowing testing of error handling.
```swift
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()],
requestError: TestExampleError.example).register()
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
XCTAssertNil(data)
XCTAssertNil(urlresponse)
XCTAssertNotNil(err)
if let err = err {
// there's not a particularly elegant way to verify an instance
// of an error, but this is a convenient workaround for testing
// purposes
XCTAssertEqual("example", String(describing: err))
}
expectation.fulfill()
}.resume()
```
##### Mock callbacks
You can register on `Mock` callbacks to make testing easier.
```swift
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [[String:String]].self, callback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments)
onRequestExpectation.fulfill()
})
mock.completion = {
endpointIsCalledExpectation.fulfill()
}
mock.register()
```
##### Mock expectations
Instead of setting the `completion` and `onRequest` you can also make use of expectations:
```swift
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let requestExpectation = expectationForRequestingMock(&mock)
let completionExpectation = expectationForCompletingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
```
### Unregister Mocks
##### Clear all registered mocks
You can clear all registered mocks:
```swift
Mocker.removeAll()
```
## Communication
- If you **found a bug**, open an issue.
- If you **have a feature request**, open an issue.
- If you **want to contribute**, submit a pull request.
## Installation
### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
```bash
$ brew update
$ brew install carthage
```
To integrate Mocker into your Xcode project using Carthage, specify it in your `Cartfile`:
```ogdl
github "WeTransfer/Mocker" ~> 2.3.0
```
Run `carthage update` to build the framework and drag the built `Mocker.framework` into your Xcode project.
### Swift Package Manager
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. Its integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
#### Manifest File
Add Mocker as a package to your `Package.swift` file and then specify it as a dependency of the Target in which you wish to use it.
```swift
import PackageDescription
let package = Package(
name: "MyProject",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.3.0"))
],
targets: [
.target(
name: "MyProject",
dependencies: ["Mocker"]),
.testTarget(
name: "MyProjectTests",
dependencies: ["MyProject"]),
]
)
```
#### Xcode
To add Mocker as a [dependency](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) to your Xcode project, select *File > Swift Packages > Add Package Dependency* and enter the repository URL.
#### Resolving Build Errors
If you get the following error: *cannot find auto-link library XCTest and XCTestSwiftSupport*, set the following property under Build Options from No to Yes.
ENABLE_TESTING_SEARCH_PATHS to YES
### Manually
If you prefer not to use any of the aforementioned dependency managers, you can integrate Mocker into your project manually.
#### Embedded Framework
- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
```bash
$ git init
```
- Add Mocker as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:
```bash
$ git submodule add https://github.com/WeTransfer/Mocker.git
```
- Open the new `Mocker ` folder, and drag the `Mocker.xcodeproj` into the Project Navigator of your application's Xcode project.
> It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
- Select the `Mocker.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
- In the tab bar at the top of that window, open the "General" panel.
- Click on the `+` button under the "Embedded Binaries" section.
- Select `Mocker.framework`.
- And that's it!
> The `Mocker.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
---
## Release Notes
See [CHANGELOG.md](https://github.com/WeTransfer/Mocker/blob/master/Changelog.md) for a list of changes.
## License
Mocker is available under the MIT license. See the LICENSE file for more info.
@@ -1,35 +0,0 @@
//
// Mock+DataType.swift
// Mocker
//
// Created by Weiß, Alexander on 26.07.22.
// Copyright © 2022 WeTransfer. All rights reserved.
//
import Foundation
extension Mock {
/// The types of content of a request. Will be used as Content-Type header inside a `Mock`.
public struct DataType {
/// Name of the data type.
public let name: String
/// The header value of the data type.
public let headerValue: String
public init(name: String, headerValue: String) {
self.name = name
self.headerValue = headerValue
}
}
}
extension Mock.DataType {
public static let json = Mock.DataType(name: "json", headerValue: "application/json; charset=utf-8")
public static let html = Mock.DataType(name: "html", headerValue: "text/html; charset=utf-8")
public static let imagePNG = Mock.DataType(name: "imagePNG", headerValue: "image/png")
public static let pdf = Mock.DataType(name: "pdf", headerValue: "application/pdf")
public static let mp4 = Mock.DataType(name: "mp4", headerValue: "video/mp4")
public static let zip = Mock.DataType(name: "zip", headerValue: "application/zip")
}
@@ -1,341 +0,0 @@
//
// Mock.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
// Mocker is only used for tests. In tests we don't even check on this SwiftLint warning, but Mocker is available through Rabbit for usage out of Rabbit. Disable for this case.
// swiftlint:disable force_unwrapping
import Foundation
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A Mock which can be used for mocking data requests with the `Mocker` by calling `Mocker.register(...)`.
public struct Mock: Equatable {
/// HTTP method definitions.
///
/// See https://tools.ietf.org/html/rfc7231#section-4.3
public enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
public typealias OnRequest = (_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void
/// The type of the data which designates the Content-Type header.
@available(*, deprecated, message: "Calling this property is unsafe after migrating to the `contentType` initializers, and will be removed in an upcoming release. Use `contentType` instead.")
public var dataType: DataType {
return contentType!
}
/// The type of the data which designates the Content-Type header. If set to `nil`, no Content-Type header is added to the headers.
public let contentType: DataType?
/// If set, the error that URLProtocol will report as a result rather than returning data from the mock
public let requestError: Error?
/// The headers to send back with the response.
public let headers: [String: String]
/// The HTTP status code to return with the response.
public let statusCode: Int
/// The URL value generated based on the Mock data. Force unwrapped on purpose. If you access this URL while it's not set, this is a programming error.
public var url: URL {
if urlToMock == nil && !data.keys.contains(.get) {
assertionFailure("For non GET mocks you should use the `request` property so the HTTP method is set.")
}
return urlToMock ?? generatedURL
}
/// The URL to mock as set implicitely from the init.
private let urlToMock: URL?
/// The URL generated from all the data set on this mock.
private let generatedURL: URL
/// The `URLRequest` to use if you did not set a specific URL.
public let request: URLRequest
/// If `true`, checking the URL will ignore the query and match only for the scheme, host and path.
public let ignoreQuery: Bool
/// The file extensions to match for.
public let fileExtensions: [String]?
/// The data which will be returned as the response based on the HTTP Method.
private let data: [HTTPMethod: Data]
/// Add a delay to a certain mock, which makes the response returned later.
public var delay: DispatchTimeInterval?
/// Allow response cache.
public var cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed
/// The callback which will be executed everytime this `Mock` was completed. Can be used within unit tests for validating that a request has been executed. The callback must be set before calling `register`.
public var completion: (() -> Void)?
/// The callback which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The callback must be set before calling `register`.
@available(*, deprecated, message: "Use `onRequestHandler` instead.")
public var onRequest: OnRequest? {
set {
onRequestHandler = OnRequestHandler(legacyCallback: newValue)
}
get {
onRequestHandler?.legacyCallback
}
}
/// The on request handler which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The handler must be set before calling `register`.
public var onRequestHandler: OnRequestHandler?
/// Can only be set internally as it's used by the `expectationForRequestingMock(_:)` method.
var onRequestExpectation: XCTestExpectation?
/// Can only be set internally as it's used by the `expectationForCompletingMock(_:)` method.
var onCompletedExpectation: XCTestExpectation?
private init(url: URL? = nil, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], requestError: Error? = nil, additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
guard data.count > 0 else {
preconditionFailure("At least one entry is required in the data dictionary")
}
self.urlToMock = url
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(contentType?.name ?? "no-content")/\(statusCode)/\(data.keys.first!.rawValue)")!
self.generatedURL = generatedURL
var request = URLRequest(url: url ?? generatedURL)
request.httpMethod = data.keys.first!.rawValue
self.request = request
self.ignoreQuery = ignoreQuery
self.requestError = requestError
self.contentType = contentType
self.statusCode = statusCode
self.data = data
self.cacheStoragePolicy = cacheStoragePolicy
var headers = additionalHeaders
if let contentType = contentType {
headers["Content-Type"] = contentType.headerValue
}
self.headers = headers
self.fileExtensions = fileExtensions?.map({ $0.replacingOccurrences(of: ".", with: "") })
}
/// Creates a `Mock` for the given data type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(contentType:statusCode:data:additionalHeaders:)")
public init(dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given content type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(contentType: DataType?, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given URL.
///
/// - Parameters:
/// - url: The URL to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
@available(*, deprecated, renamed: "init(url:ignoreQuery:cacheStoragePolicy:contentType:statusCode:data:additionalHeaders:requestError:)")
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: dataType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given URL.
///
/// - Parameters:
/// - url: The URL to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(fileExtensions:contentType:statusCode:data:additionalHeaders:)")
public init(fileExtensions: String..., dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(fileExtensions: String..., contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}
/// Creates a `Mock` for the given `URLRequest`.
///
/// - Parameters:
/// - request: The URLRequest, from which the URL and request method is used to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response. Defaults to an empty `Data` instance.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(request: URLRequest, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: Data = Data(), additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
preconditionFailure("Unexpected http method")
}
self.init(
url: request.url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: [requestHTTPMethod: data],
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Registers the mock with the shared `Mocker`.
public func register() {
Mocker.register(self)
}
/// Returns `Data` based on the HTTP Method of the passed request.
///
/// - Parameter request: The request to match data for.
/// - Returns: The `Data` which matches the request. Will be `nil` if no data is registered for the request `HTTPMethod`.
func data(for request: URLRequest) -> Data? {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return nil }
return data[requestHTTPMethod]
}
/// Used to compare the Mock data with the given `URLRequest`.
static func == (mock: Mock, request: URLRequest) -> Bool {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return false }
if let fileExtensions = mock.fileExtensions {
// If the mock contains a file extension, this should always be used to match for.
guard let pathExtension = request.url?.pathExtension else { return false }
return fileExtensions.contains(pathExtension)
} else if mock.ignoreQuery {
return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
}
return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
}
public static func == (lhs: Mock, rhs: Mock) -> Bool {
let lhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }
let rhsHTTPMethods: [String] = rhs.data.keys.compactMap { $0.rawValue }
if let lhsFileExtensions = lhs.fileExtensions, let rhsFileExtensions = rhs.fileExtensions, (!lhsFileExtensions.isEmpty || !rhsFileExtensions.isEmpty) {
/// The mocks are targeting file extensions specifically, check on those.
return lhsFileExtensions == rhsFileExtensions && lhsHTTPMethods == rhsHTTPMethods
}
return lhs.request.url!.absoluteString == rhs.request.url!.absoluteString && lhsHTTPMethods == rhsHTTPMethods
}
}
extension URL {
/// Returns the base URL string build with the scheme, host and path. "https://www.wetransfer.com/v1/test?param=test" would be "https://www.wetransfer.com/v1/test".
var baseString: String? {
guard let scheme = scheme, let host = host else { return nil }
return scheme + "://" + host + path
}
}
@@ -1,142 +0,0 @@
//
// Mocker.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// Can be used for registering Mocked data, returned by the `MockingURLProtocol`.
public struct Mocker {
private struct IgnoredRule: Equatable {
let urlToIgnore: URL
let ignoreQuery: Bool
/// Checks if the passed URL should be ignored.
///
/// - Parameter url: The URL to check for.
/// - Returns: `true` if it should be ignored, `false` if the URL doesn't correspond to ignored rules.
func shouldIgnore(_ url: URL) -> Bool {
if ignoreQuery {
return urlToIgnore.baseString == url.baseString
}
return urlToIgnore.absoluteString == url.absoluteString
}
}
public enum HTTPVersion: String {
case http1_0 = "HTTP/1.0"
case http1_1 = "HTTP/1.1"
case http2_0 = "HTTP/2.0"
}
/// The way Mocker handles unregistered urls
public enum Mode {
/// The default mode: only URLs registered with the `ignore(_ url: URL)` method are ignored for mocking.
///
/// - Registered mocked URL: Mocked.
/// - Registered ignored URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
/// - Any other URL: Raises an error.
case optout
/// Only registered mocked URLs are mocked, all others pass through.
///
/// - Registered mocked URL: Mocked.
/// - Any other URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
case optin
}
/// The mode defines how unknown URLs are handled. Defaults to `optout` which means requests without a mock will fail.
public static var mode: Mode = .optout
/// The shared instance of the Mocker, can be used to register and return mocks.
internal static var shared = Mocker()
/// The HTTP Version to use in the mocked response.
public static var httpVersion: HTTPVersion = HTTPVersion.http1_1
/// The registrated mocks.
private(set) var mocks: [Mock] = []
/// URLs to ignore for mocking.
public var ignoredURLs: [URL] {
ignoredRules.map { $0.urlToIgnore }
}
private var ignoredRules: [IgnoredRule] = []
/// For Thread Safety access.
private let queue = DispatchQueue(label: "mocker.mocks.access.queue", attributes: .concurrent)
private init() {
// Whenever someone is requesting the Mocker, we want the URL protocol to be activated.
_ = URLProtocol.registerClass(MockingURLProtocol.self)
}
/// Register new Mocked data. If a mock for the same URL and HTTPMethod exists, it will be overwritten.
///
/// - Parameter mock: The Mock to be registered for future requests.
public static func register(_ mock: Mock) {
shared.queue.async(flags: .barrier) {
/// Delete the Mock if it was already registered.
shared.mocks.removeAll(where: { $0 == mock })
shared.mocks.append(mock)
}
}
/// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist.
///
/// - Parameter url: The URL to mock.
/// - Parameter ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
public static func ignore(_ url: URL, ignoreQuery: Bool = false) {
shared.queue.async(flags: .barrier) {
let rule = IgnoredRule(urlToIgnore: url, ignoreQuery: ignoreQuery)
shared.ignoredRules.append(rule)
}
}
/// Checks if the passed URL should be handled by the Mocker. If the URL is registered to be ignored, it will not handle the URL.
///
/// - Parameter url: The URL to check for.
/// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored.
public static func shouldHandle(_ request: URLRequest) -> Bool {
switch mode {
case .optout:
guard let url = request.url else { return false }
return shared.queue.sync {
!shared.ignoredRules.contains(where: { $0.shouldIgnore(url) })
}
case .optin:
return mock(for: request) != nil
}
}
/// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test.
public static func removeAll() {
shared.queue.sync(flags: .barrier) {
shared.mocks.removeAll()
shared.ignoredRules.removeAll()
}
}
/// Retrieve a Mock for the given request. Matches on `request.url` and `request.httpMethod`.
///
/// - Parameter request: The request to search for a mock.
/// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request.
static func mock(for request: URLRequest) -> Mock? {
shared.queue.sync {
/// First check for specific URLs
if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) {
return specificMock
}
/// Second, check for generic file extension Mocks
return shared.mocks.first(where: { $0 == request })
}
}
}
@@ -1,110 +0,0 @@
//
// MockingURLProtocol.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// The protocol which can be used to send Mocked data back. Use the `Mocker` to register `Mock` data
open class MockingURLProtocol: URLProtocol {
enum Error: Swift.Error, LocalizedError, CustomDebugStringConvertible {
case missingMockedData(url: String)
case explicitMockFailure(url: String)
var errorDescription: String? {
return debugDescription
}
var debugDescription: String {
switch self {
case .missingMockedData(let url):
return "Missing mock for URL: \(url)"
case .explicitMockFailure(url: let url):
return "Induced error for URL: \(url)"
}
}
}
private var responseWorkItem: DispatchWorkItem?
/// Returns Mocked data based on the mocks register in the `Mocker`. Will end up in an error when no Mock data is found for the request.
override public func startLoading() {
guard
let mock = Mocker.mock(for: request),
let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
let data = mock.data(for: request)
else {
print("\n\n 🚨 No mocked data found for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). Did you forget to use `register()`? 🚨 \n\n")
client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString)))
return
}
if let onRequestHandler = mock.onRequestHandler {
onRequestHandler.handleRequest(request)
}
mock.onRequestExpectation?.fulfill()
guard let delay = mock.delay else {
finishRequest(for: mock, data: data, response: response)
return
}
self.responseWorkItem = DispatchWorkItem(block: { [weak self] in
guard let self = self else { return }
self.finishRequest(for: mock, data: data, response: response)
})
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).asyncAfter(deadline: .now() + delay, execute: responseWorkItem!)
}
private func finishRequest(for mock: Mock, data: Data, response: HTTPURLResponse) {
if let redirectLocation = data.redirectLocation {
self.client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: redirectLocation), redirectResponse: response)
} else if let requestError = mock.requestError {
self.client?.urlProtocol(self, didFailWithError: requestError)
} else {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: mock.cacheStoragePolicy)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
}
mock.completion?()
mock.onCompletedExpectation?.fulfill()
}
/// Implementation does nothing, but is needed for a valid inheritance of URLProtocol.
override public func stopLoading() {
responseWorkItem?.cancel()
}
/// Simply sends back the passed request. Implementation is needed for a valid inheritance of URLProtocol.
override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
/// Overrides needed to define a valid inheritance of URLProtocol.
override public class func canInit(with request: URLRequest) -> Bool {
return Mocker.shouldHandle(request)
}
}
private extension Data {
/// Returns the redirect location from the raw HTTP response if exists.
var redirectLocation: URL? {
let locationComponent = String(data: self, encoding: String.Encoding.utf8)?.components(separatedBy: "\n").first(where: { (value) -> Bool in
return value.contains("Location:")
})
guard let redirectLocationString = locationComponent?.components(separatedBy: "Location:").last, let redirectLocation = URL(string: redirectLocationString.trimmingCharacters(in: NSCharacterSet.whitespaces)) else {
return nil
}
return redirectLocation
}
}
@@ -1,129 +0,0 @@
//
// OnRequestHandler.swift
//
//
// Created by Antoine van der Lee on 03/11/2022.
// Copyright © 2022 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A handler for verifying outgoing requests.
public struct OnRequestHandler {
public typealias OnRequest<HTTPBody> = (_ request: URLRequest, _ httpBody: HTTPBody?) -> Void
private let internalCallback: (_ request: URLRequest) -> Void
let legacyCallback: Mock.OnRequest?
/// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable`.
/// - Parameters:
/// - httpBodyType: The decodable type to use for parsing the request body.
/// - callback: The callback which will be called just before the request executes.
public init<HTTPBody: Decodable>(httpBodyType: HTTPBody.Type?, callback: @escaping OnRequest<HTTPBody>) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let decodedObject = try? JSONDecoder().decode(HTTPBody.self, from: httpBody)
else {
callback(request, nil)
return
}
callback(request, decodedObject)
}
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request without parsing the body arguments.
/// - Parameter requestCallback: The callback which will be executed just before the request executes, containing the request.
public init(requestCallback: @escaping (_ request: URLRequest) -> Void) {
self.internalCallback = requestCallback
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request without parsing the body arguments and without passing the request.
/// - Parameter callback: The callback which will be executed just before the request executes.
public init(callback: @escaping () -> Void) {
self.internalCallback = { _ in
callback()
}
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Dictionary.
public init(jsonDictionaryCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void)) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
else {
jsonDictionaryCallback(request, nil)
return
}
jsonDictionaryCallback(request, jsonObject)
}
self.legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Array.
public init(jsonArrayCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [[String: Any]]?) -> Void)) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [[String: Any]]
else {
jsonArrayCallback(request, nil)
return
}
jsonArrayCallback(request, jsonObject)
}
self.legacyCallback = nil
}
init(legacyCallback: Mock.OnRequest?) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
else {
legacyCallback?(request, nil)
return
}
legacyCallback?(request, jsonObject)
}
self.legacyCallback = legacyCallback
}
func handleRequest(_ request: URLRequest) {
internalCallback(request)
}
}
private extension URLRequest {
/// We need to use the http body stream data as the URLRequest once launched converts the `httpBody` to this stream of data.
func httpBodyStreamData() -> Data? {
guard let bodyStream = self.httpBodyStream else { return nil }
bodyStream.open()
// Will read 16 chars per iteration. Can use bigger buffer if needed
let bufferSize: Int = 16
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
var data = Data()
while bodyStream.hasBytesAvailable {
let readData = bodyStream.read(buffer, maxLength: bufferSize)
data.append(buffer, count: readData)
}
buffer.deallocate()
bodyStream.close()
return data
}
}
@@ -1,24 +0,0 @@
//
// XCTest+Mocker.swift
// Mocker
//
// Created by Antoine van der Lee on 27/05/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//
import Foundation
import XCTest
public extension XCTestCase {
func expectationForRequestingMock(_ mock: inout Mock) -> XCTestExpectation {
let mockExpectation = expectation(description: "\(mock) should be requested")
mock.onRequestExpectation = mockExpectation
return mockExpectation
}
func expectationForCompletingMock(_ mock: inout Mock) -> XCTestExpectation {
let mockExpectation = expectation(description: "\(mock) should be finishing")
mock.onCompletedExpectation = mockExpectation
return mockExpectation
}
}
@@ -1,35 +0,0 @@
//
// MockTests.swift
//
//
// Created by Antoine van der Lee on 21/04/2021.
//
import Foundation
import XCTest
@testable import Mocker
final class MockTests: XCTestCase {
override func setUp() {
super.setUp()
Mocker.mode = .optout
}
override func tearDown() {
Mocker.removeAll()
Mocker.mode = .optout
super.tearDown()
}
/// It should match two file extension mocks correctly.
func testFileExtensionMocksComparing() {
let mock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let secondMock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let mock400 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [.put: Data()])
let mockJPEG = Mock(fileExtensions: "jpeg", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
XCTAssertEqual(mock200, secondMock200)
XCTAssertEqual(mock200, mock400)
XCTAssertNotEqual(mock200, mockJPEG)
}
}
@@ -1,29 +0,0 @@
//
// MockedData.swift
// Mocker
//
// Created by Antoine van der Lee on 11/08/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
/// Contains all available Mocked data.
public final class MockedData {
public static let botAvatarImageFileUrl: URL = Bundle.module.url(forResource: "wetransfer_bot_avatar", withExtension: "png")!
public static let exampleJSON: URL = Bundle.module.url(forResource: "example", withExtension: "json")!
public static let redirectGET: URL = Bundle.module.url(forResource: "sample-redirect-get", withExtension: "data")!
}
extension Bundle {
#if !SWIFT_PACKAGE
static let module = Bundle(for: MockedData.self)
#endif
}
internal extension URL {
/// Returns a `Data` representation of the current `URL`. Force unwrapping as it's only used for tests.
var data: Data {
return try! Data(contentsOf: self)
}
}
@@ -1,636 +0,0 @@
//
// MockerTests.swift
// MockerTests
//
// Created by Antoine van der Lee on 11/08/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@testable import Mocker
final class MockerTests: XCTestCase {
struct Framework {
let name: String?
let owner: String?
init(jsonDictionary: [String: Any]) {
name = jsonDictionary["name"] as? String
owner = jsonDictionary["owner"] as? String
}
}
override func setUp() {
super.setUp()
Mocker.mode = .optout
}
override func tearDown() {
Mocker.removeAll()
Mocker.mode = .optout
super.tearDown()
}
/// It should returned the register mocked image data as response.
func testImageURLDataRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://avatars3.githubusercontent.com/u/26250426?v=4&s=400")!
let mockedData = MockedData.botAvatarImageFileUrl.data
let mock = Mock(url: originalURL, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should returned the register mocked image data as response for register file types.
func testImageExtensionDataRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
URLSession.shared.dataTask(with: originalURL!) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should ignore file extension mocks if a specific URL is mocked.
func testSpecificURLOverGenericMocks() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [
.get: Data()
]).register()
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should correctly ignore queries if set.
func testIgnoreQueryMocking() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
/// Make it different compared to the mocked URL.
let customURL = URL(string: originalURL.absoluteString + "&" + UUID().uuidString)!
URLSession.shared.dataTask(with: customURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should return the mocked JSON.
func testJSONRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
.get: MockedData.exampleJSON.data
]).register()
URLSession.shared.dataTask(with: originalURL) { (data, _, _) in
guard let data = data else {
XCTFail("Data is nil")
return
}
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
XCTFail("Wrong data response \(String(describing: data))")
expectation.fulfill()
return
}
let framework = Framework(jsonDictionary: jsonDictionary)
XCTAssertEqual(framework.name, "Mocker")
XCTAssertEqual(framework.owner, "WeTransfer")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// No Content-Type should be included in the headers
func testNoContentType() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
var request = URLRequest(url: originalURL)
request.httpMethod = "PUT"
Mock(request: request, statusCode: 202).register()
URLSession.shared.dataTask(with: request) { (data, response, _) in
guard let response = response as? HTTPURLResponse else {
XCTFail("Unexpected response")
return
}
// data is only nil if there is an error
XCTAssertEqual(data, Data())
XCTAssertNil(response.allHeaderFields["Content-Type"])
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should return the additional headers.
func testAdditionalHeaders() {
let expectation = self.expectation(description: "Data request should succeed")
let headers = ["Testkey": "testvalue"]
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: headers)
mock.register()
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
XCTAssertNil(error)
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Testkey"] as? String), "testvalue", "Additional headers should be added.")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should override existing mocks.
func testMockOverriding() {
let expectation = self.expectation(description: "Data request should succeed")
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["testkey": "testvalue"])
mock.register()
let newMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["Newkey": "newvalue"])
newMock.register()
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
XCTAssertNil(error)
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Newkey"] as? String), "newvalue", "Additional headers should be added.")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should work with a custom URLSession.
func testCustomURLSession() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
urlSession.dataTask(with: originalURL!) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should be possible to test cancellation of requests with a delayed mock.
func testDelayedMockCancelation() {
let expectation = self.expectation(description: "Data request should be cancelled")
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.delay = DispatchTimeInterval.seconds(5)
mock.register()
let task = URLSession.shared.dataTask(with: mock.request) { (_, _, error) in
XCTAssertEqual(error?._code, NSURLErrorCancelled)
expectation.fulfill()
}
task.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
task.cancel()
})
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should correctly handle redirect responses.
func testRedirectResponse() throws {
#if os(Linux)
throw XCTSkip("The URLSession swift-corelibs-foundation implementation doesn't currently handle redirects directly")
#endif
let expectation = self.expectation(description: "Data request should be cancelled")
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: MockedData.redirectGET.data]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data]).register()
URLSession.shared.dataTask(with: urlWhichRedirects) { (data, _, _) in
guard let data = data else {
XCTFail("Data is nil")
return
}
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
XCTFail("Wrong data response \(String(describing: data))")
expectation.fulfill()
return
}
let framework = Framework(jsonDictionary: jsonDictionary)
XCTAssertEqual(framework.name, "Mocker")
XCTAssertEqual(framework.owner, "WeTransfer")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should be possible to ignore URLs and not let them be handled.
func testIgnoreURLs() {
let ignoredURL = URL(string: "www.wetransfer.com")!
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
Mocker.ignore(ignoredURL)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
}
/// It should be possible to ignore URLs and not let them be handled.
func testIgnoreURLsIgnoreQueries() {
let ignoredURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
let ignoredURLQueries = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
Mocker.ignore(ignoredURL, ignoreQuery: true)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
}
/// It should be possible to compose a url relative to a base and still have it match the full url
func testComposedURLMatch() {
let composedURL = URL(fileURLWithPath: "resource", relativeTo: URL(string: "https://host.com/api/"))
let simpleURL = URL(string: "https://host.com/api/resource")
let mock = Mock(url: composedURL, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data])
let urlRequest = URLRequest(url: simpleURL!)
XCTAssertEqual(composedURL.absoluteString, simpleURL?.absoluteString)
XCTAssert(mock == urlRequest)
}
/// It should call the onRequest and completion callbacks when a `Mock` is used and completed in the right order.
func testMockCallbacks() {
let onRequestExpectation = expectation(description: "Data request should start")
let completionExpectation = expectation(description: "Data request should succeed")
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.onRequest = { _, _ in
onRequestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
URLSession.shared.dataTask(with: mock.request).resume()
wait(for: [onRequestExpectation, completionExpectation], timeout: 2.0, enforceOrder: true)
}
/// It should report post body arguments if they exist.
func testOnRequestLegacyPostBodyParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = ["test": "value"]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequest = { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
onRequestExpectation.fulfill()
}
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestDecodablePostBodyParameters() throws {
struct RequestParameters: Codable, Equatable {
let name: String
}
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = RequestParameters(name: UUID().uuidString)
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONEncoder().encode(expectedParameters)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(httpBodyType: RequestParameters.self, callback: { request, postBodyDecodable in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyDecodable)
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestJSONDictionaryPostBodyParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = ["test": "value"]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(jsonDictionaryCallback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestCallbackWithoutRequestAndParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(callback: {
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
/// It should report post body arguments with top level collection type if they exist.
func testOnRequestPostBodyParametersWithTopLevelCollectionType() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = [["test": "value"], ["test": "value"]]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(jsonArrayCallback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [[String: String]])
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
/// It should call the mock after a delay.
func testDelayedMock() {
let nonDelayExpectation = expectation(description: "Data request should succeed")
let delayedExpectation = expectation(description: "Data request should succeed")
var delayedMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
delayedMock.delay = DispatchTimeInterval.seconds(1)
delayedMock.completion = {
delayedExpectation.fulfill()
}
delayedMock.register()
var nonDelayMock = Mock(contentType: .json, statusCode: 200, data: [.post: Data()])
nonDelayMock.completion = {
nonDelayExpectation.fulfill()
}
nonDelayMock.register()
XCTAssertNotEqual(delayedMock.request.url, nonDelayMock.request.url)
URLSession.shared.dataTask(with: delayedMock.request).resume()
URLSession.shared.dataTask(with: nonDelayMock.request).resume()
wait(for: [nonDelayExpectation, delayedExpectation], timeout: 2.0, enforceOrder: true)
}
/// It should remove all registered mocks correctly.
func testRemoveAll() {
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.register()
Mocker.removeAll()
XCTAssertTrue(Mocker.shared.mocks.isEmpty)
}
/// It should correctly add two mocks for the same URL if the HTTP method is different.
func testDifferentHTTPMethodSameURL() {
let url = URL(string: "https://www.fakeurl.com/\(UUID().uuidString)")!
Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()]).register()
Mock(url: url, contentType: .json, statusCode: 200, data: [.put: Data()]).register()
var request = URLRequest(url: url)
request.httpMethod = Mock.HTTPMethod.get.rawValue
XCTAssertNotNil(Mocker.mock(for: request))
request.httpMethod = Mock.HTTPMethod.put.rawValue
XCTAssertNotNil(Mocker.mock(for: request))
}
/// It should call the on request expectation.
func testOnRequestExpectation() {
let url = URL(string: "https://www.fakeurl.com")!
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let expectation = expectationForRequestingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [expectation], timeout: 2.0)
}
/// It should call the on completion expectation.
func testOnCompletionExpectation() {
let url = URL(string: "https://www.fakeurl.com")!
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let expectation = expectationForCompletingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [expectation], timeout: 2.0)
}
/// it should return the error we requested from the mock when we pass in an Error.
func testMockReturningError() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
enum TestExampleError: Error, LocalizedError {
case example
var errorDescription: String { "example" }
}
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()], requestError: TestExampleError.example).register()
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, error) in
XCTAssertNil(data)
XCTAssertNil(urlresponse)
XCTAssertNotNil(error)
if let error = error {
#if os(Linux)
XCTAssertEqual(error as? TestExampleError, .example)
#else
// there's not a particularly elegant way to verify an instance
// of an error, but this is a convenient workaround for testing
// purposes
XCTAssertTrue(String(describing: error).contains("TestExampleError"))
#endif
}
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should cache response
func testMockCachePolicy() throws {
#if os(Linux)
throw XCTSkip("URLSessionTask in swift-corelibs-foundation doesn't cache response for custom protocols")
#endif
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
Mock(url: originalURL, cacheStoragePolicy: .allowed,
contentType: .json, statusCode: 200,
data: [.get: MockedData.exampleJSON.data],
additionalHeaders: ["Cache-Control": "public, max-age=31557600, immutable"]
).register()
let configuration = URLSessionConfiguration.default
#if !os(Linux)
configuration.urlCache = URLCache()
#endif
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
urlSession.dataTask(with: originalURL) { (_, _, error) in
XCTAssertNil(error)
let cachedResponse = configuration.urlCache?.cachedResponse(for: URLRequest(url: originalURL))
XCTAssertNotNil(cachedResponse)
XCTAssertEqual(cachedResponse!.data, MockedData.exampleJSON.data)
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should process unknown URL
func testMockerOptoutMode() {
Mocker.mode = .optout
let mockedURL = URL(string: "www.google.com")!
let ignoredURL = URL(string: "www.wetransfer.com")!
let unknownURL = URL(string: "www.netflix.com")!
// Mocking
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
.register()
// Ignoring
Mocker.ignore(ignoredURL)
// Checking mocked URL are processed by Mocker
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
// Checking ignored URL are not processed by Mocker
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
// Checking unknown URL are processed by Mocker (.optout mode)
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
}
/// It should not process unknown URL
func testMockerOptinMode() {
Mocker.mode = .optin
let mockedURL = URL(string: "www.google.com")!
let ignoredURL = URL(string: "www.wetransfer.com")!
let unknownURL = URL(string: "www.netflix.com")!
// Mocking
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
.register()
// Ignoring
Mocker.ignore(ignoredURL)
// Checking mocked URL are processed by Mocker
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
// Checking ignored URL are not processed by Mocker
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
// Checking unknown URL are not processed by Mocker (.optin mode)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
}
/// Default mode should be .optout
func testDefaultMode() {
/// Checking default mode
XCTAssertEqual(.optout, Mocker.mode)
}
}
@@ -1,4 +0,0 @@
{
"name": "Mocker",
"owner": "WeTransfer"
}
@@ -1,14 +0,0 @@
HTTP/1.1 302 Moved Temporarily
Content-Type: text/html;charset=utf-8
Content-Length: 0
Cache-Control: public
Date: Tue, 10 Oct 2017 07:28:33 GMT
Location: https://wetransfer.com/redirect
Server: nginx/1.12.0
X-Content-Type-Options: nosniff
X-Request-Id: 8c43587ec891b2f1f72c61ecec2e96db
X-XSS-Protection: 1; mode=block
X-Cache: Miss from cloudfront
Via: 1.1 72f202fb973968c0cfdb028ab6f36fac.cloudfront.net (CloudFront)
X-Amz-Cf-Id: tU8eVZ9jWBJzd3aEB-4gyym_VxcPKskWFByEvXapy5WrdDkV-35-KA==
Connection: Keep-alive
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

-2
View File
@@ -1,2 +0,0 @@
* linguist-vendored
*.swift linguist-vendored=false

Some files were not shown because too many files have changed in this diff Show More