Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ extension TransparentView {
return BridgeMessage(
type: .error,
action: action,
payload: .string(error.createJSON()),
payload: .string(error.createDataJSON()),
id: id
)
}
Expand Down
47 changes: 28 additions & 19 deletions Mindbox/Model/MindboxError/MindboxError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,59 +128,68 @@ public extension MindboxError {
guard
let errorData = try? JSONEncoder().encode(self),
let errorString = String(data: errorData, encoding: .utf8) else {
return
"""
{
type: "InternalError",
data: {
errroKey: "\(self.data.errorKey ?? "null")",
errroName: "JSON encoding error",
errorMessage: "Unable to convert Data to JSON",
}
}
"""
return #"{"type":"InternalError","data":{"errorKey":"\#(self.data.errorKey ?? "null")","errorName":"JSON encoding error","errorMessage":"Unable to convert Data to JSON"}}"#
}
return errorString
}

func convertDataToString() -> String {
guard
let errorData = try? JSONEncoder().encode(data),
let errorString = String(data: errorData, encoding: .utf8) else {
return #"{"errorMessage":"Unable to convert Data to JSON"}"#
Comment on lines +136 to +140

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing on develop, but fair — fixed in 172f622: the fallback is now compact valid JSON with correct key names.

}
return errorString
}
}

func createJSON() -> String {
makeErrorJSON().convertToString()
}

/// Data-only JSON without the `{type, data}` envelope — the WebView JS-bridge
/// `onError` contract. `createJSON()` must keep the envelope: RN/Flutter dispatch on it.
internal func createDataJSON() -> String {
makeErrorJSON().convertDataToString()
}

private func makeErrorJSON() -> MindboxErrorJSON {
switch self {
case .validationError(let error):
return MindboxErrorJSON(status: error.status,
validationMessages: error.validationMessages).convertToString()
validationMessages: error.validationMessages)
case .protocolError(let error):
return MindboxErrorJSON(status: error.status,
errorMessage: error.errorMessage,
errorId: error.errorId ?? "",
httpStatusCode: error.httpStatusCode).convertToString()
httpStatusCode: error.httpStatusCode)
case .serverError(let error):
return MindboxErrorJSON(status: error.status,
errorMessage: error.errorMessage,
errorId: error.errorId ?? "",
httpStatusCode: error.httpStatusCode).convertToString()
httpStatusCode: error.httpStatusCode)
case .internalError(let error):
return MindboxErrorJSON(errorKey: error.errorKey,
errorName: error.reason ?? "",
errorMessage: error.description).convertToString()
errorMessage: error.description)
case .invalidResponse(let response):
if let httpResponse = response as? HTTPURLResponse {
let httpStatusCode = String(httpResponse.statusCode)
let errorMessage = httpResponse.description

return MindboxErrorJSON(httpStatusCode: httpStatusCode,
errorMessage: errorMessage).convertToString()
errorMessage: errorMessage)
} else {
return MindboxErrorJSON(httpStatusCode: "null",
errorMessage: "Connection error").convertToString()
errorMessage: "Connection error")
}
case .connectionError:
return MindboxErrorJSON(httpStatusCode: "null",
errorMessage: "Connection error").convertToString()
errorMessage: "Connection error")
case .unknown(let error):
return MindboxErrorJSON(errorKey: "unknown",
errorName: "",
errorMessage: error.localizedDescription).convertToString()
errorMessage: error.localizedDescription)
}
}
}
Expand Down
127 changes: 105 additions & 22 deletions MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,43 +118,126 @@ struct TransparentViewSyncOperationResponseTests {
}
}

// MARK: - Failure (.connectionError) → .error with createJSON payload
// MARK: - Failure payloads: data contents only, no {type, data} envelope (MOBILE-197)

@Test("Connection failure becomes .error with MindboxError.createJSON payload")
func connectionError_becomesError() {
private func decodedErrorPayload(_ outgoing: BridgeMessage) throws -> [String: Any] {
#expect(outgoing.type == .error)
var jsonString: String?
if case .string(let value) = outgoing.payload { jsonString = value }
let json = try #require(jsonString, "Expected .string payload, got \(String(describing: outgoing.payload))")
let data = try #require(json.data(using: .utf8))
let object = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
#expect(object["type"] == nil, "Payload must not carry the {type, data} envelope")
#expect(object["data"] == nil, "Payload must not carry the {type, data} envelope")
return object
}

@Test("Protocol error payload is the data contents: status, errorMessage, httpStatusCode, errorId")
func protocolError_payloadIsDataContentsOnly() throws {
let pe = ProtocolError(status: .protocolError, errorMessage: "Operation Test not found", httpStatusCode: 400, errorId: "error-id-1")
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.protocolError(pe)),
Comment on lines +135 to +139

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both cases in 7c6e52a: invalidResponse (HTTPURLResponse 403) and unknown (NSError with localized description).

action: action,
id: requestId
)

let payload = try decodedErrorPayload(outgoing)
#expect(payload["status"] as? String == "ProtocolError")
#expect(payload["errorMessage"] as? String == "Operation Test not found")
#expect(payload["httpStatusCode"] as? String == "400")
#expect(payload["errorId"] as? String == "error-id-1")
}

@Test("Server error payload is the data contents with InternalServerError status")
func serverError_payloadIsDataContentsOnly() throws {
let pe = ProtocolError(status: .internalServerError, errorMessage: "Something went wrong", httpStatusCode: 500)
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.serverError(pe)),
action: action,
id: requestId
)

let payload = try decodedErrorPayload(outgoing)
#expect(payload["status"] as? String == "InternalServerError")
#expect(payload["errorMessage"] as? String == "Something went wrong")
#expect(payload["httpStatusCode"] as? String == "500")
}

@Test("Connection failure payload is the data contents without the NetworkError envelope")
func connectionError_payloadIsDataContentsOnly() throws {
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.connectionError),
action: action,
id: requestId
)

#expect(outgoing.type == .error)
if case .string(let value) = outgoing.payload {
#expect(value.contains("NetworkError"), "createJSON for connectionError produces a NetworkError envelope")
#expect(value.contains("Connection error"))
} else {
Issue.record("Expected .string payload")
}
let payload = try decodedErrorPayload(outgoing)
#expect(payload["httpStatusCode"] as? String == "null")
#expect(payload["errorMessage"] as? String == "Connection error")
}

@Test("Validation error payload is the data contents with validationMessages")
func validationError_payloadIsDataContentsOnly() throws {
let ve = ValidationError(
status: .validationError,
validationMessages: [ValidationMessage(message: "Invalid email", location: "/customer/email")]
)
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.validationError(ve)),
action: action,
id: requestId
)

let payload = try decodedErrorPayload(outgoing)
#expect(payload["status"] as? String == "ValidationError")
let messages = try #require(payload["validationMessages"] as? [[String: Any]])
#expect(messages.count == 1)
#expect(messages.first?["message"] as? String == "Invalid email")
#expect(messages.first?["location"] as? String == "/customer/email")
}

// MARK: - Failure (.protocolError) → .error with createJSON payload
@Test("Internal error payload is the data contents with errorKey")
func internalError_payloadIsDataContentsOnly() throws {
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.internalError(InternalError(errorKey: .parsing, reason: "Broken body"))),
action: action,
id: requestId
)

let payload = try decodedErrorPayload(outgoing)
#expect(payload["errorKey"] as? String == "Error_parsing")
#expect(payload["errorName"] as? String == "Broken body")
}

@Test("Invalid response payload is the data contents with httpStatusCode from the response")
func invalidResponse_payloadIsDataContentsOnly() throws {
let url = try #require(URL(string: "https://api.mindbox.ru/v3/operations/sync"))
let httpResponse = try #require(HTTPURLResponse(url: url, statusCode: 403, httpVersion: nil, headerFields: nil))

@Test("Protocol error becomes .error with MindboxError.createJSON payload")
func protocolError_becomesError() {
let pe = ProtocolError(status: .protocolError, errorMessage: "Bad", httpStatusCode: 400)
let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.protocolError(pe)),
result: .failure(.invalidResponse(httpResponse)),
action: action,
id: requestId
)

#expect(outgoing.type == .error)
if case .string(let value) = outgoing.payload {
#expect(value.contains("MindboxError"))
#expect(value.contains("ProtocolError"))
} else {
Issue.record("Expected .string payload")
}
let payload = try decodedErrorPayload(outgoing)
#expect(payload["httpStatusCode"] as? String == "403")
#expect((payload["errorMessage"] as? String)?.isEmpty == false)
}

@Test("Unknown error payload is the data contents with errorKey 'unknown'")
func unknownError_payloadIsDataContentsOnly() throws {
let underlying = NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Something exploded"])

let outgoing = TransparentView.makeSyncOperationResponse(
result: .failure(.unknown(underlying)),
action: action,
id: requestId
)

let payload = try decodedErrorPayload(outgoing)
#expect(payload["errorKey"] as? String == "unknown")
#expect(payload["errorMessage"] as? String == "Something exploded")
}

// MARK: - id and action propagated
Expand Down