Skip to content

Commit 3c85b80

Browse files
authored
chore: add AWSQueryCompatible Test Cases (#1950)
1 parent d9c5325 commit 3c85b80

File tree

6 files changed

+282
-11
lines changed

6 files changed

+282
-11
lines changed

IntegrationTests/Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extension Target.Dependency {
2424
static var clientRuntime: Self { .product(name: "ClientRuntime", package: "smithy-swift") }
2525
static var smithyIdentity: Self { .product(name: "SmithyIdentity", package: "smithy-swift") }
2626
static var smithyTestUtil: Self { .product(name: "SmithyTestUtil", package: "smithy-swift") }
27+
static var smithyHttpApi: Self { .product(name: "SmithyHTTPAPI", package: "smithy-swift") }
2728
}
2829

2930
// MARK: - Base Package
@@ -105,6 +106,7 @@ private func integrationTestTarget(_ name: String) -> Target {
105106
.smithyIdentity,
106107
.awsSDKCommon,
107108
.awsIntegrationTestUtils,
109+
.smithyHttpApi,
108110
.product(name: name, package: "aws-sdk-swift")
109111
] + additionalDependencies.map {
110112
Target.Dependency.product(name: $0, package: "aws-sdk-swift", condition: nil)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import AWSDynamoDB
10+
import ClientRuntime
11+
import AWSClientRuntime
12+
import SmithyHTTPAPI
13+
import AWSSDKIdentity
14+
15+
final class QueryCompatibleTest: XCTestCase {
16+
17+
// Test Case 5: Validate SDK does not send x-amzn-query-mode header
18+
// when service doesn't have @awsQueryCompatible trait
19+
20+
func test_QueryCompatible_TC5_NoQueryModeHeaderForNonCompatibleService() async throws {
21+
var capturedHeaders: Headers?
22+
23+
let mockHTTPClient = MockHTTPClient { request in
24+
capturedHeaders = request.headers
25+
26+
// Return DynamoDB error response
27+
let response = HTTPResponse(
28+
body: .data(Data("""
29+
{
30+
"__type": "com.amazon.coral.validate#ValidationException",
31+
"message": "Test validation error"
32+
}
33+
""".utf8)),
34+
statusCode: .badRequest
35+
)
36+
response.headers.add(name: "Content-Type", value: "application/x-amz-json-1.0")
37+
return response
38+
}
39+
40+
let credentials = AWSCredentialIdentity(accessKey: "test", secret: "test")
41+
let resolver = try StaticAWSCredentialIdentityResolver(credentials)
42+
43+
let config = try await DynamoDBClient.Config(
44+
awsCredentialIdentityResolver: resolver,
45+
region: "us-east-1",
46+
httpClientEngine: mockHTTPClient
47+
)
48+
49+
let client = DynamoDBClient(config: config)
50+
51+
do {
52+
_ = try await client.getItem(input: GetItemInput(
53+
key: ["id": .s("test")],
54+
tableName: "TestTable"
55+
))
56+
XCTFail("Expected ValidationException error")
57+
} catch {
58+
// TC5: Verify x-amzn-query-mode header is NOT present in wire request
59+
XCTAssertNotNil(capturedHeaders)
60+
XCTAssertNil(capturedHeaders?.value(for: "x-amzn-query-mode"),
61+
"x-amzn-query-mode header should NOT be present for services without @awsQueryCompatible trait")
62+
}
63+
}
64+
}
65+
66+
// Mock HTTP Client Implementation
67+
68+
private class MockHTTPClient: HTTPClient {
69+
private let handler: (HTTPRequest) async throws -> HTTPResponse
70+
71+
init(handler: @escaping (HTTPRequest) -> HTTPResponse) {
72+
self.handler = { request in
73+
return handler(request)
74+
}
75+
}
76+
77+
func send(request: HTTPRequest) async throws -> HTTPResponse {
78+
return try await handler(request)
79+
}
80+
81+
func close() async throws {
82+
// No-op for mock
83+
}
84+
}
85+
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import AWSSQS
10+
import ClientRuntime
11+
import AWSClientRuntime
12+
import SmithyHTTPAPI
13+
14+
final class QueryCompatibleTests: XCTestCase {
15+
16+
// Test Case 1: Parse Code field from header
17+
18+
func test_QueryCompatible_TC1_ParseCodeFromHeader() async throws {
19+
// Create a mock HTTP client that returns the expected error response
20+
let mockHTTPClient = MockHTTPClient { request in
21+
// Verify the request has the x-amzn-query-mode header (TC4)
22+
XCTAssertEqual(request.headers.value(for: "x-amzn-query-mode"), "true")
23+
24+
// Return error response with x-amzn-query-error header
25+
let response = HTTPResponse(
26+
body: .data(Data("""
27+
{
28+
"__type": "com.amazonaws.sqs#QueueDoesNotExist",
29+
"message": "Some user-visible message"
30+
}
31+
""".utf8)),
32+
statusCode: .badRequest
33+
)
34+
response.headers.add(name: "x-amzn-query-error", value: "AWS.SimpleQueueService.NonExistentQueue;Sender")
35+
response.headers.add(name: "Content-Type", value: "application/x-amz-json-1.0")
36+
return response
37+
}
38+
39+
let config = try await SQSClient.SQSClientConfiguration(
40+
region: "us-west-2",
41+
httpClientEngine: mockHTTPClient
42+
)
43+
let mockClient = SQSClient(config: config)
44+
45+
do {
46+
_ = try await mockClient.getQueueUrl(input: .init(queueName: "non-existent-queue"))
47+
XCTFail("Expected QueueDoesNotExist error")
48+
} catch let error as QueueDoesNotExist {
49+
// TC1: Verify error code is parsed from header
50+
XCTAssertNotNil(error.errorCode, "Error code should not be nil")
51+
XCTAssertEqual(error.errorCode, "AWS.SimpleQueueService.NonExistentQueue",
52+
"Error code should be parsed from x-amzn-query-error header")
53+
}
54+
}
55+
56+
// Test Case 2: Handle missing Code field
57+
58+
func test_QueryCompatible_TC2_HandleMissingCode() async throws {
59+
let mockHTTPClient = MockHTTPClient { request in
60+
// Return error response WITHOUT x-amzn-query-error header
61+
let response = HTTPResponse(
62+
body: .data(Data("""
63+
{
64+
"__type": "com.amazonaws.sqs#QueueDoesNotExist",
65+
"message": "Some user-visible message"
66+
}
67+
""".utf8)),
68+
statusCode: .badRequest
69+
)
70+
response.headers.add(name: "Content-Type", value: "application/x-amz-json-1.0")
71+
return response
72+
}
73+
74+
let config = try await SQSClient.SQSClientConfiguration(
75+
region: "us-west-2",
76+
httpClientEngine: mockHTTPClient
77+
)
78+
let mockClient = SQSClient(config: config)
79+
80+
do {
81+
_ = try await mockClient.getQueueUrl(input: .init(queueName: "non-existent-queue"))
82+
XCTFail("Expected QueueDoesNotExist error")
83+
} catch let error as AWSServiceError {
84+
// TC2: Verify error code falls back to __type field
85+
XCTAssertNotNil(error.errorCode, "Error code should not be nil")
86+
XCTAssertEqual(error.errorCode, "QueueDoesNotExist",
87+
"Error code should be parsed from __type field when header is missing")
88+
}
89+
}
90+
91+
// Test Case 3: Parse Type field
92+
93+
func test_QueryCompatible_TC3_ParseTypeField() async throws {
94+
let mockHTTPClient = MockHTTPClient { request in
95+
let response = HTTPResponse(
96+
body: .data(Data("""
97+
{
98+
"__type": "com.amazonaws.sqs#QueueDoesNotExist",
99+
"message": "Some user-visible message"
100+
}
101+
""".utf8)),
102+
statusCode: .badRequest
103+
)
104+
response.headers.add(name: "x-amzn-query-error", value: "AWS.SimpleQueueService.NonExistentQueue;Sender")
105+
response.headers.add(name: "Content-Type", value: "application/x-amz-json-1.0")
106+
return response
107+
}
108+
109+
let config = try await SQSClient.SQSClientConfiguration(
110+
region: "us-west-2",
111+
httpClientEngine: mockHTTPClient
112+
)
113+
let mockClient = SQSClient(config: config)
114+
115+
do {
116+
_ = try await mockClient.getQueueUrl(input: .init(queueName: "non-existent-queue"))
117+
XCTFail("Expected QueueDoesNotExist error")
118+
} catch let error as HTTPError {
119+
// TC3: Verify Type field can be parsed.
120+
// This is NOT exposed to customers on the error.
121+
let errorDetails = error.httpResponse.headers.value(for: "x-amzn-query-error")
122+
let parsedError = try AwsQueryCompatibleErrorDetails.parse(errorDetails)
123+
XCTAssertEqual(parsedError?.type, "Sender")
124+
}
125+
}
126+
127+
// Test Case 4: Verify x-amzn-query-mode header is sent
128+
129+
func test_QueryCompatible_TC4_SendsQueryModeHeader() async throws {
130+
var capturedHeaders: Headers?
131+
132+
let mockHTTPClient = MockHTTPClient { request in
133+
capturedHeaders = request.headers
134+
135+
// Return successful response
136+
let response = HTTPResponse(
137+
body: .data(Data("""
138+
{
139+
"QueueUrl": "https://sqs.us-west-2.amazonaws.com/123456789012/test-queue"
140+
}
141+
""".utf8)),
142+
statusCode: .ok
143+
)
144+
return response
145+
}
146+
147+
let config = try await SQSClient.SQSClientConfiguration(
148+
region: "us-west-2",
149+
httpClientEngine: mockHTTPClient
150+
)
151+
let mockClient = SQSClient(config: config)
152+
153+
_ = try await mockClient.getQueueUrl(input: .init(queueName: "test-queue"))
154+
155+
// TC4: Verify x-amzn-query-mode header is present and set to "true"
156+
XCTAssertNotNil(capturedHeaders)
157+
XCTAssertEqual(capturedHeaders?.value(for: "x-amzn-query-mode"), "true",
158+
"x-amzn-query-mode header should be present and set to 'true'")
159+
}
160+
}
161+
162+
// Mock HTTP Client Implementation
163+
164+
private class MockHTTPClient: HTTPClient {
165+
private let handler: (HTTPRequest) async throws -> HTTPResponse
166+
167+
init(handler: @escaping (HTTPRequest) -> HTTPResponse) {
168+
self.handler = { request in
169+
return handler(request)
170+
}
171+
}
172+
173+
func send(request: HTTPRequest) async throws -> HTTPResponse {
174+
return try await handler(request)
175+
}
176+
177+
func close() async throws {
178+
// No-op for mock
179+
}
180+
}

Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSJSON/AwsQueryCompatibleErrorDetails.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ public class AwsQueryCompatibleErrorDetails {
1616
self.type = type
1717
}
1818

19-
public static func parse(_ value: String?) throws -> AwsQueryCompatibleErrorDetails {
19+
/**
20+
* Parses the `x-amzn-query-error` header value in format `code;type`.
21+
*
22+
* - Parameter value: The raw header value (e.g., "InvalidParameterValue;Sender")
23+
* - Returns: Parsed error details if valid, `nil` if value is `nil`
24+
* - Throws: `ParseError` if the value is malformed, has empty code, or empty type
25+
*/
26+
public static func parse(_ value: String?) throws -> AwsQueryCompatibleErrorDetails? {
2027
guard let value else {
21-
throw ParseError.missingQueryErrorData
28+
return nil
2229
}
2330
return try parseImpl(value)
2431
}
@@ -31,7 +38,6 @@ public enum ParseError: Error, CustomDebugStringConvertible {
3138
case malformedErrorString
3239
case emptyCode
3340
case emptyType
34-
case missingQueryErrorData
3541

3642
public var debugDescription: String {
3743
switch self {
@@ -41,8 +47,6 @@ public enum ParseError: Error, CustomDebugStringConvertible {
4147
return "code is empty"
4248
case .emptyType:
4349
return "type is empty"
44-
case .missingQueryErrorData:
45-
return "x-amzn-query-error header not found"
4650
}
4751
}
4852
}

Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSQuery/AWSQueryCompatibleUtils.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public enum AWSQueryCompatibleUtils {
2020
noErrorWrapping: Bool,
2121
errorDetails: String?
2222
) throws -> RpcV2CborError {
23-
let errorCode = try AwsQueryCompatibleErrorDetails.parse(errorDetails).code
23+
let errorCode = try AwsQueryCompatibleErrorDetails.parse(errorDetails)?.code
2424
return try RpcV2CborError(
2525
httpResponse: httpResponse,
2626
responseReader: responseReader,
@@ -36,7 +36,7 @@ public enum AWSQueryCompatibleUtils {
3636
noErrorWrapping: Bool,
3737
errorDetails: String?
3838
) throws -> AWSJSONError {
39-
let errorCode = try AwsQueryCompatibleErrorDetails.parse(errorDetails).code
39+
let errorCode = try AwsQueryCompatibleErrorDetails.parse(errorDetails)?.code
4040
return try AWSJSONError(
4141
httpResponse: httpResponse,
4242
responseReader: responseReader,

Sources/Core/AWSClientRuntime/Tests/AWSClientRuntimeTests/Protocols/AWSJSON/AwsQueryCompatibleErrorDetailsTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class AwsQueryCompatibleErrorDetailsTests: XCTestCase {
2727
type: "Sender"
2828
)
2929
let actual = try AwsQueryCompatibleErrorDetails.parse("com.test.ErrorCode;Sender")
30-
XCTAssertEqual(expected.code, actual.code)
31-
XCTAssertEqual(expected.type, actual.type)
30+
XCTAssertEqual(expected.code, actual?.code)
31+
XCTAssertEqual(expected.type, actual?.type)
3232
}
3333

3434
func testParseErrorServer() throws {
@@ -37,7 +37,7 @@ class AwsQueryCompatibleErrorDetailsTests: XCTestCase {
3737
type: "Receiver"
3838
)
3939
let actual = try AwsQueryCompatibleErrorDetails.parse("com.test.ErrorCode;Receiver")
40-
XCTAssertEqual(expected.code, actual.code)
41-
XCTAssertEqual(expected.type, actual.type)
40+
XCTAssertEqual(expected.code, actual?.code)
41+
XCTAssertEqual(expected.type, actual?.type)
4242
}
4343
}

0 commit comments

Comments
 (0)