diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 1ccdcfd00..49ea3973c 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -344,9 +344,9 @@ extension ClientResponse.Stream { } } - /// Returns metadata received from the server at the end of the response. + /// Returns the messages received from the server. /// - /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + /// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``. public var messages: RPCAsyncSequence { switch self.accepted { case let .success(contents): diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift index fea2d18c3..112a36ee3 100644 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -21,6 +21,12 @@ public struct AssertionFailure: Error { public var message: String public var file: String public var line: Int + + public init(message: String, file: String = #fileID, line: Int = #line) { + self.message = message + self.file = file + self.line = line + } } /// Asserts that the value of an expression is `true`. @@ -34,3 +40,18 @@ public func assertTrue( throw AssertionFailure(message: message, file: file, line: line) } } + +/// Asserts that the two given values are equal. +public func assertEqual( + _ value1: T, + _ value2: T, + file: String = #fileID, + line: Int = #line +) throws { + return try assertTrue( + value1 == value2, + "'\(value1)' is not equal to '\(value2)'", + file: file, + line: line + ) +} diff --git a/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..5f1b75dd6 --- /dev/null +++ b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift new file mode 100644 index 000000000..6a909e6ea --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -0,0 +1,98 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import GRPCCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol InteroperabilityTest { + /// Run a test case using the given connection. + /// + /// The test case is considered unsuccessful if any exception is thrown, conversely if no + /// exceptions are thrown it is successful. + /// + /// - Parameter client: The client to use for the test. + /// - Throws: Any exception may be thrown to indicate an unsuccessful test. + func run(client: GRPCClient) async throws +} + +/// Test cases as listed by the [gRPC interoperability test description +/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). +/// +/// This is not a complete list, the following tests have not been implemented: +/// - cacheable_unary +/// - client-compressed-unary +/// - server-compressed-unary +/// - client_compressed_streaming +/// - server_compressed_streaming +/// - compute_engine_creds +/// - jwt_token_creds +/// - oauth2_auth_token +/// - per_rpc_creds +/// - google_default_credentials +/// - compute_engine_channel_credentials +/// - cancel_after_begin +/// - cancel_after_first_response +/// +/// Note: Tests for compression have not been implemented yet as compression is +/// not supported. Once the API which allows for compression will be implemented +/// these tests should be added. +public enum InteroperabilityTestCase: String, CaseIterable { + case emptyUnary = "empty_unary" + case largeUnary = "large_unary" + case clientStreaming = "client_streaming" + case serverStreaming = "server_streaming" + case pingPong = "ping_pong" + case emptyStream = "empty_stream" + case customMetadata = "custom_metadata" + case statusCodeAndMessage = "status_code_and_message" + case specialStatusMessage = "special_status_message" + case unimplementedMethod = "unimplemented_method" + case unimplementedService = "unimplemented_service" + + public var name: String { + return self.rawValue + } +} + +@available(macOS 13.0, *) +extension InteroperabilityTestCase { + /// Return a new instance of the test case. + public func makeTest() -> InteroperabilityTest { + switch self { + case .emptyUnary: + return EmptyUnary() + case .largeUnary: + return LargeUnary() + case .clientStreaming: + return ClientStreaming() + case .serverStreaming: + return ServerStreaming() + case .pingPong: + return PingPong() + case .emptyStream: + return EmptyStream() + case .customMetadata: + return CustomMetadata() + case .statusCodeAndMessage: + return StatusCodeAndMessage() + case .specialStatusMessage: + return SpecialStatusMessage() + case .unimplementedMethod: + return UnimplementedMethod() + case .unimplementedService: + return UnimplementedService() + } + } +} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift new file mode 100644 index 000000000..cfebda759 --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -0,0 +1,695 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GRPCCore + +import struct Foundation.Data + +/// This test verifies that implementations support zero-size messages. Ideally, client +/// implementations would verify that the request and response were zero bytes serialized, but +/// this is generally prohibitive to perform, so is not required. +/// +/// Server features: +/// - EmptyCall +/// +/// Procedure: +/// 1. Client calls EmptyCall with the default Empty message +/// +/// Client asserts: +/// - call was successful +/// - response is non-null +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct EmptyUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.emptyCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + try assertEqual(response.message, Grpc_Testing_Empty()) + } + } +} + +/// This test verifies unary calls succeed in sending messages, and touches on flow control (even +/// if compression is enabled on the channel). +/// +/// Server features: +/// - UnaryCall +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - response payload body is 314159 bytes in size +/// - clients are free to assert that the response payload body contents are zero and comparing +/// the entire response message against a golden response +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct LargeUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: request) + ) { response in + try assertEqual( + response.message.payload, + Grpc_Testing_Payload.with { + $0.body = Data(count: 314_159) + } + ) + } + } +} + +/// This test verifies that client-only streaming succeeds. +/// +/// Server features: +/// - StreamingInputCall +/// +/// Procedure: +/// 1. Client calls StreamingInputCall +/// 2. Client sends: +/// ``` +/// { +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 3. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 4. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 5. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 6. Client half-closes +/// +/// Client asserts: +/// - call was successful +/// - response aggregated_payload_size is 74922 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ClientStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = ClientRequest.Stream { writer in + for bytes in [27182, 8, 1828, 45904] { + let message = Grpc_Testing_StreamingInputCallRequest.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: bytes) + } + } + try await writer.write(message) + } + } + + try await testServiceClient.streamingInputCall(request: request) { response in + try assertEqual(response.message.aggregatedPayloadSize, 74922) + } + } +} + +/// This test verifies that server-only streaming succeeds. +/// +/// Server features: +/// - StreamingOutputCall +/// +/// Procedure: +/// 1. Client calls StreamingOutputCall with StreamingOutputCallRequest: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// response_parameters:{ +/// size: 9 +/// } +/// response_parameters:{ +/// size: 2653 +/// } +/// response_parameters:{ +/// size: 58979 +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ServerStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let responseSizes = [31415, 9, 2653, 58979] + let request = Grpc_Testing_StreamingOutputCallRequest.with { request in + request.responseParameters = responseSizes.map { + var parameter = Grpc_Testing_ResponseParameters() + parameter.size = Int32($0) + return parameter + } + } + + try await testServiceClient.streamingOutputCall( + request: ClientRequest.Single(message: request) + ) { response in + var responseParts = response.messages.makeAsyncIterator() + // There are 4 response sizes, so if there isn't a message for each one, + // it means that the client didn't receive 4 messages back. + for responseSize in responseSizes { + if let message = try await responseParts.next() { + try assertEqual(message.payload.body.count, responseSize) + } else { + throw AssertionFailure( + message: "There were less than four responses received." + ) + } + } + // Check that there were not more than 4 responses from the server. + try assertEqual(try await responseParts.next(), nil) + } + } +} + +/// This test verifies that full duplex bidi is supported. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 2. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 9 +/// } +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 3. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 2653 +/// } +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 4. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 58979 +/// } +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 5. After getting a reply, client half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct PingPong: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let ids = AsyncStream.makeStream(of: Int.self) + + let request = ClientRequest.Stream { writer in + let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] + for try await id in ids.stream { + var message = Grpc_Testing_StreamingOutputCallRequest() + switch id { + case 1 ... 4: + let (responseSize, bodySize) = sizes[id - 1] + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = Int32(responseSize) + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: bodySize) + } + default: + // When the id is higher than 4 it means the client received all the expected responses + // and it doesn't need to send another message. + return + } + try await writer.write(message) + } + } + ids.continuation.yield(1) + try await testServiceClient.fullDuplexCall(request: request) { response in + var id = 1 + for try await message in response.messages { + switch id { + case 1: + try assertEqual(message.payload.body, Data(count: 31_415)) + case 2: + try assertEqual(message.payload.body, Data(count: 9)) + case 3: + try assertEqual(message.payload.body, Data(count: 2_653)) + case 4: + try assertEqual(message.payload.body, Data(count: 58_979)) + default: + throw AssertionFailure( + message: "We should only receive messages with ids between 1 and 4." + ) + } + + // Add the next id to the continuation. + id += 1 + ids.continuation.yield(id) + } + } + } +} + +/// This test verifies that streams support having zero-messages in both directions. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly zero responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct EmptyStream: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = ClientRequest.Stream { _ in } + + try await testServiceClient.fullDuplexCall(request: request) { response in + var messages = response.messages.makeAsyncIterator() + try await assertEqual(messages.next(), nil) + } + } +} + +/// This test verifies that custom metadata in either binary or ascii format can be sent as +/// initial-metadata by the client and as both initial- and trailing-metadata by the server. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Metadata +/// +/// Procedure: +/// 1. The client attaches custom metadata with the following keys and values +/// to a UnaryCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// 2. The client attaches custom metadata with the following keys and values +/// to a FullDuplexCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_parameters:{ +/// size: 314159 +/// } +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - metadata with key "x-grpc-test-echo-initial" and value "test_initial_metadata_value" is +/// received in the initial metadata for calls in Procedure steps 1 and 2. +/// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the +/// trailing metadata for calls in Procedure steps 1 and 2. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct CustomMetadata: InteroperabilityTest { + let initialMetadataName = "x-grpc-test-echo-initial" + let initialMetadataValue = "test_initial_metadata_value" + + let trailingMetadataName = "x-grpc-test-echo-trailing-bin" + let trailingMetadataValue: [UInt8] = [0xAB, 0xAB, 0xAB] + + func checkInitialMetadata(_ metadata: Metadata) throws { + let values = metadata[self.initialMetadataName] + try assertEqual(Array(values), [.string(self.initialMetadataValue)]) + } + + func checkTrailingMetadata(_ metadata: Metadata) throws { + let values = metadata[self.trailingMetadataName] + try assertEqual(Array(values), [.binary(self.trailingMetadataValue)]) + } + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let unaryRequest = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + let metadata: Metadata = [ + self.initialMetadataName: .string(self.initialMetadataValue), + self.trailingMetadataName: .binary(self.trailingMetadataValue), + ] + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: unaryRequest, metadata: metadata) + ) { response in + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try checkInitialMetadata(receivedInitialMetadata) + + // Check the message. + try assertEqual(response.message.payload.body, Data(count: 314_159)) + + // Check the trailing metadata. + try checkTrailingMetadata(response.trailingMetadata) + } + + let streamingRequest = ClientRequest.Stream(metadata: metadata) { writer in + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 314_159 + } + ] + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: streamingRequest) { response in + switch response.accepted { + case .success(let contents): + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try self.checkInitialMetadata(receivedInitialMetadata) + + let parts = try await contents.bodyParts.reduce(into: []) { $0.append($1) } + try assertEqual(parts.count, 2) + + for part in parts { + switch part { + // Check the message. + case .message(let message): + try assertEqual(message.payload.body, Data(count: 314_159)) + // Check the trailing metadata. + case .trailingMetadata(let receivedTrailingMetadata): + try self.checkTrailingMetadata(receivedTrailingMetadata) + } + } + case .failure(_): + throw AssertionFailure( + message: "The client should have received a response from the server." + ) + } + } + } +} + +/// This test verifies unary calls succeed in sending messages, and propagate back status code and +/// message sent along with the messages. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 2. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 3. and then half-closes +/// +/// Client asserts: +/// - received status code is the same as the sent code for both Procedure steps 1 and 2 +/// - received status message is the same as the sent message for both Procedure steps 1 and 2 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct StatusCodeAndMessage: InteroperabilityTest { + let expectedCode = 2 + let expectedMessage = "test status message" + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in + switch response.accepted { + case .failure(let error): + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + case .success(_): + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client." + ) + } + } + + let request = ClientRequest.Stream { writer in + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: request) { response in + do { + for try await _ in response.messages { + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client." + ) + } + } catch let error as RPCError { + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + } + } + } +} + +/// This test verifies Unicode and whitespace is correctly processed in status message. "\t" is +/// horizontal tab. "\r" is carriage return. "\n" is line feed. +/// +/// Server features: +/// - UnaryCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is the same as the sent code for Procedure step 1 +/// - received status message is the same as the sent message for Procedure step 1, including all +/// whitespace characters +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct SpecialStatusMessage: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = 2 + $0.message = responseMessage + } + } + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in + switch response.accepted { + case .success(_): + throw AssertionFailure( + message: "The response should be an error with the error code 2." + ) + case .failure(let error): + try assertEqual(error.code.rawValue, 2) + try assertEqual(error.message, responseMessage) + } + } + } +} + +/// This test verifies that calling an unimplemented RPC method returns the UNIMPLEMENTED status +/// code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.TestService/UnimplementedCall with an empty request (defined as +/// grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct UnimplementedMethod: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + let result = response.accepted + switch result { + case .success(_): + throw AssertionFailure( + message: "The result should be an error." + ) + case .failure(let error): + try assertEqual(error.code, .unimplemented) + } + } + } +} + +/// This test verifies calling an unimplemented server returns the UNIMPLEMENTED status code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.UnimplementedService/UnimplementedCall with an empty request +/// (defined as grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct UnimplementedService: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) + try await unimplementedServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + let result = response.accepted + switch result { + case .success(_): + throw AssertionFailure( + message: "The result should be an error." + ) + case .failure(let error): + try assertEqual(error.code, .unimplemented) + } + } + } +}