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 @@ -37,15 +37,15 @@ struct ComposedCheckoutCommunicationClient: CheckoutCommunicationProtocol {
}

private static func method(_ message: String) -> String? {
guard
let object = try? JSONSerialization.jsonObject(with: Data(message.utf8)) as? [String: Any]
else {
return nil
}
return object["method"] as? String
guard let request = try? JSONDecoder().decode(MethodEnvelope.self, from: Data(message.utf8)) else { return nil }
return request.method
}
}

private struct MethodEnvelope: Decodable {
let method: String
}

struct DefaultClientBinding {
let client: any CheckoutCommunicationProtocol
let policy: DefaultClientPolicy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,27 @@ class CheckoutWebViewTests: XCTestCase {
XCTAssertTrue(MockCheckoutBridge.sendResponseCalled)
}

@MainActor
func testMalformedReadyParamsReturnParseError() async throws {
view.client = MockBridgeClient(responseMessage: "client-response")
let body = #"{"jsonrpc":"2.0","method":"ec.ready","id":"ready-bad","params":{"delegate":[null]}}"#
let responseSent = expectation(description: "response sent")
MockCheckoutBridge.sendResponseExpectation = responseSent
let message = MockScriptMessage(body: body)

view.userContentController(WKUserContentController(), didReceive: message)

await fulfillment(of: [responseSent], timeout: 5.0)

let response = try XCTUnwrap(MockCheckoutBridge.lastResponseBody)
XCTAssertNotEqual(response, "client-response")
let parsed = try XCTUnwrap(try JSONSerialization.jsonObject(with: Data(response.utf8)) as? [String: Any])
XCTAssertEqual(parsed["id"] as? String, "ready-bad")
let error = try XCTUnwrap(parsed["error"] as? [String: Any])
XCTAssertEqual(error["code"] as? Int, -32700)
XCTAssertEqual(error["message"] as? String, "Parse error")
}

@MainActor
func testSupportedRequestUsesRawClientResponse() async {
let id = "req-window-raw"
Expand Down Expand Up @@ -726,17 +747,21 @@ class CheckoutWebViewTests: XCTestCase {
}

@MainActor
func testWindowOpenRequestIgnoresMalformedBody() async {
func testWindowOpenRequestReturnsInvalidParamsForMalformedBody() async throws {
view.client = nil
let notFired = expectation(description: "sendResponse must not fire")
notFired.isInverted = true
MockCheckoutBridge.sendResponseExpectation = notFired
let responseSent = expectation(description: "sendResponse fires")
MockCheckoutBridge.sendResponseExpectation = responseSent
let message = MockScriptMessage(body: #"{"jsonrpc":"2.0","method":"ec.window.open_request","id":"r","params":{}}"#)

view.userContentController(WKUserContentController(), didReceive: message)

await fulfillment(of: [notFired], timeout: 1.0)
XCTAssertFalse(MockCheckoutBridge.sendResponseCalled)
await fulfillment(of: [responseSent], timeout: 1.0)
let response = try XCTUnwrap(MockCheckoutBridge.lastResponseBody)
let parsed = try XCTUnwrap(try JSONSerialization.jsonObject(with: Data(response.utf8)) as? [String: Any])
XCTAssertEqual(parsed["id"] as? String, "r")
let error = try XCTUnwrap(parsed["error"] as? [String: Any])
XCTAssertEqual(error["code"] as? Int, -32602)
XCTAssertEqual(error["message"] as? String, "Invalid params")
}

// MARK: - ec.error severity-based dismissal
Expand Down
96 changes: 96 additions & 0 deletions platforms/swift/api/ShopifyCheckoutProtocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,102 @@
}
]
},
{
"kind": "Var",
"name": "parseErrorCode",
"printedName": "parseErrorCode",
"children": [
{
"kind": "TypeNominal",
"name": "Int",
"printedName": "Swift.Int",
"usr": "s:Si"
}
],
"declKind": "Var",
"usr": "s:23ShopifyCheckoutProtocol0bC0O14parseErrorCodeSivpZ",
"mangledName": "$s23ShopifyCheckoutProtocol0bC0O14parseErrorCodeSivpZ",
"moduleName": "ShopifyCheckoutProtocol",
"static": true,
"isInternal": true,
"declAttributes": [
"HasInitialValue",
"HasStorage"
],
"isLet": true,
"hasStorage": true,
"accessors": [
{
"kind": "Accessor",
"name": "Get",
"printedName": "Get()",
"children": [
{
"kind": "TypeNominal",
"name": "Int",
"printedName": "Swift.Int",
"usr": "s:Si"
}
],
"declKind": "Accessor",
"usr": "s:23ShopifyCheckoutProtocol0bC0O14parseErrorCodeSivgZ",
"mangledName": "$s23ShopifyCheckoutProtocol0bC0O14parseErrorCodeSivgZ",
"moduleName": "ShopifyCheckoutProtocol",
"static": true,
"implicit": true,
"isInternal": true,
"accessorKind": "get"
}
]
},
{
"kind": "Var",
"name": "parseErrorMessage",
"printedName": "parseErrorMessage",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Var",
"usr": "s:23ShopifyCheckoutProtocol0bC0O17parseErrorMessageSSvpZ",
"mangledName": "$s23ShopifyCheckoutProtocol0bC0O17parseErrorMessageSSvpZ",
"moduleName": "ShopifyCheckoutProtocol",
"static": true,
"isInternal": true,
"declAttributes": [
"HasInitialValue",
"HasStorage"
],
"isLet": true,
"hasStorage": true,
"accessors": [
{
"kind": "Accessor",
"name": "Get",
"printedName": "Get()",
"children": [
{
"kind": "TypeNominal",
"name": "String",
"printedName": "Swift.String",
"usr": "s:SS"
}
],
"declKind": "Accessor",
"usr": "s:23ShopifyCheckoutProtocol0bC0O17parseErrorMessageSSvgZ",
"mangledName": "$s23ShopifyCheckoutProtocol0bC0O17parseErrorMessageSSvgZ",
"moduleName": "ShopifyCheckoutProtocol",
"static": true,
"implicit": true,
"isInternal": true,
"accessorKind": "get"
}
]
},
{
"kind": "Var",
"name": "methodNotFoundCode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public enum CheckoutProtocol {
public static let defaultDelegations: [String] = ["window.open"]

package static let readyMethod = "ec.ready"
package static let parseErrorCode = -32700
package static let parseErrorMessage = "Parse error"
package static let methodNotFoundCode = -32601
package static let methodNotFoundMessage = "Method not found"

Expand Down Expand Up @@ -33,59 +35,33 @@ public enum CheckoutProtocol {

package static func supportedProtocolMethod(_ message: String) -> String? {
guard
let object = try? JSONSerialization.jsonObject(with: Data(message.utf8)) as? [String: Any],
object["jsonrpc"] as? String == "2.0",
let method = object["method"] as? String,
supportedProtocolMethods.contains(method)
let envelope = try? JSONDecoder().decode(JSONRPCEnvelope.self, from: Data(message.utf8)),
envelope.jsonrpc == "2.0",
supportedProtocolMethods.contains(envelope.method)
else {
return nil
}

return method
return envelope.method
}

package static func methodNotFoundResponse(forUnsupportedProtocolRequest message: String) -> String? {
guard
let object = try? JSONSerialization.jsonObject(with: Data(message.utf8)) as? [String: Any],
object["jsonrpc"] as? String == "2.0",
let method = object["method"] as? String,
!supportedProtocolMethods.contains(method),
let id = jsonRpcRequestID(object["id"])
let request = try? JSONDecoder().decode(JSONRPCEnvelope.self, from: Data(message.utf8)),
request.jsonrpc == "2.0",
!supportedProtocolMethods.contains(request.method),
let id = request.id
else {
return nil
}

let response: [String: Any] = [
"jsonrpc": "2.0",
"id": id,
"error": [
"code": methodNotFoundCode,
"message": methodNotFoundMessage
]
]

guard
JSONSerialization.isValidJSONObject(response),
let data = try? JSONSerialization.data(withJSONObject: response),
let body = String(data: data, encoding: .utf8)
else {
return nil
}

return body
}

private static func jsonRpcRequestID(_ id: Any?) -> Any? {
switch id {
case let value as String:
return value
case let value as NSNumber:
guard CFGetTypeID(value) != CFBooleanGetTypeID() else {
return nil
}
return value
default:
return nil
}
let response = JSONRPCErrorResponse(
id: id,
error: JSONRPCError(code: methodNotFoundCode, message: methodNotFoundMessage)
)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
guard let data = try? encoder.encode(response) else { return nil }
return String(data: data, encoding: .utf8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ extension CheckoutProtocol {
let accepted = requested.filter(Set(delegations).contains)
return CheckoutProtocol.encodeReadyResponse(id: id, acceptedDelegations: accepted)

case let .error(id, code, message):
return CheckoutProtocol.encodeErrorResponse(id: id, code: code, message: message)

case let .notification(method, payload):
await notificationHandlers[method]?(payload)
return nil
Expand All @@ -70,6 +73,6 @@ extension CheckoutProtocol {

struct DelegationEntry {
let delegation: String
let handler: @MainActor @Sendable (String, Data) async -> String?
let handler: @MainActor @Sendable (JSONRPCID, Data) async -> String?
}
}
Loading
Loading