DetailedErrorTests.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import GRPCCore
  17. import GRPCInProcessTransport
  18. import GRPCProtobuf
  19. import SwiftProtobuf
  20. import Testing
  21. struct DetailedErrorTests {
  22. @Test(
  23. "Google RPC Status is transferred over the wire",
  24. arguments: [
  25. ([], []),
  26. (["ErrorInfo"], [.errorInfo(.testValue)]),
  27. (["RetryInfo"], [.retryInfo(.testValue)]),
  28. (["DebugInfo"], [.debugInfo(.testValue)]),
  29. (["QuotaFailure"], [.quotaFailure(.testValue)]),
  30. (["PreconditionFailure"], [.preconditionFailure(.testValue)]),
  31. (["BadRequest"], [.badRequest(.testValue)]),
  32. (["RequestInfo"], [.requestInfo(.testValue)]),
  33. (["ResourceInfo"], [.resourceInfo(.testValue)]),
  34. (["Help"], [.help(.testValue)]),
  35. (["LocalizedMessage"], [.localizedMessage(.testValue)]),
  36. (["DebugInfo", "RetryInfo"], [.debugInfo(.testValue), .retryInfo(.testValue)]),
  37. (["Help", "PreconditionFailure"], [.help(.testValue), .preconditionFailure(.testValue)]),
  38. (["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]),
  39. ] as [([String], [ErrorDetails])]
  40. )
  41. func rpcStatus(details: [String], expected: [ErrorDetails]) async throws {
  42. let inProcess = InProcessTransport()
  43. try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in
  44. try await withGRPCClient(transport: inProcess.client) { client in
  45. let errorClient = ErrorService.Client(wrapping: client)
  46. let subkinds = details.joined(separator: ",")
  47. let kind = "status/\(subkinds)"
  48. await #expect {
  49. try await errorClient.throwError(.with { $0.kind = kind })
  50. } throws: { error in
  51. guard let rpcError = error as? RPCError else { return false }
  52. guard let status = try? rpcError.unpackGoogleRPCStatus() else { return false }
  53. // Code/message should be the same.
  54. #expect(status.code == rpcError.code)
  55. #expect(status.message == rpcError.message)
  56. // Set by the service.
  57. #expect(status.code == .unknown)
  58. #expect(status.message == subkinds)
  59. #expect(status.details == expected)
  60. return true
  61. }
  62. }
  63. }
  64. }
  65. @Test(
  66. arguments: [
  67. (.errorInfo(.testValue), #"ErrorInfo(reason: "r", domain: "d", metadata: ["k": "v"])"#),
  68. (.retryInfo(.testValue), #"RetryInfo(delay: 1.0 seconds)"#),
  69. (.debugInfo(.testValue), #"DebugInfo(stack: ["foo.foo()", "foo.bar()"], detail: "detail")"#),
  70. (
  71. .quotaFailure(.testValue),
  72. #"QuotaFailure(violations: [Violation(subject: "s", violationDescription: "d")])"#
  73. ),
  74. (
  75. .preconditionFailure(.testValue),
  76. #"PreconditionFailure(violations: [Violation(subject: "s", type: "t", violationDescription: "d")])"#
  77. ),
  78. (
  79. .badRequest(.testValue),
  80. #"BadRequest(violations: [FieldViolation(field: "f", violationDescription: "d")])"#
  81. ),
  82. (.requestInfo(.testValue), #"RequestInfo(requestID: "id", servingData: "d")"#),
  83. (
  84. .resourceInfo(.testValue),
  85. #"ResourceInfo(name: "n", owner: "", type: "t", errorDescription: "d")"#
  86. ),
  87. (.help(.testValue), #"Help(links: [Link(url: "url", linkDescription: "d")])"#),
  88. (.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#),
  89. ] as [(ErrorDetails, String)]
  90. )
  91. func errorInfoDescription(_ details: ErrorDetails, expected: String) {
  92. #expect(String(describing: details) == expected)
  93. }
  94. }
  95. private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
  96. func throwError(
  97. request: ThrowInput,
  98. context: ServerContext
  99. ) async throws -> Google_Protobuf_Empty {
  100. if request.kind.starts(with: "status/") {
  101. try self.throwStatusError(kind: String(request.kind.dropFirst("status/".count)))
  102. } else {
  103. throw RPCError(code: .invalidArgument, message: "'\(request.kind)' is invalid.")
  104. }
  105. }
  106. private func throwStatusError(kind: String) throws(GoogleRPCStatus) -> Never {
  107. var details: [ErrorDetails] = []
  108. for subkind in kind.split(separator: ",") {
  109. if let detail = self.errorDetails(kind: String(subkind)) {
  110. details.append(detail)
  111. } else {
  112. throw GoogleRPCStatus(
  113. code: .invalidArgument,
  114. message: "Unknown error subkind",
  115. details: [
  116. .badRequest(
  117. violations: [
  118. ErrorDetails.BadRequest.FieldViolation(
  119. field: "kind",
  120. description: "'\(kind)' is invalid"
  121. )
  122. ]
  123. )
  124. ]
  125. )
  126. }
  127. }
  128. throw GoogleRPCStatus(code: .unknown, message: kind, details: details)
  129. }
  130. private func errorDetails(kind: String) -> ErrorDetails? {
  131. let details: ErrorDetails?
  132. switch kind {
  133. case "ErrorInfo":
  134. details = .errorInfo(.testValue)
  135. case "RetryInfo":
  136. details = .retryInfo(.testValue)
  137. case "DebugInfo":
  138. details = .debugInfo(.testValue)
  139. case "QuotaFailure":
  140. details = .quotaFailure(.testValue)
  141. case "PreconditionFailure":
  142. details = .preconditionFailure(.testValue)
  143. case "BadRequest":
  144. details = .badRequest(.testValue)
  145. case "RequestInfo":
  146. details = .requestInfo(.testValue)
  147. case "ResourceInfo":
  148. details = .resourceInfo(.testValue)
  149. case "Help":
  150. details = .help(.testValue)
  151. case "LocalizedMessage":
  152. details = .localizedMessage(.testValue)
  153. default:
  154. details = nil
  155. }
  156. return details
  157. }
  158. }
  159. extension ErrorDetails.ErrorInfo {
  160. fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"])
  161. }
  162. extension ErrorDetails.RetryInfo {
  163. fileprivate static let testValue = Self(delay: .seconds(1))
  164. }
  165. extension ErrorDetails.DebugInfo {
  166. fileprivate static let testValue = Self(
  167. stack: ["foo.foo()", "foo.bar()"],
  168. detail: "detail"
  169. )
  170. }
  171. extension ErrorDetails.QuotaFailure {
  172. fileprivate static let testValue = Self(
  173. violations: [
  174. Violation(subject: "s", description: "d")
  175. ]
  176. )
  177. }
  178. extension ErrorDetails.PreconditionFailure {
  179. fileprivate static let testValue = Self(
  180. violations: [
  181. Violation(type: "t", subject: "s", description: "d")
  182. ]
  183. )
  184. }
  185. extension ErrorDetails.BadRequest {
  186. fileprivate static let testValue = Self(
  187. violations: [
  188. FieldViolation(field: "f", description: "d")
  189. ]
  190. )
  191. }
  192. extension ErrorDetails.RequestInfo {
  193. fileprivate static let testValue = Self(requestID: "id", servingData: "d")
  194. }
  195. extension ErrorDetails.ResourceInfo {
  196. fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d")
  197. }
  198. extension ErrorDetails.Help {
  199. fileprivate static let testValue = Self(
  200. links: [
  201. Link(url: "url", description: "d")
  202. ]
  203. )
  204. }
  205. extension ErrorDetails.LocalizedMessage {
  206. fileprivate static let testValue = Self(locale: "l", message: "m")
  207. }