diff --git a/docs/source/spec/http-protocol-compliance-tests.rst b/docs/source/spec/http-protocol-compliance-tests.rst new file mode 100644 index 00000000000..71e7d91b2bb --- /dev/null +++ b/docs/source/spec/http-protocol-compliance-tests.rst @@ -0,0 +1,535 @@ +.. _http-protocol-compliance-tests: + +============================== +HTTP Protocol Compliance Tests +============================== + +Smithy is a protocol-agnostic IDL that tries to abstract the serialization +format of request and response messages sent between a client and server. +Protocol specifications define the serialization format of a protocol, and +protocol compliance tests help to ensure that implementations correctly +implement a protocol specification. + +.. contents:: Table of contents + :depth: 2 + :local: + :backlinks: none + + +-------- +Overview +-------- + +This specification defines two traits in the ``smithy.test`` namespace that +are used to make assertions about client and server protocol implementations. + +:ref:`smithy.test#httpRequestTests ` + Used to define how an HTTP request is serialized given a specific + protocol, authentication scheme, and set of input parameters. +:ref:`smithy.test#httpResponseTests ` + Used to define how an HTTP response is serialized given a specific + protocol, authentication scheme, and set of output or error parameters. + +Protocol implementation developers use these traits to ensure that their +implementation is correct. This can be done through code generation of test +cases or by dynamically loading test cases at runtime. For example, a Java +implementation could generate JUnit test cases to assert that the +expectations defined in a model match the behavior of a generated client +or server. + + +Parameter format +================ + +The ``params`` property used in both the ``httpRequestTests`` trait and +``httpResponseTests`` trait test cases represents parameters that are used +to serialize HTTP requests and responses. In order to compare implementation +specific results against the expected result of each test case across +different programming languages, parameters are defined in the same format +specified in :ref:`trait-definition-values` with the following additional +constraints: + +* Timestamp values must be converted to a Unix timestamp represented + as an integer. +* Client implementations that automatically provide values for members marked + with the :ref:`idempotencyToken-trait` MUST use a constant value of + ``00000000-0000-4000-8000-000000000000``. + + +.. _httpRequestTests-trait: + +---------------- +httpRequestTests +---------------- + +Summary + The ``httpRequestTests`` trait is used to define how an HTTP request is + serialized given a specific protocol, authentication scheme, and set of + input parameters. +Trait selector + .. code-block:: css + + operation +Value type + [``HttpRequestTestCase``] + +The ``httpRequestTests`` trait is a list of ``HttpRequestTestCase`` objects +that support the following properties: + +.. list-table:: + :header-rows: 1 + :widths: 10 25 65 + + * - Property + - Type + - Description + * - id + - ``string`` + - **Required**. The identifier of the test case. This identifier can + be used by protocol test implementations to filter out unsupported + test cases by ID, to generate test case names, etc. The provided + ``id`` MUST match Smithy's :token:`identifier` ABNF. No two + ``httpRequestTests`` test cases can share the same ID. + * - protocol + - ``string`` + - **Required**. The name of the :ref:`protocol ` to + test. Because Smithy services can support multiple protocols, each + test MUST specify which protocol is under test. + * - method + - ``string`` + - **Required**. The expected serialized HTTP request method. + * - uri + - ``string`` + - **Required**. The request-target of the HTTP request, not including + the query string (for example, "/foo/bar"). + * - authScheme + - ``string`` + - The optional :ref:`authentication scheme ` to assume. + It's possible that specific authentication schemes might influence + the serialization logic of an HTTP request. + * - queryParams + - ``Map`` + - A map of expected query string parameters. + + A serialized HTTP request is not in compliance with the protocol + if any query string parameter defined in ``queryParams`` is not + defined in the request or if the value of a query string parameter + in the request differs from the expected value. + + Each key represents the query string parameter name, and each + value represents the query string parameter value. Both keys and + values MUST appear in the format in which it is expected to be + sent over the wire; if a key or value needs to be percent-encoded, + then it MUST appear percent-encoded in this map. + + ``queryParams`` applies no constraints on additional query parameters. + * - forbidQueryParams + - [``string``] + - A list of query string parameter names that must not appear in the + serialized HTTP request. + + Each value MUST appear in the format in which it is sent over the + wire; if a key needs to be percent-encoded, then it MUST appear + percent-encoded in this list. + * - requireQueryParams + - [``string``] + - A list of query string parameter names that MUST appear in the + serialized request URI, but no assertion is made on the value. + + Each value MUST appear in the format in which it is sent over the + wire; if a key needs to be percent-encoded, then it MUST appear + percent-encoded in this list. + * - headers + - ``Map`` + - A map of expected HTTP headers. Each key represents a header field + name and each value represents the expected header value. An HTTP + request is not in compliance with the protocol if any listed header + is missing from the serialized request or if the expected header + value differs from the serialized request value. + + ``headers`` applies no constraints on additional headers. + * - forbidHeaders + - [``string``] + - A list of header field names that must not appear in the serialized + HTTP request. + * - requireHeaders + - [``string``] + - A list of header field names that must appear in the serialized + HTTP message, but no assertion is made on the value. Headers listed + in ``headers`` do not need to appear in this list. + * - body + - ``string`` + - The expected HTTP message body. If no request body is defined, + then no assertions are made about the body of the message. + * - bodyMediaType + - ``string`` + - The media type of the ``body``. This is used to help test runners + to parse and validate the expected data against generated data. + Binary media type formats require that the contents of ``body`` are + base64 encoded. + * - params + - ``document`` + - Defines the input parameters used to generate the HTTP request. These + parameters MUST be compatible with the input of the operation. + * - vendorParams + - ``document`` + - Defines vendor-specific parameters that are used to influence the + request. For example, some vendors might utilize environment + variables, configuration files on disk, or other means to influence + the serialization formats used by clients or servers. + * - documentation + - ``string`` + - A description of the test and what is being asserted defined in + CommonMark_. + + +HTTP request example +==================== + +The following example defines a protocol compliance test for a JSON protocol +that uses :ref:`HTTP binding traits `. + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use smithy.test#httpRequestTests + + @http(method: "POST", uri: "/") + @httpRequestTests([ + { + id: "say_hello", + protocol: "example", + params: { + "greeting": "Hi", + "name": "Teddy" + }, + method: "POST", + uri: "/", + headers: { + "X-Greeting": "Hi", + }, + body: "{\"name\": \"Teddy\"}", + bodyMediaType: "application/json" + } + ]) + operation SayHello(SayHelloInput) + + structure SayHelloInput { + @httpHeader("X-Greeting") + greeting: String, + + name: String + } + + .. code-tab:: json + + { + "smithy": "0.5.0", + "shapes": { + "smithy.example#SayHello": { + "type": "operation", + "input": { + "target": "smithy.example#SayHelloInput" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/", + "code": 200 + }, + "smithy.test#httpRequestTests": [ + { + "id": "say_hello", + "protocol": "example", + "headers": { + "X-Greeting": "Hi" + }, + "body": "{\"name\": \"Teddy\"}", + "bodyMediaType": "application/json" + "params": { + "greeting": "Hi", + "name": "Teddy" + }, + "method": "POST", + "uri": "/" + } + ] + } + }, + "smithy.example#SayHelloInput": { + "type": "structure", + "members": { + "greeting": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpHeader": "X-Greeting" + } + }, + "name": { + "target": "smithy.api#String" + } + } + } + } + } + + +.. _httpResponseTests-trait: + +----------------- +httpResponseTests +----------------- + +Summary + The ``httpResponseTests`` trait is used to define how an HTTP response + is serialized given a specific protocol, authentication scheme, and set + of output or error parameters. +Trait selector + .. code-block:: css + + :each(operation, structure[trait|error]) +Value type + [``HttpResponseTestCase``] + +The ``httpResponseTests`` trait is a list of ``HttpResponseTestCase`` objects +that support the following properties: + +.. list-table:: + :header-rows: 1 + :widths: 10 25 65 + + * - Property + - Type + - Description + * - id + - ``string`` + - **Required**. The identifier of the test case. This identifier can + be used by protocol test implementations to filter out unsupported + test cases by ID, to generate test case names, etc. The provided + ``id`` MUST match Smithy's :token:`identifier` ABNF. No two + ``httpResponseTests`` test cases can share the same ID. + * - protocol + - ``string`` + - **Required**. The name of the :ref:`protocol ` to + test. Because Smithy services can support multiple protocols, each + test MUST specify which protocol is under test. + * - code + - ``integer`` + - **Required**. The expected HTTP response status code. + * - authScheme + - ``string`` + - The optional :ref:`authentication scheme ` to assume. + It's possible that specific authentication schemes might influence + the serialization logic of an HTTP response. + * - headers + - ``Map`` + - A map of expected HTTP headers. Each key represents a header field + name and each value represents the expected header value. An HTTP + response is not in compliance with the protocol if any listed header + is missing from the serialized response or if the expected header + value differs from the serialized response value. + + ``headers`` applies no constraints on additional headers. + * - forbidHeaders + - [``string``] + - A list of header field names that must not appear in the serialized + HTTP response. + * - requireHeaders + - [``string``] + - A list of header field names that must appear in the serialized + HTTP response, but no assertion is made on the value. Headers listed + in ``headers`` do not need to appear in this list. + * - body + - ``string`` + - The expected HTTP message body. If no response body is defined, + then no assertions are made about the body of the message. + * - bodyMediaType + - ``string`` + - The media type of the ``body``. This is used to help test runners + to parse and validate the expected data against generated data. + Binary media type formats require that the contents of ``body`` are + base64 encoded. + * - params + - ``document`` + - Defines the output or error parameters used to generate the HTTP + response. These parameters MUST be compatible with the targeted + operation's output or the targeted error structure. + * - vendorParams + - ``document`` + - Defines vendor-specific parameters that are used to influence the + response. For example, some vendors might utilize environment + variables, configuration files on disk, or other means to influence + the serialization formats used by clients or servers. + * - documentation + - ``string`` + - A description of the test and what is being asserted defined in + CommonMark_. + + +HTTP response example +===================== + +The following example defines a protocol compliance test for a JSON protocol +that uses :ref:`HTTP binding traits `. + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use smithy.test#httpResponseTests + + @http(method: "POST", uri: "/") + @httpResponseTests([ + { + id: "say_goodbye", + protocol: "example", + params: {farewell: "Bye"}, + code: 200, + headers: { + "X-Farewell": "Bye", + "Content-Length": "0" + } + } + ]) + operation SayGoodbye() -> SayGoodbyeOutput + + structure SayGoodbyeOutput { + @httpHeader("X-Farewell") + farewell: String, + } + + .. code-tab:: json + + { + "smithy": "0.5.0", + "shapes": { + "smithy.example#SayGoodbye": { + "type": "operation", + "output": { + "target": "smithy.example#SayGoodbyeOutput" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/", + "code": 200 + }, + "smithy.test#httpResponseTests": [ + { + "id": "say_goodbye", + "protocol": "example", + "headers": { + "Content-Length": "0", + "X-Farewell": "Bye" + }, + "params": { + "farewell": "Bye" + }, + "code": 200 + } + ] + } + }, + "smithy.example#SayGoodbyeOutput": { + "type": "structure", + "members": { + "farewell": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpHeader": "X-Farewell" + } + } + } + } + } + } + + +HTTP error response example +=========================== + +The ``httpResponseTests`` trait can be applied to error structures to define +how an error HTTP response is serialized. Client protocol compliance test +implementations SHOULD ensure that each error with the ``httpResponseTests`` +trait associated with an operation can be properly deserialized. + +The following example defines a protocol compliance test for a JSON protocol +that uses :ref:`HTTP binding traits `. + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use smithy.test#httpResponseTests + + @error("client") + @httpError(400) + @httpResponseTests([ + { + id: "invalid_greeting", + protocol: "example", + params: {foo: "baz", message: "Hi"}, + code: 400, + headers: {"X-Foo": "baz"}, + body: "{\"message\": \"Hi\"}", + bodyMediaType: "application/json", + } + ]) + structure InvalidGreeting { + @httpHeader("X-Foo") + foo: String, + + message: String, + } + + .. code-tab:: json + + { + "smithy": "0.5.0", + "shapes": { + "smithy.example#InvalidGreeting": { + "type": "structure", + "members": { + "foo": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpHeader": "X-Foo" + } + }, + "message": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#error": "client", + "smithy.api#httpError": 400, + "smithy.test#httpResponseTests": [ + { + "id": "invalid_greeting", + "protocol": "example", + "body": "{\"message\": \"Hi\"}", + "bodyMediaType": "application/json", + "headers": { + "X-Foo": "baz" + }, + "params": { + "foo": "baz", + "message": "Hi" + }, + "code": 400 + } + ] + } + } + } + } + + +.. _CommonMark: https://spec.commonmark.org/ diff --git a/docs/source/spec/index.rst b/docs/source/spec/index.rst index c26d8809ebe..98f15355468 100644 --- a/docs/source/spec/index.rst +++ b/docs/source/spec/index.rst @@ -56,6 +56,7 @@ Additional specifications validation mqtt + http-protocol-compliance-tests *Additional specifications* define additional functionality and enhancements. @@ -67,6 +68,9 @@ enhancements. - Defines how to configure custom validation. * - :doc:`mqtt` - Defines how to bind models to MQTT. + * - :doc:`http-protocol-compliance-tests` + - Defines traits used to validate HTTP-based + client and server protocol implementations. ------------------ diff --git a/settings.gradle.kts b/settings.gradle.kts index bef42d5229a..3bc6649e91b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ include(":smithy-mqtt-traits") include(":smithy-jsonschema") include(":smithy-openapi") include(":smithy-utils") +include(":smithy-protocol-test-traits") project(":smithy-aws-traits").projectDir = file("aws/smithy-aws-traits") project(":smithy-aws-apigateway-openapi").projectDir = file("aws/smithy-aws-apigateway-openapi") diff --git a/smithy-protocol-test-traits/build.gradle.kts b/smithy-protocol-test-traits/build.gradle.kts new file mode 100644 index 00000000000..a8c46c52dd7 --- /dev/null +++ b/smithy-protocol-test-traits/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +description = "Defines protocol test traits." +extra["displayName"] = "Smithy :: Protocol Test Traits" +extra["moduleName"] = "software.amazon.smithy.protocoltest.traits" + +dependencies { + api(project(":smithy-model")) +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpMessageTestCase.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpMessageTestCase.java new file mode 100644 index 00000000000..df885e3dfc3 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpMessageTestCase.java @@ -0,0 +1,295 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; + +public abstract class HttpMessageTestCase implements ToNode { + + private static final String ID = "id"; + private static final String PROTOCOL = "protocol"; + private static final String DOCUMENTATION = "documentation"; + private static final String AUTH_SCHEME = "authScheme"; + private static final String BODY = "body"; + private static final String BODY_MEDIA_TYPE = "bodyMediaType"; + private static final String PARAMS = "params"; + private static final String VENDOR_PARAMS = "vendorParams"; + private static final String HEADERS = "headers"; + private static final String FORBID_HEADERS = "forbidHeaders"; + private static final String REQUIRE_HEADERS = "requireHeaders"; + + private final String id; + private final String documentation; + private final String protocol; + private final String authScheme; + private final String body; + private final String bodyMediaType; + private final ObjectNode params; + private final ObjectNode vendorParams; + private final Map headers; + private final List forbidHeaders; + private final List requireHeaders; + + HttpMessageTestCase(Builder builder) { + id = SmithyBuilder.requiredState(ID, builder.id); + protocol = SmithyBuilder.requiredState(PROTOCOL, builder.protocol); + documentation = builder.documentation; + authScheme = builder.authScheme; + body = builder.body; + bodyMediaType = builder.bodyMediaType; + params = builder.params; + vendorParams = builder.vendorParams; + headers = Collections.unmodifiableMap(new TreeMap<>(builder.headers)); + forbidHeaders = ListUtils.copyOf(builder.forbidHeaders); + requireHeaders = ListUtils.copyOf(builder.requireHeaders); + } + + public String getId() { + return id; + } + + public Optional getDocumentation() { + return Optional.ofNullable(documentation); + } + + public String getProtocol() { + return protocol; + } + + public Optional getAuthScheme() { + return Optional.ofNullable(authScheme); + } + + public Optional getBody() { + return Optional.ofNullable(body); + } + + public Optional getBodyMediaType() { + return Optional.ofNullable(bodyMediaType); + } + + public ObjectNode getParams() { + return params; + } + + public ObjectNode getVendorParams() { + return vendorParams; + } + + public Map getHeaders() { + return headers; + } + + public List getForbidHeaders() { + return forbidHeaders; + } + + public List getRequireHeaders() { + return requireHeaders; + } + + static void updateBuilderFromNode(Builder builder, Node node) { + ObjectNode o = node.expectObjectNode(); + builder.id(o.expectStringMember(ID).getValue()); + builder.protocol(o.expectStringMember(PROTOCOL).getValue()); + o.getStringMember(DOCUMENTATION).map(StringNode::getValue).ifPresent(builder::documentation); + o.getStringMember(AUTH_SCHEME).map(StringNode::getValue).ifPresent(builder::authScheme); + o.getStringMember(BODY).map(StringNode::getValue).ifPresent(builder::body); + o.getStringMember(BODY_MEDIA_TYPE).map(StringNode::getValue).ifPresent(builder::bodyMediaType); + o.getObjectMember(PARAMS).ifPresent(builder::params); + o.getObjectMember(VENDOR_PARAMS).ifPresent(builder::vendorParams); + + o.getObjectMember(HEADERS).ifPresent(headers -> { + headers.getStringMap().forEach((k, v) -> { + builder.putHeader(k, v.expectStringNode().getValue()); + }); + }); + + o.getArrayMember(FORBID_HEADERS).ifPresent(headers -> { + builder.forbidHeaders(headers.getElementsAs(StringNode::getValue)); + }); + + o.getArrayMember(REQUIRE_HEADERS).ifPresent(headers -> { + builder.requireHeaders(headers.getElementsAs(StringNode::getValue)); + }); + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = Node.objectNodeBuilder() + .withMember(ID, getId()) + .withMember(PROTOCOL, getProtocol()) + .withOptionalMember(DOCUMENTATION, getDocumentation().map(Node::from)) + .withOptionalMember(AUTH_SCHEME, getAuthScheme().map(Node::from)) + .withOptionalMember(BODY, getBody().map(Node::from)) + .withOptionalMember(BODY_MEDIA_TYPE, getBodyMediaType().map(Node::from)); + + if (!headers.isEmpty()) { + builder.withMember(HEADERS, ObjectNode.fromStringMap(getHeaders())); + } + + if (!forbidHeaders.isEmpty()) { + builder.withMember(FORBID_HEADERS, ArrayNode.fromStrings(forbidHeaders)); + } + + if (!requireHeaders.isEmpty()) { + builder.withMember(REQUIRE_HEADERS, ArrayNode.fromStrings(requireHeaders)); + } + + if (!params.isEmpty()) { + builder.withMember(PARAMS, getParams()); + } + + if (!vendorParams.isEmpty()) { + builder.withMember(VENDOR_PARAMS, getVendorParams()); + } + + return builder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || o.getClass() != getClass()) { + return false; + } else { + return toNode().equals(((HttpMessageTestCase) o).toNode()); + } + } + + @Override + public int hashCode() { + return toNode().hashCode(); + } + + void updateBuilder(Builder builder) { + builder + .id(id) + .headers(headers) + .forbidHeaders(forbidHeaders) + .requireHeaders(requireHeaders) + .params(params) + .vendorParams(vendorParams) + .documentation(documentation) + .authScheme(authScheme) + .protocol(protocol) + .body(body) + .bodyMediaType(bodyMediaType); + } + + abstract static class Builder implements SmithyBuilder { + + private String id; + private String documentation; + private String protocol; + private String authScheme; + private String body; + private String bodyMediaType; + private ObjectNode params = Node.objectNode(); + private ObjectNode vendorParams = Node.objectNode(); + private final Map headers = new TreeMap<>(); + private final List forbidHeaders = new ArrayList<>(); + private final List requireHeaders = new ArrayList<>(); + + @SuppressWarnings("unchecked") + public B id(String id) { + this.id = id; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B documentation(String documentation) { + this.documentation = documentation; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B protocol(String protocol) { + this.protocol = protocol; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B authScheme(String authScheme) { + this.authScheme = authScheme; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B body(String body) { + this.body = body; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B bodyMediaType(String bodyMediaType) { + this.bodyMediaType = bodyMediaType; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B params(ObjectNode params) { + this.params = params; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B vendorParams(ObjectNode vendorParams) { + this.vendorParams = vendorParams; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B headers(Map headers) { + this.headers.clear(); + this.headers.putAll(headers); + return (B) this; + } + + @SuppressWarnings("unchecked") + public B putHeader(String key, String value) { + headers.put(key, value); + return (B) this; + } + + @SuppressWarnings("unchecked") + public B forbidHeaders(List forbidHeaders) { + this.forbidHeaders.clear(); + this.forbidHeaders.addAll(forbidHeaders); + return (B) this; + } + + @SuppressWarnings("unchecked") + public B requireHeaders(List requireHeaders) { + this.requireHeaders.clear(); + this.requireHeaders.addAll(requireHeaders); + return (B) this; + } + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestCase.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestCase.java new file mode 100644 index 00000000000..0a9322f0149 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestCase.java @@ -0,0 +1,181 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Defines a test case for an HTTP request. + */ +public final class HttpRequestTestCase extends HttpMessageTestCase implements ToSmithyBuilder { + + private static final String METHOD = "method"; + private static final String URI = "uri"; + private static final String QUERY_PARAMS = "queryParams"; + private static final String FORBID_QUERY_PARAMS = "forbidQueryParams"; + private static final String REQUIRE_QUERY_PARAMS = "requireQueryParams"; + + private final String method; + private final String uri; + private final Map queryParams; + private final List forbidQueryParams; + private final List requireQueryParams; + + private HttpRequestTestCase(Builder builder) { + super(builder); + method = SmithyBuilder.requiredState(METHOD, builder.method); + uri = SmithyBuilder.requiredState(URI, builder.uri); + queryParams = Collections.unmodifiableMap(new LinkedHashMap<>(builder.queryParams)); + forbidQueryParams = ListUtils.copyOf(builder.forbidQueryParams); + requireQueryParams = ListUtils.copyOf(builder.requireQueryParams); + } + + public String getMethod() { + return method; + } + + public String getUri() { + return uri; + } + + public Map getQueryParams() { + return queryParams; + } + + public List getForbidQueryParams() { + return forbidQueryParams; + } + + public List getRequireQueryParams() { + return requireQueryParams; + } + + public static HttpRequestTestCase fromNode(Node node) { + HttpRequestTestCase.Builder builder = builder(); + updateBuilderFromNode(builder, node); + ObjectNode o = node.expectObjectNode(); + builder.method(o.expectStringMember(METHOD).getValue()); + builder.uri(o.expectStringMember(URI).getValue()); + o.getObjectMember(QUERY_PARAMS).ifPresent(headers -> { + headers.getStringMap().forEach((k, v) -> { + builder.putQueryParam(k, v.expectStringNode().getValue()); + }); + }); + o.getArrayMember(FORBID_QUERY_PARAMS).ifPresent(params -> { + builder.forbidQueryParams(params.getElementsAs(StringNode::getValue)); + }); + o.getArrayMember(REQUIRE_QUERY_PARAMS).ifPresent(params -> { + builder.requireQueryParams(params.getElementsAs(StringNode::getValue)); + }); + return builder.build(); + } + + @Override + public Node toNode() { + ObjectNode.Builder node = super.toNode().expectObjectNode().toBuilder(); + node.withMember(METHOD, getMethod()); + node.withMember(URI, getUri()); + if (!queryParams.isEmpty()) { + node.withMember(QUERY_PARAMS, ObjectNode.fromStringMap(getQueryParams())); + } + if (!forbidQueryParams.isEmpty()) { + node.withMember(FORBID_QUERY_PARAMS, ArrayNode.fromStrings(getForbidQueryParams())); + } + if (!requireQueryParams.isEmpty()) { + node.withMember(REQUIRE_QUERY_PARAMS, ArrayNode.fromStrings(getRequireQueryParams())); + } + return node.build(); + } + + @Override + public Builder toBuilder() { + Builder builder = builder() + .method(getMethod()) + .uri(getUri()) + .queryParams(getQueryParams()) + .forbidQueryParams(getForbidQueryParams()) + .requireQueryParams(getRequireQueryParams()); + updateBuilder(builder); + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Builder used to create a HttpRequestTestsTrait. + */ + public static final class Builder extends HttpMessageTestCase.Builder { + + private String method; + private String uri; + private final Map queryParams = new LinkedHashMap<>(); + private final List forbidQueryParams = new ArrayList<>(); + private final List requireQueryParams = new ArrayList<>(); + + private Builder() {} + + public Builder method(String method) { + this.method = method; + return this; + } + + public Builder uri(String uri) { + this.uri = uri; + return this; + } + + public Builder queryParams(Map queryParams) { + this.queryParams.clear(); + this.queryParams.putAll(queryParams); + return this; + } + + public Builder putQueryParam(String key, String value) { + queryParams.put(key, value); + return this; + } + + public Builder forbidQueryParams(List forbidQueryParams) { + this.forbidQueryParams.clear(); + this.forbidQueryParams.addAll(forbidQueryParams); + return this; + } + + public Builder requireQueryParams(List requireQueryParams) { + this.requireQueryParams.clear(); + this.requireQueryParams.addAll(requireQueryParams); + return this; + } + + @Override + public HttpRequestTestCase build() { + return new HttpRequestTestCase(this); + } + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsInputValidator.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsInputValidator.java new file mode 100644 index 00000000000..65b3f8851b0 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsInputValidator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.List; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Ensures that input parameters of protocol request test cases are + * valid for the attached operation. + */ +public final class HttpRequestTestsInputValidator extends ProtocolTestCaseValidator { + + public HttpRequestTestsInputValidator() { + super(HttpRequestTestsTrait.ID, HttpRequestTestsTrait.class, "input"); + } + + @Override + StructureShape getStructure(Shape shape, OperationIndex operationIndex) { + return operationIndex.getInput(shape).orElse(null); + } + + @Override + List getTestCases(HttpRequestTestsTrait trait) { + return trait.getTestCases(); + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsTrait.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsTrait.java new file mode 100644 index 00000000000..a2eb539320e --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpRequestTestsTrait.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.List; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; + +/** + * Defines HTTP request protocol tests. + */ +public final class HttpRequestTestsTrait extends AbstractTrait { + public static final ShapeId ID = ShapeId.from("smithy.test#httpRequestTests"); + + private final List testCases; + + public HttpRequestTestsTrait(List testCases) { + this(SourceLocation.NONE, testCases); + } + + public HttpRequestTestsTrait(SourceLocation sourceLocation, List testCases) { + super(ID, sourceLocation); + this.testCases = ListUtils.copyOf(testCases); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + ArrayNode values = value.expectArrayNode(); + List testCases = values.getElementsAs(HttpRequestTestCase::fromNode); + return new HttpRequestTestsTrait(value.getSourceLocation(), testCases); + } + } + + public List getTestCases() { + return testCases; + } + + @Override + protected Node createNode() { + return getTestCases().stream().collect(ArrayNode.collect()); + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestCase.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestCase.java new file mode 100644 index 00000000000..39ee10a25cc --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestCase.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Defines a test case for an HTTP response. + */ +public final class HttpResponseTestCase extends HttpMessageTestCase implements ToSmithyBuilder { + + private static final String CODE = "code"; + private final int code; + + private HttpResponseTestCase(Builder builder) { + super(builder); + code = SmithyBuilder.requiredState(CODE, builder.code); + } + + public int getCode() { + return code; + } + + static HttpResponseTestCase fromNode(Node node) { + HttpResponseTestCase.Builder builder = builder(); + ObjectNode o = node.expectObjectNode(); + builder.code(o.expectNumberMember(CODE).getValue().intValue()); + updateBuilderFromNode(builder, node); + return builder.build(); + } + + @Override + public Node toNode() { + return super.toNode().expectObjectNode().withMember(CODE, getCode()); + } + + @Override + public Builder toBuilder() { + Builder builder = builder().code(getCode()); + updateBuilder(builder); + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Builder used to create a HttpResponseTestCase. + */ + public static final class Builder extends HttpMessageTestCase.Builder { + + private Integer code; + + private Builder() {} + + public Builder code(Integer code) { + this.code = code; + return this; + } + + @Override + public HttpResponseTestCase build() { + return new HttpResponseTestCase(this); + } + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsErrorValidator.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsErrorValidator.java new file mode 100644 index 00000000000..7f976afea8e --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsErrorValidator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Validates that protocol tests on errors use the correct params. + */ +public final class HttpResponseTestsErrorValidator extends HttpResponseTestsOutputValidator { + + public HttpResponseTestsErrorValidator() { + super("error"); + } + + @Override + StructureShape getStructure(Shape shape, OperationIndex operationIndex) { + return shape.asStructureShape().orElse(null); + } + + @Override + boolean isValidatedBy(Shape shape) { + return shape instanceof StructureShape; + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsOutputValidator.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsOutputValidator.java new file mode 100644 index 00000000000..69338bd7f18 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsOutputValidator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.List; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Validates that protocol tests on output use the correct params. + */ +public class HttpResponseTestsOutputValidator extends ProtocolTestCaseValidator { + + public HttpResponseTestsOutputValidator() { + this("output"); + } + + HttpResponseTestsOutputValidator(String descriptor) { + super(HttpResponseTestsTrait.ID, HttpResponseTestsTrait.class, descriptor); + } + + @Override + StructureShape getStructure(Shape shape, OperationIndex operationIndex) { + return operationIndex.getOutput(shape).orElse(null); + } + + @Override + final List getTestCases(HttpResponseTestsTrait trait) { + return trait.getTestCases(); + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsTrait.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsTrait.java new file mode 100644 index 00000000000..e81898395ef --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/HttpResponseTestsTrait.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.List; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; + +/** + * Defines HTTP request protocol tests. + */ +public final class HttpResponseTestsTrait extends AbstractTrait { + public static final ShapeId ID = ShapeId.from("smithy.test#httpResponseTests"); + + private final List testCases; + + public HttpResponseTestsTrait(List testCases) { + this(SourceLocation.NONE, testCases); + } + + public HttpResponseTestsTrait(SourceLocation sourceLocation, List testCases) { + super(ID, sourceLocation); + this.testCases = ListUtils.copyOf(testCases); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + ArrayNode values = value.expectArrayNode(); + List testCases = values.getElementsAs(HttpResponseTestCase::fromNode); + return new HttpResponseTestsTrait(value.getSourceLocation(), testCases); + } + } + + public List getTestCases() { + return testCases; + } + + @Override + protected Node createNode() { + return getTestCases().stream().collect(ArrayNode.collect()); + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/ProtocolTestCaseValidator.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/ProtocolTestCaseValidator.java new file mode 100644 index 00000000000..bb16f738dfe --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/ProtocolTestCaseValidator.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.NodeValidationVisitor; +import software.amazon.smithy.model.validation.ValidationEvent; + +abstract class ProtocolTestCaseValidator extends AbstractValidator { + + private final Class traitClass; + private final ShapeId traitId; + private final String descriptor; + + ProtocolTestCaseValidator(ShapeId traitId, Class traitClass, String descriptor) { + this.traitId = traitId; + this.traitClass = traitClass; + this.descriptor = descriptor; + } + + @Override + public List validate(Model model) { + OperationIndex operationIndex = model.getKnowledge(OperationIndex.class); + + return Stream.concat(model.shapes(OperationShape.class), model.shapes(StructureShape.class)) + .flatMap(operation -> Trait.flatMapStream(operation, traitClass)) + .flatMap(pair -> validateOperation(model, operationIndex, pair.left, pair.right).stream()) + .collect(Collectors.toList()); + } + + abstract StructureShape getStructure(Shape shape, OperationIndex operationIndex); + + abstract List getTestCases(T trait); + + boolean isValidatedBy(Shape shape) { + return shape instanceof OperationShape; + } + + private List validateOperation( + Model model, + OperationIndex operationIndex, + Shape shape, + T trait + ) { + List events = new ArrayList<>(); + List testCases = getTestCases(trait); + + for (int i = 0; i < testCases.size(); i++) { + HttpMessageTestCase testCase = testCases.get(i); + StructureShape struct = getStructure(shape, operationIndex); + if (struct != null) { + NodeValidationVisitor validator = createVisitor(testCase.getParams(), model, shape, i); + events.addAll(struct.accept(validator)); + } else if (!testCase.getParams().isEmpty() && isValidatedBy(shape)) { + events.add(error(shape, trait, String.format( + "Protocol test %s parameters provided for operation with no %s: `%s`", + descriptor, descriptor, Node.printJson(testCase.getParams())))); + } + } + + return events; + } + + private NodeValidationVisitor createVisitor(ObjectNode value, Model model, Shape shape, int position) { + return NodeValidationVisitor.builder() + .model(model) + .eventShapeId(shape.getId()) + .value(value) + .startingContext(traitId + "." + position + ".params") + .eventId(getName()) + .build(); + } +} diff --git a/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/UniqueProtocolTestCaseIdValidator.java b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/UniqueProtocolTestCaseIdValidator.java new file mode 100644 index 00000000000..ea6a8b0b50a --- /dev/null +++ b/smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/UniqueProtocolTestCaseIdValidator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package software.amazon.smithy.protocoltests.traits; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationUtils; + +/** + * Validates that the "id" property of {@code smithy.test#httpRequestTests} + * and {@code smithy.test#httpResponseTests} are unique across all test + * cases. + */ +public class UniqueProtocolTestCaseIdValidator extends AbstractValidator { + + @Override + public List validate(Model model) { + Map> requestIdsToTraits = new TreeMap<>(); + Map> responseIdsToTraits = new TreeMap<>(); + + Stream.concat(model.shapes(OperationShape.class), model.shapes(StructureShape.class)).forEach(shape -> { + shape.getTrait(HttpRequestTestsTrait.class) + .ifPresent(trait -> addTestCaseIdsToMap(shape, trait.getTestCases(), requestIdsToTraits)); + shape.getTrait(HttpResponseTestsTrait.class) + .ifPresent(trait -> addTestCaseIdsToMap(shape, trait.getTestCases(), responseIdsToTraits)); + }); + + removeEntriesWithSingleValue(requestIdsToTraits); + removeEntriesWithSingleValue(responseIdsToTraits); + + return collectEvents(requestIdsToTraits, responseIdsToTraits); + } + + private void addTestCaseIdsToMap( + Shape shape, + List testCases, + Map> map + ) { + for (HttpMessageTestCase testCase : testCases) { + map.computeIfAbsent(testCase.getId(), id -> new ArrayList<>()).add(shape); + } + } + + private void removeEntriesWithSingleValue(Map> map) { + map.keySet().removeIf(key -> map.get(key).size() == 1); + } + + private List collectEvents( + Map> requestIdsToTraits, + Map> responseIdsToTraits + ) { + if (requestIdsToTraits.isEmpty() && responseIdsToTraits.isEmpty()) { + return Collections.emptyList(); + } + + List mutableEvents = new ArrayList<>(); + addValidationEvents(requestIdsToTraits, mutableEvents, HttpRequestTestsTrait.ID); + addValidationEvents(responseIdsToTraits, mutableEvents, HttpResponseTestsTrait.ID); + return mutableEvents; + } + + private void addValidationEvents( + Map> conflicts, + List mutableEvents, + ShapeId trait + ) { + for (Map.Entry> entry : conflicts.entrySet()) { + for (Shape shape : entry.getValue()) { + mutableEvents.add(error(shape, String.format( + "Conflicting `%s` test case IDs found for ID `%s`: %s", + trait, entry.getKey(), + ValidationUtils.tickedList(entry.getValue().stream().map(Shape::getId))))); + } + } + } +} diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 00000000000..591ac9a2b7c --- /dev/null +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1,2 @@ +software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait$Provider +software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait$Provider diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator new file mode 100644 index 00000000000..3607c539b87 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -0,0 +1,4 @@ +software.amazon.smithy.protocoltests.traits.HttpRequestTestsInputValidator +software.amazon.smithy.protocoltests.traits.HttpResponseTestsOutputValidator +software.amazon.smithy.protocoltests.traits.HttpResponseTestsErrorValidator +software.amazon.smithy.protocoltests.traits.UniqueProtocolTestCaseIdValidator diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/manifest b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 00000000000..8970a79dcd8 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +smithy.test.smithy diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy new file mode 100644 index 00000000000..d10346fdbb8 --- /dev/null +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy @@ -0,0 +1,207 @@ +$version: "0.5.0" + +namespace smithy.test + +/// Define how an HTTP request is serialized given a specific protocol, +/// authentication scheme, and set of input parameters. +@trait(selector: "operation") +@length(min: 1) +list httpRequestTests { + member: HttpRequestTestCase, +} + +@private +structure HttpRequestTestCase { + /// The identifier of the test case. This identifier can be used by + /// protocol test implementations to filter out unsupported test + /// cases by ID, to generate test case names, etc. The provided `id` + /// MUST match Smithy's `identifier` ABNF. No two `httpRequestTests` + /// test cases can share the same ID. + @required + @pattern("[A-Za-z_][A-Za-z0-9_]+") + id: String, + + /// The name of the protocol to test. + @required + protocol: String, + + /// The expected serialized HTTP request method. + @required + @length(min: 1) + method: String, + + /// The request-target of the HTTP request, not including + /// the query string (for example, "/foo/bar"). + @required + @length(min: 1) + uri: String, + + /// The optional authentication scheme to assume. It's possible that + /// specific authentication schemes might influence the serialization + /// logic of an HTTP request. + authScheme: String, + + /// A map of expected query string parameters. + /// + /// A serialized HTTP request is not in compliance with the protocol + /// if any query string parameter defined in `queryParams` is not + /// defined in the request or if the value of a query string parameter + /// in the request differs from the expected value. + /// + /// Each key represents the query string parameter name, and each + /// value represents the query string parameter value. Both keys and + /// values MUST appear in the format in which it is expected to be + /// sent over the wire; if a key or value needs to be percent-encoded, + /// then it MUST appear percent-encoded in this map. + /// + /// `queryParams` applies no constraints on additional query parameters. + queryParams: StringMap, + + /// A list of query string parameter names that must not appear in the + /// serialized HTTP request. + /// + /// Each value MUST appear in the format in which it is sent over the + /// wire; if a key needs to be percent-encoded, then it MUST appear + /// percent-encoded in this list. + forbidQueryParams: StringList, + + /// A list of query string parameter names that MUST appear in the + /// serialized request URI, but no assertion is made on the value. + /// + /// Each value MUST appear in the format in which it is sent over the + /// wire; if a key needs to be percent-encoded, then it MUST appear + /// percent-encoded in this list. + requireQueryParams: StringList, + + /// Defines a map of expected HTTP headers. + /// + /// Headers that are not listed in this map are ignored unless they are + /// explicitly forbidden through `forbidHeaders`. + headers: StringMap, + + /// A list of header field names that must not appear in the serialized + /// HTTP request. + forbidHeaders: StringList, + + /// A list of header field names that must appear in the serialized + /// HTTP message, but no assertion is made on the value. + /// + /// Headers listed in `headers` do not need to appear in this list. + requireHeaders: StringList, + + /// The expected HTTP message body. + /// + /// If no request body is defined, then no assertions are made about + /// the body of the message. + body: String, + + /// The media type of the `body`. + /// + /// This is used to help test runners to parse and validate the expected + /// data against generated data. + bodyMediaType: String, + + /// Defines the input parameters used to generated the HTTP request. + /// + /// These parameters MUST be compatible with the input of the operation. + params: Document, + + /// Defines vendor-specific parameters that are used to influence the + /// request. For example, some vendors might utilize environment + /// variables, configuration files on disk, or other means to influence + /// the serialization formats used by clients or servers. + vendorParams: Document, + + /// A description of the test and what is being asserted. + documentation: String, +} + +@private +map StringMap { + key: String, + value: String, +} + +@private +list StringList { + member: String, +} + +/// Define how an HTTP response is serialized given a specific protocol, +/// authentication scheme, and set of output or error parameters. +@trait(selector: ":each(operation, structure[trait|error])") +@length(min: 1) +list httpResponseTests { + member: HttpResponseTestCase, +} + +@private +structure HttpResponseTestCase { + /// The identifier of the test case. This identifier can be used by + /// protocol test implementations to filter out unsupported test + /// cases by ID, to generate test case names, etc. The provided `id` + /// MUST match Smithy's `identifier` ABNF. No two `httpResponseTests` + /// test cases can share the same ID. + @required + @pattern("[A-Za-z_][A-Za-z0-9_]+") + id: String, + + /// The name of the protocol to test. + @required + protocol: String, + + /// Defines the HTTP response code. + @required + @range(min: 100, max: 599) + code: Integer, + + /// The optional authentication scheme to assume. It's possible that + /// specific authentication schemes might influence the serialization + /// logic of an HTTP response. + authScheme: String, + + /// A map of expected HTTP headers. Each key represents a header field + /// name and each value represents the expected header value. An HTTP + /// response is not in compliance with the protocol if any listed header + /// is missing from the serialized response or if the expected header + /// value differs from the serialized response value. + /// + /// `headers` applies no constraints on additional headers. + headers: StringMap, + + /// A list of header field names that must not appear. + forbidHeaders: StringList, + + /// A list of header field names that must appear in the serialized + /// HTTP message, but no assertion is made on the value. + /// + /// Headers listed in `headers` map do not need to appear in this list. + requireHeaders: StringList, + + /// Defines the HTTP message body. + /// + /// If no response body is defined, then no assertions are made about + /// the body of the message. + body: String, + + /// The media type of the `body`. + /// + /// This is used to help test runners to parse and validate the expected + /// data against generated data. Binary media type formats require that + /// the contents of `body` are base64 encoded. + bodyMediaType: String, + + /// Defines the output parameters deserialized from the HTTP response. + /// + /// These parameters MUST be compatible with the output of the operation. + params: Document, + + /// Defines vendor-specific parameters that are used to influence the + /// response. For example, some vendors might utilize environment + /// variables, configuration files on disk, or other means to influence + /// the serialization formats used by clients or servers. + vendorParams: Document, + + /// A description of the test and what is being asserted. + documentation: String, +} diff --git a/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/RunnerTest.java b/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/RunnerTest.java new file mode 100644 index 00000000000..2d9fa09d96a --- /dev/null +++ b/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/RunnerTest.java @@ -0,0 +1,19 @@ +package software.amazon.smithy.protocoltests.traits; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.ModelAssembler; +import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite; + +public class RunnerTest { + @Test + public void runTests() { + ModelAssembler assembler = Model.assembler(getClass().getClassLoader()) + .discoverModels(getClass().getClassLoader()); + + System.out.println(SmithyTestSuite.runner() + .setModelAssemblerFactory(assembler::copy) + .addTestCasesFromUrl(getClass().getResource("errorfiles")) + .run()); + } +} diff --git a/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/TraitTest.java b/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/TraitTest.java new file mode 100644 index 00000000000..5f9d156188b --- /dev/null +++ b/smithy-protocol-test-traits/src/test/java/software/amazon/smithy/protocoltests/traits/TraitTest.java @@ -0,0 +1,44 @@ +package software.amazon.smithy.protocoltests.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; + +public class TraitTest { + @Test + public void simpleRequestTest() { + Model model = Model.assembler() + .addImport(getClass().getResource("say-hello.smithy")) + .discoverModels() + .assemble() + .unwrap(); + HttpRequestTestCase testCase = model.expectShape(ShapeId.from("smithy.example#SayHello")) + .getTrait(HttpRequestTestsTrait.class) + .get() + .getTestCases() + .get(0); + + assertThat(testCase.toBuilder().build(), equalTo(testCase)); + assertThat(HttpRequestTestCase.fromNode(testCase.toNode()), equalTo(testCase)); + } + + @Test + public void simpleResponseTest() { + Model model = Model.assembler() + .addImport(getClass().getResource("say-goodbye.smithy")) + .discoverModels() + .assemble() + .unwrap(); + HttpResponseTestCase testCase = model.expectShape(ShapeId.from("smithy.example#SayGoodbye")) + .getTrait(HttpResponseTestsTrait.class) + .get() + .getTestCases() + .get(0); + + assertThat(testCase.toBuilder().build(), equalTo(testCase)); + assertThat(HttpResponseTestCase.fromNode(testCase.toNode()), equalTo(testCase)); + } +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-request-features.errors b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-request-features.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-request-features.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-request-features.smithy new file mode 100644 index 00000000000..f4f2731af09 --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-request-features.smithy @@ -0,0 +1,30 @@ +namespace smithy.example + +use smithy.test#httpRequestTests + +@http(method: "POST", uri: "/") +@httpRequestTests([ + { + id: "foo", + documentation: "Testing...", + protocol: "example", + authScheme: "test", + method: "POST", + uri: "/", + queryParams: {"foo": "baz"}, + forbidQueryParams: ["Nope"], + requireQueryParams: ["Yap"], + headers: {"X-Foo": "baz"}, + forbidHeaders: ["X-Nope"], + requireHeaders: ["X-Yap"], + body: "Hi", + bodyMediaType: "text/plain", + params: {body: "Hi"}, + vendorParams: {foo: "Bar"} + } +]) +operation SayHello(SayHelloInput) +structure SayHelloInput { + @httpPayload + body: String, +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-response-features.errors b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-response-features.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-response-features.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-response-features.smithy new file mode 100644 index 00000000000..794dda0587d --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/all-response-features.smithy @@ -0,0 +1,38 @@ +namespace smithy.example + +use smithy.test#httpResponseTests + +@readonly +@http(method: "GET", uri: "/") +@httpResponseTests([ + { + id: "foo2", + protocol: "example", + authScheme: "test", + code: 200, + headers: { + "X-Blah": "Blarg" + }, + forbidHeaders: ["X-Nope"], + requireHeaders: ["X-Yep"], + body: "Baz", + bodyMediaType: "text/plain", + params: { + bar: "Baz", + blah: "Blarg" + }, + vendorParams: { + lorem: "ipsum" + }, + documentation: "Hi" + } +]) +operation GetFoo() -> GetFooOutput + +structure GetFooOutput { + @httpPayload + bar: String, + + @httpHeader("X-Blah") + blah: String, +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.errors b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.errors new file mode 100644 index 00000000000..a15cdbf56df --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.errors @@ -0,0 +1,6 @@ +[ERROR] smithy.example#MyError: Conflicting `smithy.test#httpResponseTests` test case IDs found for ID `foo`: `smithy.example#MyError`, `smithy.example#SayGoodbye`, `smithy.example#SayGoodbye` | UniqueProtocolTestCaseId +[ERROR] smithy.example#SayGoodbye: Conflicting `smithy.test#httpResponseTests` test case IDs found for ID `foo`: `smithy.example#MyError`, `smithy.example#SayGoodbye`, `smithy.example#SayGoodbye` | UniqueProtocolTestCaseId +[ERROR] smithy.example#SayGoodbye: Conflicting `smithy.test#httpResponseTests` test case IDs found for ID `foo`: `smithy.example#MyError`, `smithy.example#SayGoodbye`, `smithy.example#SayGoodbye` | UniqueProtocolTestCaseId +[ERROR] smithy.example#SayHello2: Conflicting `smithy.test#httpRequestTests` test case IDs found for ID `foo`: `smithy.example#SayHello`, `smithy.example#SayHello`, `smithy.example#SayHello2` | UniqueProtocolTestCaseId +[ERROR] smithy.example#SayHello: Conflicting `smithy.test#httpRequestTests` test case IDs found for ID `foo`: `smithy.example#SayHello`, `smithy.example#SayHello`, `smithy.example#SayHello2` | UniqueProtocolTestCaseId +[ERROR] smithy.example#SayHello: Conflicting `smithy.test#httpRequestTests` test case IDs found for ID `foo`: `smithy.example#SayHello`, `smithy.example#SayHello`, `smithy.example#SayHello2` | UniqueProtocolTestCaseId diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.smithy new file mode 100644 index 00000000000..827933bb0af --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-duplicate-ids.smithy @@ -0,0 +1,71 @@ +namespace smithy.example + +use smithy.test#httpResponseTests +use smithy.test#httpRequestTests + +@http(method: "POST", uri: "/") +@httpResponseTests([ + { + id: "foo", // conflict with self and MyError + protocol: "example", + code: 200, + }, + { + id: "foo", // conflict with self and MyError + protocol: "example", + code: 200, + } +]) +operation SayGoodbye() -> SayGoodbyeOutput +structure SayGoodbyeOutput {} + +@httpResponseTests([ + { + id: "foo", // conflict with self and SayGoodbye + protocol: "example", + code: 200, + }, + { + id: "baz", // no conflict + protocol: "example", + code: 200, + }, +]) +@error("client") +structure MyError {} + +@http(method: "POST", uri: "/") +@httpRequestTests([ + { + id: "foo", // conflict with self and SayHello2 + protocol: "example", + method: "POST", + uri: "/", + }, + { + id: "foo", // conflict with self and SayHello2 + protocol: "example", + method: "POST", + uri: "/", + }, +]) +operation SayHello(SayHelloInput) +structure SayHelloInput {} + +@http(method: "POST", uri: "/") +@httpRequestTests([ + { + id: "foo", // conflict + protocol: "example", + method: "POST", + uri: "/", + }, + { + id: "baz", // no conflict + protocol: "example", + method: "POST", + uri: "/", + } +]) +operation SayHello2(SayHelloInput2) +structure SayHelloInput2 {} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.errors b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.errors new file mode 100644 index 00000000000..9803bcaa34d --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.errors @@ -0,0 +1,3 @@ +[ERROR] smithy.example#MyError: smithy.test#httpResponseTests.0.params.foo: Expected number value for integer shape, `smithy.api#Integer`; found string value, `Hi` | HttpResponseTestsError +[ERROR] smithy.example#SayGoodbye: smithy.test#httpResponseTests.0.params: Invalid structure member `invalid` found for `smithy.example#SayGoodbyeOutput` | HttpResponseTestsOutput +[ERROR] smithy.example#SayHello: smithy.test#httpRequestTests.0.params.badType: Expected boolean value for boolean shape, `smithy.api#Boolean`; found string value, `hi` | HttpRequestTestsInput diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.smithy new file mode 100644 index 00000000000..f4a109220f2 --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/detects-invalid-params.smithy @@ -0,0 +1,50 @@ +namespace smithy.example + +use smithy.test#httpResponseTests +use smithy.test#httpRequestTests + +@http(method: "POST", uri: "/") +@httpResponseTests([ + { + id: "foo1", + protocol: "example", + code: 200, + params: { + invalid: true + } + } +]) +operation SayGoodbye() -> SayGoodbyeOutput +structure SayGoodbyeOutput {} + +@httpResponseTests([ + { + id: "foo2", + protocol: "example", + code: 200, + params: { + foo: "Hi" + } + } +]) +@error("client") +structure MyError { + foo: Integer, +} + +@http(method: "POST", uri: "/") +@httpRequestTests([ + { + id: "foo3", + protocol: "example", + method: "POST", + uri: "/", + params: { + badType: "hi" + } + } +]) +operation SayHello(SayHelloInput) +structure SayHelloInput { + badType: Boolean +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/test-case-on-error.errors b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/test-case-on-error.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/test-case-on-error.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/test-case-on-error.smithy new file mode 100644 index 00000000000..a5ec06aa658 --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/test-case-on-error.smithy @@ -0,0 +1,23 @@ +namespace smithy.example + +use smithy.test#httpResponseTests + +@error("client") +@httpError(400) +@httpResponseTests([ + { + id: "invalid_greeting", + protocol: "example", + params: {foo: "baz", message: "Hi"}, + code: 400, + headers: {"X-Foo": "baz"}, + body: "{\"message\": \"Hi\"}", + bodyMediaType: "application/json", + } +]) +structure InvalidGreeting { + @httpHeader("X-Foo") + foo: String, + + message: String, +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-goodbye.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-goodbye.smithy new file mode 100644 index 00000000000..c15bbd3aa15 --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-goodbye.smithy @@ -0,0 +1,23 @@ +namespace smithy.example + +use smithy.test#httpResponseTests + +@http(method: "POST", uri: "/") +@httpResponseTests([ + { + id: "say_goodbye", + protocol: "example", + params: {farewell: "Bye"}, + code: 200, + headers: { + "X-Farewell": "Bye", + "Content-Length": "0" + } + } +]) +operation SayGoodbye() -> SayGoodbyeOutput + +structure SayGoodbyeOutput { + @httpHeader("X-Farewell") + farewell: String, +} diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-hello.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-hello.smithy new file mode 100644 index 00000000000..be509a4cef6 --- /dev/null +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/say-hello.smithy @@ -0,0 +1,30 @@ +namespace smithy.example + +use smithy.test#httpRequestTests + +@http(method: "POST", uri: "/") +@httpRequestTests([ + { + id: "say_hello", + protocol: "example", + params: { + "greeting": "Hi", + "name": "Teddy" + }, + method: "POST", + uri: "/", + headers: { + "X-Greeting": "Hi" + }, + body: "{\"name\": \"Teddy\"}", + bodyMediaType: "application/json" + } +]) +operation SayHello(SayHelloInput) + +structure SayHelloInput { + @httpHeader("X-Greeting") + greeting: String, + + name: String +}