diff --git a/NodeKit/NodeKit/Chains/ChainBuilder.swift b/NodeKit/NodeKit/Chains/ChainBuilder.swift index 212da6d0..4ea17111 100644 --- a/NodeKit/NodeKit/Chains/ChainBuilder.swift +++ b/NodeKit/NodeKit/Chains/ChainBuilder.swift @@ -219,7 +219,7 @@ open class URLChainBuilder: ChainConfigBuilder, ChainBu open func build() -> AnyAsyncNode 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(next: metadataConnectorChain) let dtoEncoderNode = ModelInputNode(next: rawEncoderNode) diff --git a/NodeKit/NodeKit/Chains/ServiceChainProvider.swift b/NodeKit/NodeKit/Chains/ServiceChainProvider.swift index a9479f5d..8b4473b0 100644 --- a/NodeKit/NodeKit/Chains/ServiceChainProvider.swift +++ b/NodeKit/NodeKit/Chains/ServiceChainProvider.swift @@ -17,7 +17,9 @@ public protocol ServiceChainProvider { with providers: [MetadataProvider] ) -> any AsyncNode - func provideRequestMultipartChain() -> any AsyncNode + func provideRequestMultipartChain( + with providers: [MetadataProvider] + ) -> any AsyncNode } open class URLServiceChainProvider: ServiceChainProvider { @@ -87,13 +89,15 @@ open class URLServiceChainProvider: ServiceChainProvider { } /// Chain for creating and sending a request, expecting a Multipart response. - open func provideRequestMultipartChain() -> any AsyncNode { + open func provideRequestMultipartChain( + with providers: [MetadataProvider] + ) -> any AsyncNode { 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) } } diff --git a/NodeKit/NodeKit/Layers/RequestProcessingLayer/MultipartRequestCreatorNode.swift b/NodeKit/NodeKit/Layers/RequestProcessingLayer/MultipartRequestCreatorNode.swift index da4cb67e..48995adc 100644 --- a/NodeKit/NodeKit/Layers/RequestProcessingLayer/MultipartRequestCreatorNode.swift +++ b/NodeKit/NodeKit/Layers/RequestProcessingLayer/MultipartRequestCreatorNode.swift @@ -35,15 +35,18 @@ open class MultipartRequestCreatorNode: 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, + providers: [MetadataProvider] = [], multipartFormDataFactory: MultipartFormDataFactory = AlamofireMultipartFormDataFactory() ) { self.next = next + self.providers = providers self.multipartFormDataFactory = multipartFormDataFactory } @@ -61,13 +64,17 @@ open class MultipartRequestCreatorNode: 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)) diff --git a/NodeKit/NodeKitMock/Builder/ServiceChainProviderMock.swift b/NodeKit/NodeKitMock/Builder/ServiceChainProviderMock.swift index 0a47d566..f6aadc00 100644 --- a/NodeKit/NodeKitMock/Builder/ServiceChainProviderMock.swift +++ b/NodeKit/NodeKitMock/Builder/ServiceChainProviderMock.swift @@ -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)! - open func provideRequestMultipartChain() -> any AsyncNode { + open func provideRequestMultipartChain( + with providers: [MetadataProvider] + ) -> any AsyncNode { invokedProvideRequestMultipartChain = true invokedProvideRequestMultipartChainCount += 1 + invokedProvideRequestMultipartChainParameter = providers + invokedProvideRequestMultipartChainParameterList.append(providers) return stubbedProvideRequestMultipartChainResult } } diff --git a/NodeKit/NodeKitMock/URLServiceChainProviderMock.swift b/NodeKit/NodeKitMock/URLServiceChainProviderMock.swift index 7b81586d..031e622d 100644 --- a/NodeKit/NodeKitMock/URLServiceChainProviderMock.swift +++ b/NodeKit/NodeKitMock/URLServiceChainProviderMock.swift @@ -34,13 +34,15 @@ class URLServiceChainProviderMock: URLServiceChainProvider { return RequestCreatorNode(next: aborterNode, providers: providers) } - override func provideRequestMultipartChain() -> any AsyncNode { + override func provideRequestMultipartChain( + with providers: [MetadataProvider] + ) -> any AsyncNode { 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) } } diff --git a/NodeKit/NodeKitTests/UnitTests/Nodes/MultipartRequestCreatorNodeTests.swift b/NodeKit/NodeKitTests/UnitTests/Nodes/MultipartRequestCreatorNodeTests.swift index db8d1ef5..96f9836d 100644 --- a/NodeKit/NodeKitTests/UnitTests/Nodes/MultipartRequestCreatorNodeTests.swift +++ b/NodeKit/NodeKitTests/UnitTests/Nodes/MultipartRequestCreatorNodeTests.swift @@ -20,10 +20,6 @@ final class MultipartRequestCreatorNodeTest: XCTestCase { private var multipartFormDataFactoryMock: MultipartFormDataFactoryMock! private var multipartFormDataMock: MultipartFormDataMock! - // MARK: - Sut - - private var sut: MultipartRequestCreatorNode! - // MARK: - Lifecycle override func setUp() { @@ -33,10 +29,6 @@ final class MultipartRequestCreatorNodeTest: XCTestCase { multipartFormDataFactoryMock = MultipartFormDataFactoryMock() multipartFormDataMock = MultipartFormDataMock() multipartFormDataFactoryMock.stubbedProduceResult = multipartFormDataMock - sut = MultipartRequestCreatorNode( - next: nextNodeMock, - multipartFormDataFactory: multipartFormDataFactoryMock - ) } override func tearDown() { @@ -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]>( @@ -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]>( @@ -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" @@ -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" @@ -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")!, @@ -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")!, @@ -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"] @@ -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")!, @@ -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")!, @@ -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")!, @@ -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")!, @@ -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( + 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( + 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) + } }