Skip to content

HTTP Protocol Compliance Tests

Juli Tera edited this page Apr 19, 2024 · 8 revisions

This wiki contains a mapping between Smithy HTTP Protocol Compliance Tests and generated Ruby code.

httpRequestTests trait

The httpRequestTests trait is used to define how an HTTP request is serialized given a specific protocol, authentication scheme, and set of input parameters.

To handle these tests, middleware containing RSpec expectations is inserted before Send. The client is stubbed to prevent network calls. A successful test will call the operation and not raise an error.

client = Client.new(stub_responses: true)

middleware = Hearth::MiddlewareBuilder.before_send do |input, context|
  request = context.request
  request_uri = URI.parse(context.request.url)
  # expectations
  # ...  
end

client.get_high_score(
  {id: '1'},
  middleware: middleware
)

See Smithy Documentation for more details.

Each section is listed below, assuming that the value comes from a test object.

id

The identifier of the test case. This becomes the RSpec test name.

it "#{test.id}" do
  ...
end

protocol

A shape ID that targets a shape marked with the @protocolDefinition trait. This is used to filter protocol tests to the protocol being generated.

method

The expected serialized HTTP request method. The RSpec expectation is:

expect(request.http_method).to eq(test.method)

uri

The request-target of the HTTP request, not including the query string (for example, "/foo/bar"). The RSpec expectation is:

expect(request.uri.path).to eq(test.uri)

host

The host / endpoint provided to the client (for example, “example.com”). This value is passed to the Client:

options[:endpoint] = test.host

resolvedHost

The host / endpoint that the client should send to, not including the path or scheme (for example, "prefix.example.com"). The RSpec expectation is:

expect(request.uri.host).to eq(test.resolvedHost)

authScheme

A shape ID that specifies the optional authentication scheme to assume. This does not influence code generation right now.

queryParams

A list of the expected serialized query string parameters. The list should be joined by & and parsed by CGI to output a hash. The actual query string is also parsed by CGI. Then, for each value in the expected hash, check if it is the same as the actual hash for the same key. The Rspec expectation is:

expected_query = ::CGI.parse(['foo=bar', 'hello'].join('&'))
actual_query = ::CGI.parse(request.uri.query)
expected_query.each do |k, v|
  expect(actual_query[k]).to eq(v)
end

forbidQueryParams

A list of query string parameter names that must not appear in the serialized HTTP request. Using CGI parsed query params, check if the key is a forbidden query param. The RSpec expectation is:

forbid_query = test.forbid_query_params
actual_query = ::CGI.parse(request_uri.query)
forbid_query.each do |query|
  expect(actual_query.key?(query)).to be false
end

requireQueryParams

A list of query string parameter names that MUST appear in the serialized request URI, but no assertion is made on the value. Using CGI parsed query params, check if the required params are keys in the query params. The RSpec expectation is:

require_query = test.require_query_params
actual_query = ::CGI.parse(request_uri.query)
require_query.each do |query|
  expect(actual_query.key?(query)).to be true
end

headers

A map of expected HTTP Headers - each key is a header field. The request headers hash is checked to include the test headers hash. The Rspec expectation is:

{ *test.headers }.each { |k, v| expect(request.headers[k]).to eq(v) }

forbidHeaders

A list of headers that must not appear in the request. The request header keys are checked to not include the forbidden headers. The Rspec expectation is:

[*test.headers].each { |k| expect(request.headers.key?(k)).to be(false) }

requireHeaders

A list of headers that must appear in the request. Values are not checked. The request headers keys are checked to include the required headers. The RSpec expectation is:

[*test.headers].each { |k| expect(request.headers.key?(k)).to be(true) }

body

The expected HTTP message body. If no request body is defined, then no assertions are made about the body of the message. Because the body parameter is a string, binary data MUST be represented in body by base64 encoding the data (for example, use "Zm9vCg==" and not "foo"). The bodyMediaType is used to determine the comparison.

bodyMediaType

The media type of the body. This is used to help parse and validate the expected data against generated data.

For plain text (or default), the bodies can be compared using string equality:

expect(request.body.read).to eq(test.body)

For application/json the bodies are parsed as json and compared:

expect(JSON.parse(request.body.read)).to eq(JSON.parse(<test body>))

For application/xml the bodies are parsed as xml and the Hearth match_xml_node matcher is used:

expect(Hearth::XML.parse(request.body.read)).to match_xml_node(Hearth::XML.parse(<test body>))

For application/x-www-form-urlencoded (Used in AWS Query Protocol), bodies are parsed using CGI.parse and compared using the match_query_params:

expect(CGI.parse(request.body.read)).to match_query_params(CGI.parse(<test body>))

params

Defines the input parameters used to generate the HTTP request. These parameters MUST be compatible with the input of the operation. These values are passed into the Client’s operation:

client.get_high_score(*test.params)

vendorParams

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.

This does not need to influence initial protocol test generation, but can be used to extend it in the future for more complex tests (eg validating behavior of configuration/env).

vendorParamsShape

A shape to be used to validate the vendorParams member contents. Not used in the initial protocol test generation.

documentation

A description of the test and what is being asserted defined in CommonMark. The documentation is added to the RSpec test:

# #{test.documentation}
it "#{test.id}" do
  ...
end

tags

Attaches a list of tags that allow test cases to be categorized and grouped. Does not influence code generation.

appliesTo

Indicates that the test case is only to be implemented by "client" or "server" implementations. If appliesTo is server then test generation is skipped.

httpResponseTests trait

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. However - in the context of client testing - httpResponseTests define deserialization tests: given some http request, does the response/error match the expected. The Send and Build middleware are removed, and a new middleware is provided that sets test values on the response object.

client = Client.new(stub_responses: true)

middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context|
  response = context.response
  # status, body, and headers are overriden here
  # skip the app.call to skip real send
  Hearth::Output.new
end
middleware
  .remove_send
  .remove_build
output = client.get_high_score({}, middleware: middleware)

See Smithy Documentation for more details.

Each section is listed below, assuming that the value comes from a test object.

id

The identifier of the test case. This will become the RSpec test name.

it "#{test.id}" do
  ...
end

protocol

A shape ID that targets a shape marked with the @protocolDefinition trait. This is used to filter protocol tests to the protocol being generated.

code

The expected HTTP response status code. The replacement middleware contains:

response.status = test.code

authScheme

A shape ID that specifies the optional authentication scheme to assume. This does not influence code generation right now.

headers

A map of expected HTTP headers. Each key represents a header field name and each value represents the expected header value. The replacement middleware contains:

response.headers = test.headers

forbidHeaders

A list of header field names that must not appear in the serialized HTTP response. Does not influence client code generation.

requireHeaders

A list of header field names that must appear in the serialized HTTP response, but no assertion is made on the value. Does not influence client code generation.

body

The expected HTTP message body. If no response body is defined, then no assertions are made about the body of the message. The replacement middleware contains:

response.body = StringIO.new(test.body)

bodyMediaType

The media type of the body. This is used to help parse and validate the expected data against generated data. Does not influence client code generation.

params

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. These are used to validate the returned operation data. The test params will be code generated as a Ruby hash, and compared against output.to_h.

expected = { } # code generate test.params as Ruby hash
expect(output.to_h).to include(expected)

vendorParams

Defines vendor-specific parameters that are used to influence the response. Does not influence client code generation.

vendorParamsShape

A shape to be used to validate the vendorParams member contents. Does not influence client code generation.

documentation

A description of the test and what is being asserted defined in CommonMark. The documentation is added to the RSpec test:

# #{test.documentation}
it "#{test.id}" do
  ...
end

tags

Attaches a list of tags that allow test cases to be categorized and grouped. Does not influence code generation.

appliesTo

Indicates that the test case is only to be implemented by "client" or "server" implementations. If appliesTo is server then test generation is skipped.

@httpResponseTests on error

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 Send and Build middleware are removed, and a new middleware is provided that sets test values on the response object. The expected Error class is rescued and assertions are added.

middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context|
  response = context.response
  # status, body, and headers are overridden here
  # skip the app.call to skip real send
  Hearth::Output.new
end
middleware.remove_send.remove_build
begin
  client.greeting_with_errors({}, middleware: middleware)
rescue Errors::ExpectedError => e
  # expect values on e to match test params
end

The test trait values (id, protocol, code, ect) are handled identically to those described in the httpResponseTests section above.

httpMalformedRequestTests trait

This trait is intended for servers to validate requests and it does not influence client code generation.

See Smithy Documentation for more details.

Clone this wiki locally