Skip to content

Commit

Permalink
Merge pull request #2 from tinkoff-mobile-tech/unit_tests
Browse files Browse the repository at this point in the history
Added unit tests
  • Loading branch information
ovrchk authored Jan 25, 2021
2 parents 4b27bfe + e36dad0 commit c418ed8
Show file tree
Hide file tree
Showing 46 changed files with 1,840 additions and 118 deletions.
48 changes: 15 additions & 33 deletions Development/Source/API/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ import Foundation

final class API: IAPI {

enum Error: Swift.Error {
case unknown
}

private lazy var session = URLSession.shared
private lazy var decoder = JSONDecoder()

private let requestBuilder: IRequestBuilder
let requestBuilder: IRequestBuilder
let requestProcessor: IURLRequestProcessor
let responseDispatcher: Dispatcher

init(requestBuilder: IRequestBuilder) {
init(requestBuilder: IRequestBuilder, requestProcessor: IURLRequestProcessor, responseDispatcher: Dispatcher) {
self.requestBuilder = requestBuilder
self.requestProcessor = requestProcessor
self.responseDispatcher = responseDispatcher
}

func obtainCredentials(with code: String,
Expand Down Expand Up @@ -56,34 +55,17 @@ final class API: IAPI {
do {
let request = try requestClosure()

session.dataTask(with: request) { data, response, error in
self.handleResponse(data, response, error, completion)
}.resume()
requestProcessor.process(request) { response in
let result = Result {
try self.decoder.decode(T.self, from: try response.get())
}

self.responseDispatcher.dispatch {
completion(result)
}
}
} catch {
completion(.failure(error))
}
}

private func handleResponse<T:Decodable>(_ data: Data?,
_ response: URLResponse?,
_ error: Swift.Error?,
_ completion: @escaping (Result<T, Swift.Error>) -> Void) {
guard let data = data else {
return finish(completion, with: .failure(Error.unknown))
}

do {
let credentials = try decoder.decode(T.self, from: data)

finish(completion, with: .success(credentials))
} catch {
finish(completion, with: .failure(error))
}
}

private func finish<T>(_ completion: @escaping (Result<T, Swift.Error>) -> Void, with result: Result<T, Swift.Error>) {
DispatchQueue.main.async {
completion(result)
}
}
}
20 changes: 20 additions & 0 deletions Development/Source/API/Dispatcher/Dispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Dispatcher.swift
// TinkoffID
//
// Created by Dmitry on 19.01.2021.
//

import Foundation

/// Объект, умеющий выполнять определенные блоки кода
protocol Dispatcher {
/// Выполняет блок кода
func dispatch(_ block: @escaping () -> Void)
}

extension DispatchQueue: Dispatcher {
func dispatch(_ block: @escaping () -> Void) {
async(execute: block)
}
}
6 changes: 4 additions & 2 deletions Development/Source/API/RequestBuilder/RequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ final class RequestBuilder: IRequestBuilder {
_ clientId: String,
_ codeVerifier: String,
_ redirectUri: String) throws -> URLRequest {
return try buildTokenRequest(clientId: clientId, grantType: .grantTypeAuthorizationCode, bodyParams: [
try buildTokenRequest(clientId: clientId, grantType: .grantTypeAuthorizationCode, bodyParams: [
.code: code,
.codeVerifier: codeVerifier,
.redirectUri: redirectUri
])
}

func buildTokenRequest(with refreshToken: String, clientId: String) throws -> URLRequest {
return try buildTokenRequest(clientId: clientId, grantType: .grantTypeRefreshToken, bodyParams: [
try buildTokenRequest(clientId: clientId, grantType: .grantTypeRefreshToken, bodyParams: [
.refreshToken : refreshToken
])
}
Expand All @@ -78,6 +78,7 @@ final class RequestBuilder: IRequestBuilder {
}

// MARK: - Private

private func getAuthorizationHeaderValue(for clientId: String) -> String {
return String(format: .authorizationHeaderFormat, Data((clientId + ":").utf8).base64EncodedString())
}
Expand Down Expand Up @@ -117,6 +118,7 @@ final class RequestBuilder: IRequestBuilder {
private extension Dictionary where Key == RequestBuilder.Param, Value == String {
var httpBody: Data? {
map { $0.key.rawValue + "=" + $0.value }
.sorted()
.joined(separator: "&")
.data(using: .utf8)
}
Expand Down
35 changes: 35 additions & 0 deletions Development/Source/API/RequestProcessor/IURLRequestProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// IURLRequestProcessor.swift
// TinkoffID
//
// Created by Dmitry on 19.01.2021.
//

import Foundation

/// Объект, выполняющий сетевые запросы
protocol IURLRequestProcessor {

/// Выполняет сетевой запрос
/// - Parameters:
/// - request: Запрос
/// - completion: Коллбек с результатом выполнения
func process(_ request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void)
}

extension URLSession: IURLRequestProcessor {

enum Error: Swift.Error {
case unknown
}

func process(_ request: URLRequest, completion: @escaping (Result<Data, Swift.Error>) -> Void) {
dataTask(with: request) { data, response, error in
guard let data = data else {
return completion(.failure(error ?? Error.unknown))
}

completion(.success(data))
}.resume()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// URLSchemeAppLauncher.swift
// URLSchemeAppLauncherTests.swift
// TinkoffID
//
// Created by Dmitry Overchuk on 25.03.2020.
Expand All @@ -13,45 +13,27 @@ final class URLSchemeAppLauncher: IAppLauncher {
case launchFailure
}

private let app = UIApplication.shared
private let builder: IURLSchemeBuilder
private let appUrlScheme: String
let builder: IURLSchemeBuilder
let appUrlScheme: String
let router: IURLRouter

init(builder: IURLSchemeBuilder, appUrlScheme: String) {
init(appUrlScheme: String, builder: IURLSchemeBuilder, router: IURLRouter) {
self.builder = builder
self.appUrlScheme = appUrlScheme
self.router = router
}

var canLaunchApp: Bool {
app.canOpenURL(
URL(string: appUrlScheme)
)
URL(string: appUrlScheme)
.map(router.canOpenURL) ?? false
}

func launchApp(with options: AppLaunchOptions) throws {
let appUrl = try builder.buildUrlScheme(with: options)

if app.canOpenURL(appUrl) {
// Открывается приложение
app.open(appUrl)
} else {
if !router.open(appUrl) {
// Не удалось запустить приложение
throw Error.launchFailure
}
}
}

private extension UIApplication {

func canOpenURL(_ url: URL?) -> Bool {
guard let url = url else { return false }

return canOpenURL(url)
}

func open(_ url: URL?) {
guard let url = url else { return }

open(url, options: [:], completionHandler: nil)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// IURLRouter.swift
// TinkoffID
//
// Created by Dmitry on 20.01.2021.
//

import Foundation

/// Роутер URL
protocol IURLRouter {
/// Возвращает `true` если заданный URL может быть открыт
func canOpenURL(_ url: URL) -> Bool

/// Открывает заданный URL и возвращает `true` если открытие удалось
func open(_ url: URL) -> Bool
}

extension UIApplication: IURLRouter {
func open(_ url: URL) -> Bool {
guard canOpenURL(url) else { return false }

open(url, options: [:], completionHandler: nil)

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Опции запуска приложения
struct AppLaunchOptions {
struct AppLaunchOptions: Equatable {
/// Идентификатор авторизуемого клиента
let clientId: String
/// URL обратного вызова по которому будет осуществлен переход обратно в приложение
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Результат обработки URL
enum CallbackURLParseResult {
enum CallbackURLParseResult: Equatable {
/// Удалось извлечь код
case codeObtained(String)
/// Удалось извлечь флаг об отмене процесса пользователем
Expand Down
2 changes: 1 addition & 1 deletion Development/Source/Models/SignOutTokenTypeHint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

enum SignOutTokenTypeHint: String {
public enum SignOutTokenTypeHint: String {
case access = "access_token"
case refresh = "refresh_token"
}
2 changes: 1 addition & 1 deletion Development/Source/Models/TinkoffTokenPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Авторизационные данные
public struct TinkoffTokenPayload {
public struct TinkoffTokenPayload: Equatable {
/// Токен для обращения к API Тинькофф
public let accessToken: String
/// Токен, необходимый для получения нового `accessToken`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ import Foundation
protocol IPKCEPayloadGenerator {

/// Создает и возвращает новый `PKCECodePayload`
func generatePayload() -> PKCECodePayload
func generatePayload() throws -> PKCECodePayload
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
protocol IPKCECodeChallengeDerivator {

/// Выводит `code challenge `используя заданый `code verifier`
func deriveCodeChallenge(using codeVerifier: String) -> String
func deriveCodeChallenge(using codeVerifier: String) throws -> String

/// Возвращает метод получения `code challenge` (например `plain`, `S256`, и т.д..)
var codeChallengeMethod: String { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import CommonCrypto
final class RFC7636PKCECodeChallengeDerivator: IPKCECodeChallengeDerivator {
var codeChallengeMethod = "S256"

func deriveCodeChallenge(using codeVerifier: String) -> String {
enum Error: Swift.Error {
case serializationError
}

func deriveCodeChallenge(using codeVerifier: String) throws -> String {
guard let codeVerifierBytes = codeVerifier.data(using: .ascii) else {
assertionFailure("Failed to serialize codeVerifier")
return String()
throw Error.serializationError
}

var buffer = [UInt8](repeating: .zero, count: Int(CC_SHA256_DIGEST_LENGTH))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ import Foundation

final class PKCEPayloadGenerator: IPKCEPayloadGenerator {

private let codeVerifierGenerator: IPKCECodeVerifierGenerator
private let codeChallengeGenerator: IPKCECodeChallengeDerivator
let codeVerifierGenerator: IPKCECodeVerifierGenerator
let codeChallengeDerivator: IPKCECodeChallengeDerivator

init(codeVerifierGenerator: IPKCECodeVerifierGenerator,
codeChallengeGenerator: IPKCECodeChallengeDerivator) {
codeChallengeDerivator: IPKCECodeChallengeDerivator) {
self.codeVerifierGenerator = codeVerifierGenerator
self.codeChallengeGenerator = codeChallengeGenerator
self.codeChallengeDerivator = codeChallengeDerivator
}

func generatePayload() -> PKCECodePayload {
func generatePayload() throws -> PKCECodePayload {
let codeVerifier = codeVerifierGenerator.generateCodeVerifier()
let codeChallenge = codeChallengeGenerator.deriveCodeChallenge(using: codeVerifier)
let codeChallenge = try codeChallengeDerivator.deriveCodeChallenge(using: codeVerifier)

return PKCECodePayload(verifier: codeVerifier,
challenge: codeChallenge,
challengeMethod: codeChallengeGenerator.codeChallengeMethod)
challengeMethod: codeChallengeDerivator.codeChallengeMethod)
}
}
2 changes: 1 addition & 1 deletion Development/Source/PKCE/PKCECodePayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Набор PKCE параметров в соответствии с https://tools.ietf.org/html/rfc7636#page-8
struct PKCECodePayload {
struct PKCECodePayload: Equatable {
let verifier: String
let challenge: String
let challengeMethod: String
Expand Down
11 changes: 7 additions & 4 deletions Development/Source/SDK/Builder/TinkoffIDBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ public final class TinkoffIDBuilder {

public func build() -> ITinkoffID {
let urlSchemeBuilder = URLSchemeBuilder(baseUrlString: app.authUrl)
let appLauncher = URLSchemeAppLauncher(builder: urlSchemeBuilder,
appUrlScheme: app.urlScheme)
let appLauncher = URLSchemeAppLauncher(appUrlScheme: app.urlScheme,
builder: urlSchemeBuilder,
router: UIApplication.shared)

let requestBuilder = RequestBuilder(baseUrl: app.apiBaseUrl)
let api = API(requestBuilder: requestBuilder)
let api = API(requestBuilder: requestBuilder,
requestProcessor: URLSession.shared,
responseDispatcher: DispatchQueue.main)

let codeVerifierGenerator = RFC7636PKCECodeVerifierGenerator()
let codeChallengeDerivator = RFC7636PKCECodeChallengeDerivator()

let payloadGenerator = PKCEPayloadGenerator(codeVerifierGenerator: codeVerifierGenerator,
codeChallengeGenerator: codeChallengeDerivator)
codeChallengeDerivator: codeChallengeDerivator)

let callbackUrlParser = CallbackURLParser()

Expand Down
Loading

0 comments on commit c418ed8

Please sign in to comment.