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

Header providers for multipart request #145

Open
wants to merge 1 commit into
base: update-xcresultparser
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion NodeKit/NodeKit/Chains/ChainBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ open class URLChainBuilder<Route: URLRouteProvider>: ChainConfigBuilder, ChainBu

open func build<I: DTOEncodable, O: DTODecodable>() -> AnyAsyncNode<I, O>
where O.DTO.Raw == Json, I.DTO.Raw == MultipartModel<[String : Data]> {
let requestChain = serviceChainProvider.provideRequestMultipartChain()
let requestChain = serviceChainProvider.provideRequestMultipartChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let rawEncoderNode = DTOMapperNode<I.DTO,O.DTO>(next: metadataConnectorChain)
let dtoEncoderNode = ModelInputNode<I, O>(next: rawEncoderNode)
Expand Down
10 changes: 7 additions & 3 deletions NodeKit/NodeKit/Chains/ServiceChainProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public protocol ServiceChainProvider {
with providers: [MetadataProvider]
) -> any AsyncNode<TransportURLRequest, Data>

func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json>
func provideRequestMultipartChain(
with providers: [MetadataProvider]
) -> any AsyncNode<MultipartURLRequest, Json>
}

open class URLServiceChainProvider: ServiceChainProvider {
Expand Down Expand Up @@ -87,13 +89,15 @@ open class URLServiceChainProvider: ServiceChainProvider {
}

/// Chain for creating and sending a request, expecting a Multipart response.
open func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json> {
open func provideRequestMultipartChain(
with providers: [MetadataProvider]
) -> any AsyncNode<MultipartURLRequest, Json> {
let responseChain = provideResponseMultipartChain()
let requestSenderNode = RequestSenderNode(
rawResponseProcessor: responseChain,
manager: session
)
let aborterNode = AborterNode(next: requestSenderNode, aborter: requestSenderNode)
return MultipartRequestCreatorNode(next: aborterNode)
return MultipartRequestCreatorNode(next: aborterNode, providers: providers)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@ open class MultipartRequestCreatorNode<Output>: AsyncNode {
// MARK: - Private Properties

private let multipartFormDataFactory: MultipartFormDataFactory
private let providers: [MetadataProvider]

/// Initializer.
///
/// - Parameter next: The next node for processing.
public init(
next: any AsyncNode<URLRequest, Output>,
providers: [MetadataProvider] = [],
multipartFormDataFactory: MultipartFormDataFactory = AlamofireMultipartFormDataFactory()
) {
self.next = next
self.providers = providers
self.multipartFormDataFactory = multipartFormDataFactory
}

Expand All @@ -61,13 +64,17 @@ open class MultipartRequestCreatorNode<Output>: AsyncNode {
return .success(try formData.encode())
}
.asyncFlatMap { encodedData in
var mergedHeaders = data.headers

providers.map { $0.metadata() }.forEach { dict in
mergedHeaders.merge(dict, uniquingKeysWith: { $1 })
}

var request = URLRequest(url: data.url)
request.httpMethod = data.method.rawValue

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

request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
request.httpBody = encodedData
mergedHeaders.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }

return await .withCheckedCancellation {
await logContext.add(getLogMessage(data))
Expand Down
8 changes: 7 additions & 1 deletion NodeKit/NodeKitMock/Builder/ServiceChainProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,17 @@ open class ServiceChainProviderMock: ServiceChainProvider {

public var invokedProvideRequestMultipartChain = false
public var invokedProvideRequestMultipartChainCount = 0
public var invokedProvideRequestMultipartChainParameter: [MetadataProvider]?
public var invokedProvideRequestMultipartChainParameterList: [[MetadataProvider]] = []
public var stubbedProvideRequestMultipartChainResult: (any AsyncNode<MultipartURLRequest, Json>)!

open func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json> {
open func provideRequestMultipartChain(
with providers: [MetadataProvider]
) -> any AsyncNode<MultipartURLRequest, Json> {
invokedProvideRequestMultipartChain = true
invokedProvideRequestMultipartChainCount += 1
invokedProvideRequestMultipartChainParameter = providers
invokedProvideRequestMultipartChainParameterList.append(providers)
return stubbedProvideRequestMultipartChainResult
}
}
6 changes: 4 additions & 2 deletions NodeKit/NodeKitMock/URLServiceChainProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ class URLServiceChainProviderMock: URLServiceChainProvider {
return RequestCreatorNode(next: aborterNode, providers: providers)
}

override func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json> {
override func provideRequestMultipartChain(
with providers: [MetadataProvider]
) -> any AsyncNode<MultipartURLRequest, Json> {
let responseChain = provideResponseMultipartChain()
let requestSenderNode = RequestSenderNode(
rawResponseProcessor: responseChain,
manager: NetworkMock().urlSession
)
let aborterNode = AborterNode(next: requestSenderNode, aborter: requestSenderNode)
return MultipartRequestCreatorNode(next: aborterNode)
return MultipartRequestCreatorNode(next: aborterNode, providers: providers)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
private var multipartFormDataFactoryMock: MultipartFormDataFactoryMock!
private var multipartFormDataMock: MultipartFormDataMock!

// MARK: - Sut

private var sut: MultipartRequestCreatorNode<Int>!

// MARK: - Lifecycle

override func setUp() {
Expand All @@ -33,10 +29,6 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
multipartFormDataFactoryMock = MultipartFormDataFactoryMock()
multipartFormDataMock = MultipartFormDataMock()
multipartFormDataFactoryMock.stubbedProduceResult = multipartFormDataMock
sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)
}

override func tearDown() {
Expand All @@ -45,14 +37,18 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
logContextMock = nil
multipartFormDataFactoryMock = nil
multipartFormDataMock = nil
sut = nil
}

// MARK: - Tests

func testAsyncProcess_withMultipartFormPayloadData_thenMultipartFormDataAppendCalled() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let payloadKey = "TestPayloadKey"
let payloadValue = "TestPayloadValue".data(using: .utf8)!
let multipartModel = MultipartModel<[String: Data]>(
Expand Down Expand Up @@ -92,7 +88,12 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {

func testAsyncProcess_withMultipartFormFileURL_thenMultipartFormDataAppendCalled() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let fileKey = "TestFileKey1"
let fileURL = URL(string: "www.testfirstfile.com")!
let multipartModel = MultipartModel<[String: Data]>(
Expand Down Expand Up @@ -129,7 +130,12 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {

func testAsyncProcess_withMultipartFormFileData_thenMultipartFormDataAppendCalled() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let fileKey = "TestFileKey2"
let fileData = "TestSecondFileData".data(using: .utf8)!
let fileName = "TestSecondFile.name"
Expand Down Expand Up @@ -170,7 +176,12 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {

func testAsyncProcess_withMultipartFormFileCustomURL_thenMultipartFormDataAppendCalled() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let fileKey = "TestFileKey3"
let fileURL = URL(string: "www.testthirdfile.com")!
let fileName = "TestThirdFile.name"
Expand Down Expand Up @@ -212,6 +223,11 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
func testAsyncProcess_withEncodingError_thenNextDidNotCall() async throws {
// given

let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .delete,
url: URL(string: "www.testprocess.com")!,
Expand All @@ -235,6 +251,11 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
func testAsyncProcess_withEncodingError_thenErrorReceived() async throws {
// given

let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .delete,
url: URL(string: "www.testprocess.com")!,
Expand All @@ -258,6 +279,11 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
func testAsyncProcess_withEncodingSuccess_thenNextCalled() async throws {
// given

let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let expectedURL = URL(string: "www.testprocess.com")!
let stubbedContentType = "TestContentType"
let headers = ["TestHeaderKey": "TestHeaderValue"]
Expand Down Expand Up @@ -296,6 +322,11 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
func testAsyncProcess_withSuccess_thenSuccessReceived() async throws {
// given

let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
Expand All @@ -321,6 +352,11 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
func testAsyncProcess_withError_thenErrorReceived() async throws {
// given

let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
Expand All @@ -344,7 +380,12 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {

func testAsyncProcess_withCancelTask_beforeStart_thenCancellationErrorReceived() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
Expand Down Expand Up @@ -375,7 +416,12 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {

func testAsyncProcess_withCancelTask_afterStart_thenCancellationErrorReceived() async throws {
// given


let sut = MultipartRequestCreatorNode(
next: nextNodeMock,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
Expand Down Expand Up @@ -408,4 +454,108 @@ final class MultipartRequestCreatorNodeTest: XCTestCase {
let error = try XCTUnwrap(result.error)
XCTAssertTrue(error is CancellationError)
}

func testAsyncProcess_withProviders_thenProvidersHeadersMergedWithPassedHeadersReceived() async throws {
// given

let expectedResult = 66
let firstProvider = MetadataProviderMock()
let secondProvider = MetadataProviderMock()
let providers = [
firstProvider,
secondProvider
]

let sut = MultipartRequestCreatorNode<Int>(
next: nextNodeMock,
providers: providers,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
headers: ["TestKey":"TestValue"],
data: MultipartModel<[String: Data]>(payloadModel: [:])
)

multipartFormDataMock.stubbedContentTypeResult = "test"
multipartFormDataMock.stubbedEncodeResult = .success(Data())
nextNodeMock.stubbedAsyncProccessResult = .success(1)

let expectedHeaders = [
"Content-Type": "test",
"TestKey": "TestValue",
"TestFirstProviderKey": "TestFirstProviderValue",
"TestSecondProviderKey": "TestSecondProviderValue"
]

firstProvider.stubbedMetadataResult = ["TestFirstProviderKey": "TestFirstProviderValue"]
secondProvider.stubbedMetadataResult = ["TestSecondProviderKey": "TestSecondProviderValue"]
nextNodeMock.stubbedAsyncProccessResult = .success(expectedResult)

// when

let result = await sut.process(model, logContext: logContextMock)

// then

let parameters = try XCTUnwrap(nextNodeMock.invokedAsyncProcessParameters?.data)
let value = try XCTUnwrap(result.value)

XCTAssertEqual(nextNodeMock.invokedAsyncProcessCount, 1)
XCTAssertEqual(parameters.headers.dictionary, expectedHeaders)
XCTAssertEqual(value, expectedResult)
}

func testAsyncProcess_withProvidersAndSameKeys_thenProvidersHeadersMergedWithPassedHeadersReceived() async throws {
// given

let expectedResult = 66
let firstProvider = MetadataProviderMock()
let secondProvider = MetadataProviderMock()
let providers = [
firstProvider,
secondProvider
]
let sut = MultipartRequestCreatorNode<Int>(
next: nextNodeMock,
providers: providers,
multipartFormDataFactory: multipartFormDataFactoryMock
)

let model = MultipartURLRequest(
method: .get,
url: URL(string: "www.testprocess.com")!,
headers: ["TestKey": "TestValue"],
data: MultipartModel<[String: Data]>(payloadModel: [:])
)

let expectedHeaders = [
"Content-Type": "test",
"TestKey": "TestSecondProviderValue",
"TestFirstProviderKey": "TestFirstProviderValue"
]

multipartFormDataMock.stubbedContentTypeResult = "test"
multipartFormDataMock.stubbedEncodeResult = .success(Data())
nextNodeMock.stubbedAsyncProccessResult = .success(1)

firstProvider.stubbedMetadataResult = ["TestFirstProviderKey": "TestFirstProviderValue"]
secondProvider.stubbedMetadataResult = ["TestKey": "TestSecondProviderValue"]
nextNodeMock.stubbedAsyncProccessResult = .success(expectedResult)

// when

let result = await sut.process(model, logContext: logContextMock)

// then

let parameters = try XCTUnwrap(nextNodeMock.invokedAsyncProcessParameters?.data)
let value = try XCTUnwrap(result.value)

XCTAssertEqual(nextNodeMock.invokedAsyncProcessCount, 1)
XCTAssertEqual(parameters.headers.dictionary, expectedHeaders)
XCTAssertEqual(value, expectedResult)
}
}
Loading