Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SPT-1998 Модульные тесты часть 1 #124

Merged
merged 10 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ jobs:
runs-on: macos-14

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Init
run: |
make init
- name: Force select xcode 15
- name: Force select xcode 15.3
run: |
sudo xcode-select -switch /Applications/Xcode_15.2.app
sudo xcode-select -switch /Applications/Xcode_15.3.app
- name: Build
run: |
make build
Expand All @@ -33,7 +35,14 @@ jobs:
run: |
make test
- name: Upload coverage to Codecov
uses: codecov/[email protected]
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
flags: tests
directory: ./CoverageReports
file: ./coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- name: documentation
if: github.ref == 'refs/heads/master'
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
build/
DerivedData/
Docs/swift_output/
CoverageReports/

## Various settings
*.pbxuser
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ spm_build:
swift package clean
swift build --sdk "`xcrun -sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios17.4-simulator" -Xswiftc "-lswiftUIKit"

## Run tests
## Run tests and create coverage report
test:
xcodebuild test -scheme NodeKit -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -enableCodeCoverage YES -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.4' | bundle exec xcpretty -c
rm -rf DerivedData
mkdir CoverageReports
xcodebuild test -scheme NodeKit -derivedDataPath DerivedData -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -enableCodeCoverage YES -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.4' | bundle exec xcpretty -c
./xcresultparser/xcresultparser --output-format cobertura DerivedData/Logs/Test/*.xcresult > ./CoverageReports/coverage.xml

## Created documentation by comments from code
doc:
Expand Down
170 changes: 127 additions & 43 deletions NodeKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion NodeKit/CacheNode/ETag/UrlETagSaverNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public extension URL {
///
/// **ВАЖНО**
///
/// Полученная строка нможет быть невалидным URL - т.к. задача этого метода - получить уникальный идентификатор из URL
/// Полученная строка может быть невалидным URL - т.к. задача этого метода - получить уникальный идентификатор из URL
/// Причем так, чтобы порядок перечисления query парамтеров был не важен.
func withOrderedQuery() -> String? {
guard var comp = URLComponents(string: self.absoluteString) else {
Expand Down
8 changes: 4 additions & 4 deletions NodeKit/Chains/UrlChainsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ open class UrlChainsBuilder<Route: UrlRouteProvider> {

let urlRequestEncodingNode = UrlJsonRequestEncodingNode(next: transportChain)
let urlRequestTrasformatorNode = UrlRequestTrasformatorNode(next: urlRequestEncodingNode, method: self.method)
let requstEncoderNode = RequstEncoderNode(next: urlRequestTrasformatorNode, encoding: self.encoding)
let requestEncoderNode = RequestEncoderNode(next: urlRequestTrasformatorNode, encoding: self.encoding)

let queryInjector = URLQueryInjectorNode(next: requstEncoderNode, config: self.urlQueryConfig)
let queryInjector = URLQueryInjectorNode(next: requestEncoderNode, config: self.urlQueryConfig)

let requestRouterNode = self.requestRouterNode(next: queryInjector)

Expand Down Expand Up @@ -256,7 +256,7 @@ open class UrlChainsBuilder<Route: UrlRouteProvider> {

let encoding = UrlJsonRequestEncodingNode(next: creator)
let tranformator = UrlRequestTrasformatorNode(next: encoding, method: self.method)
let encoder = RequstEncoderNode(next: tranformator, encoding: self.encoding)
let encoder = RequestEncoderNode(next: tranformator, encoding: self.encoding)

let queryInjector = URLQueryInjectorNode(next: encoder, config: self.urlQueryConfig)

Expand Down Expand Up @@ -284,7 +284,7 @@ open class UrlChainsBuilder<Route: UrlRouteProvider> {

let encoding = UrlJsonRequestEncodingNode(next: creator)
let tranformator = UrlRequestTrasformatorNode(next: encoding, method: self.method)
let encoder = RequstEncoderNode(next: tranformator, encoding: self.encoding)
let encoder = RequestEncoderNode(next: tranformator, encoding: self.encoding)

let queryInjector = URLQueryInjectorNode(next: encoder, config: self.urlQueryConfig)

Expand Down
6 changes: 3 additions & 3 deletions NodeKit/Encodings/UrlJsonRequestEncodingNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ open class UrlJsonRequestEncodingNode<Type>: AsyncNode {
log.message += "type: Json"
} catch {
log += "But can't encode data -> terminate with error"
return Context<Type>().log(log).emit(error: RequestEncodingError.unsupportedDataType)
return Context<Type>().log(log).emit(error: RequestEncodingNodeError.unsupportedDataType)
}

guard let unwrappedRequest = request else {
log += "Unsupported data type -> terminate with error"
return Context<Type>().log(log).emit(error: RequestEncodingError.unsupportedDataType)
return Context<Type>().log(log).emit(error: RequestEncodingNodeError.unsupportedDataType)
}

return next.processLegacy(unwrappedRequest).log(log)
Expand All @@ -69,7 +69,7 @@ open class UrlJsonRequestEncodingNode<Type>: AsyncNode {
} catch {
log += "But can't encode data -> terminate with error"
await logContext.add(log)
return .failure(RequestEncodingError.unsupportedDataType)
return .failure(RequestEncodingNodeError.unsupportedDataType)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// RequstEncoderNode.swift
// RequestEncoderNode.swift
// CoreNetKit
//
// Created by Александр Кравченков on 05/03/2019.
Expand All @@ -17,7 +17,7 @@ import Foundation
/// - `RequestRouterNode`
/// - `EncodableRequestModel`
/// - `UrlRequestTrasformatorNode`
open class RequstEncoderNode<Raw, Route, Encoding, Output>: AsyncNode {
open class RequestEncoderNode<Raw, Route, Encoding, Output>: AsyncNode {

/// Тип для следюущего узла.
public typealias NextNode = AsyncNode<EncodableRequestModel<Route, Raw, Encoding>, Output>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Foundation
/// - `RoutableRequestModel`
/// - `Node`
/// - `MetadataConnectorNode`
/// - `RequstEncoderNode`
/// - `RequestEncoderNode`
open class RequestRouterNode<Raw, Route, Output>: AsyncNode {

/// Тип для следующего узла.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import Foundation

enum RequestEncodingError: Error {
case unsupportedDataType
}

/// Этот узел переводит Generic запрос в конкретную реализацию.
/// Данный узел работает с URL-запросами, по HTTP протоколу с JSON
open class UrlRequestTrasformatorNode<Type>: AsyncNode {
Expand Down Expand Up @@ -59,7 +55,7 @@ open class UrlRequestTrasformatorNode<Type>: AsyncNode {
return await .withMappedExceptions {
let url = try data.route.url()
let params = TransportUrlParameters(
method: self.method,
method: method,
url: url,
headers: data.metadata
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// MultipartFormDataFactory.swift
// NodeKit
//
// Created by Andrei Frolov on 05.04.24.
// Copyright © 2024 Surf. All rights reserved.
//

/// Протокол фабрики для создания объекта, позволяющего собирать multipart/form-data.
public protocol MultipartFormDataFactory {

/// Метод создания объекта.
///
/// - Returns: Объекта для сборки multipart/form-data.
func produce() -> MultipartFormDataProtocol
}

/// Фабрика для создания MultipartFormData - реализации Alamofire.
public struct AlamofireMultipartFormDataFactory: MultipartFormDataFactory {

// MARK: - Initialization

public init() { }

// MARK: - MultipartFormDataFactory

/// Метод создания объекта.
///
/// - Returns: Реализация протокола ``MultipartFormDataProtocol`` от Alamofire.
public func produce() -> MultipartFormDataProtocol {
return MultipartFormData()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ public struct MultipartUrlRequest {

/// Узел, умеющий создавать multipart-запрос.
open class MultipartRequestCreatorNode<Output>: AsyncNode {

// MARK: - Public Properties

/// Следующий узел для обработки.
public var next: any AsyncNode<URLRequest, Output>

// MARK: - Private Properties

private let multipartFormDataFactory: MultipartFormDataFactory

/// Инициаллизирует узел.
///
/// - Parameter next: Следующий узел для обработки.
public init(next: any AsyncNode<URLRequest, Output>) {
public init(
next: any AsyncNode<URLRequest, Output>,
multipartFormDataFactory: MultipartFormDataFactory = AlamofireMultipartFormDataFactory()
) {
self.next = next
self.multipartFormDataFactory = multipartFormDataFactory
}

/// Конфигурирует низкоуровневый запрос.
Expand All @@ -44,10 +55,10 @@ open class MultipartRequestCreatorNode<Output>: AsyncNode {
request.httpMethod = data.method.rawValue

// Add Headers
data.headers.forEach { request.addValue($0.key, forHTTPHeaderField: $0.value) }
data.headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }

// Form Data
let formData = MultipartFormData(fileManager: FileManager.default)
let formData = multipartFormDataFactory.produce()
append(multipartForm: formData, with: data)
request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
let encodedFormData = try formData.encode()
Expand All @@ -71,10 +82,10 @@ open class MultipartRequestCreatorNode<Output>: AsyncNode {
request.httpMethod = data.method.rawValue

// Add Headers
data.headers.forEach { request.addValue($0.key, forHTTPHeaderField: $0.value) }
data.headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }

// Form Data
let formData = MultipartFormData(fileManager: FileManager.default)
let formData = multipartFormDataFactory.produce()
append(multipartForm: formData, with: data)
request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
let encodedFormData = try formData.encode()
Expand All @@ -96,7 +107,7 @@ open class MultipartRequestCreatorNode<Output>: AsyncNode {
return Log(message, id: self.objectName, order: LogOrder.requestCreatorNode)
}

open func append(multipartForm: MultipartFormData, with request: MultipartUrlRequest) {
open func append(multipartForm: MultipartFormDataProtocol, with request: MultipartUrlRequest) {
request.data.payloadModel.forEach { key, value in
multipartForm.append(value, withName: key)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ open class RequestSenderNode<Type>: AsyncNode, Aborter {
result: result
)
continuation.resume(with: .success(nodeResponse))
}
}.resume()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ open class ResponseDataParserNode: AsyncNode {
} catch {
var log = Log(logViewObjectName, id: objectName, order: LogOrder.responseDataParserNode)
switch error {
case ResponseDataParserNodeError.cantCastDesirializedDataToJson(let logMsg), ResponseDataParserNodeError.cantDeserializeJson(let logMsg):
case ResponseDataParserNodeError.cantCastDesirializedDataToJson(let logMsg),
ResponseDataParserNodeError.cantDeserializeJson(let logMsg):
log += logMsg
default:
log += "Catch \(error)"
Expand Down
6 changes: 4 additions & 2 deletions NodeKit/Layers/Utils/AccessSafe/AccessSafeNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ open class AccessSafeNode: AsyncNode {
) async -> NodeResult<Json> {
return await next.process(data, logContext: logContext)
.asyncFlatMapError { error in
guard case ResponseHttpErrorProcessorNodeError.forbidden = error else {
switch error {
case ResponseHttpErrorProcessorNodeError.forbidden, ResponseHttpErrorProcessorNodeError.unauthorized:
return await processWithTokenUpdate(data, logContext: logContext)
default:
return .failure(error)
}
return await processWithTokenUpdate(data, logContext: logContext)
}
}

Expand Down
2 changes: 1 addition & 1 deletion NodeKit/ThirdParty/Alamofire/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import CoreServices
/// - https://www.ietf.org/rfc/rfc2388.txt
/// - https://www.ietf.org/rfc/rfc2045.txt
/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
open class MultipartFormData {
open class MultipartFormData: MultipartFormDataProtocol {

// MARK: - Helper Types
struct EncodingCharacters {
Expand Down
84 changes: 84 additions & 0 deletions NodeKit/ThirdParty/Alamofire/MultipartFormDataProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// MultipartFormDataProtocol.swift
// NodeKit
//
// Created by Andrei Frolov on 05.04.24.
// Copyright © 2024 Surf. All rights reserved.
//

import Foundation

/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
public protocol MultipartFormDataProtocol {

/// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`
var contentType: String { get set }

/// Creates a body part from the data and appends it to the multipart form data object.
///
/// The body part data will be encoded using the following format:
///
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
/// - `Content-Type: #{mimeType}` (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// - parameter data: The data to encode into the multipart form data.
/// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
/// - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header.
/// - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header.
func append(_ data: Data, withName name: String, fileName: String?, mimeType: String?)

/// Creates a body part from the file and appends it to the multipart form data object.
///
/// The body part data will be encoded using the following format:
///
/// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
/// - `Content-Type: #{generated mimeType}` (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
/// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
/// system associated MIME type.
///
/// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
/// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
func append(_ fileURL: URL, withName name: String)

/// Creates a body part from the file and appends it to the multipart form data object.
///
/// The body part data will be encoded using the following format:
///
/// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
/// - Content-Type: #{mimeType} (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
/// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
/// - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header.
/// - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header.
func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String)

/// Encodes all the appended body parts into a single `Data` value.
///
/// It is important to note that this method will load all the appended body parts into memory all at the same
/// time. This method should only be used when the encoded data will have a small memory footprint. For large data
/// cases, please use the `writeEncodedData(to:))` method.
///
/// - throws: An `AFError` if encoding encounters an error.
///
/// - returns: The encoded `Data` if encoding is successful.
func encode() throws -> Data
}

extension MultipartFormDataProtocol {
func append(_ data: Data, withName name: String) {
append(data, withName: name, fileName: nil, mimeType: nil)
}
}
Loading
Loading