Skip to content

Add except option for testing error responses#465

Open
sadahiro-ono wants to merge 1 commit intointeragent:masterfrom
sadahiro-ono:add-except-option
Open

Add except option for testing error responses#465
sadahiro-ono wants to merge 1 commit intointeragent:masterfrom
sadahiro-ono:add-except-option

Conversation

@sadahiro-ono
Copy link

@sadahiro-ono sadahiro-ono commented Feb 9, 2026

Summary

Add except option to assert_request_schema_confirm to support testing error responses where required parameters are intentionally omitted.

Background

When writing tests for error responses (e.g. 401 Unauthorized, 400 Bad Request), it is common to intentionally omit required parameters to trigger the error. However, assert_request_schema_confirm would fail on the missing parameters before the response could be validated — making it impossible to verify both the request schema and the error response in the same test.

# Previously: had to choose between validating the request or testing the error
get "/resources"  # intentionally no Authorization header
assert_request_schema_confirm  # => raises InvalidRequest: missing required header
assert_response_schema_confirm(401)

Usage

# Except specific parameters from request validation
assert_request_schema_confirm(except: { headers: ['authorization'] })
assert_response_schema_confirm(401)

# Multiple parameter types at once
assert_request_schema_confirm(
  except: {
    headers: ['authorization'],
    query:   ['page'],
    body:    ['required_field']
  }
)

Changes

New file: lib/committee/test/except_parameter.rb

Introduces ExceptParameter and its handler classes that temporarily inject dummy values for excepted parameters and restore the original state after validation.

Handler classes:

Class Target Storage
HeaderHandler HTTP request headers request.env directly
QueryHandler Query string parameters request.GET (rack.request.query_hash)
BodyHandler Request body rack.input (JSON) or rack.request.form_hash (form)

Key design decisions:

  • StringDummyLookup module — shared by all handlers, provides resolve_operation for OpenAPI3 schema lookup (prefix-stripping + operation resolution) and type/format/enum-aware dummy value generation
  • BodyHandler dispatches on Content-Type: treats application/json and +json variants (e.g. application/vnd.api+json) as JSON (replaces rack.input); calls request.POST to force Rack to parse the form body first, then injects into the live rack.request.form_hash for application/x-www-form-urlencoded / multipart/form-data; no-op for other content types (e.g. binary). This logic mirrors request_unpacker.rb exactly.
  • HeaderHandler handles the CGI special cases where Content-TypeCONTENT_TYPE and Content-LengthCONTENT_LENGTH (no HTTP_ prefix)
  • Dummy values are injected only when the parameter is absent (nil). Parameters that already carry a value are left untouched.
  • Dummy value selection: enum first value takes priority; otherwise determined by typeinteger/number/boolean/array get a zero value (native types for JSON bodies, string-encoded for query/header/form); object gets {} for JSON bodies only and falls back to "dummy-{name}" for other parameter types; string with a recognized format (date-time, date, email, uuid) gets a format-aware string; everything else falls back to "dummy-{name}". Format-aware strings and type-based zero values are parallel branches of the same case statement, not sequential priorities.
  • For JSON bodies, native Ruby types are used (0, 0.0, false, []; object{}); for query/header/form, string-encoded values are used ("0", "true") since coerce_value handles the conversion (object type is not string-encodable and falls back to "dummy-{name}")
  • rescue StandardError in schema lookup methods (find_parameter_schema, body_param_schema) prevents unexpected failures in openapi_parser internals from breaking test assertions; falls back to "dummy-{name}"
  • apply_json parses the body before committing any side-effects (@original_body, form cache deletion) so that a JSON::ParserError does not trigger a spurious restore_json call
  • apply_form uses ||= in its rescue clause to preserve any per-parameter originals already saved before the error, ensuring restore_form can undo partial injections
  • Type-aware dummy values require OpenAPI 3. Hyper-Schema and OpenAPI 2 fall back to "dummy-{name}"

Modified: lib/committee/test/methods.rb

  • Added except: {} keyword argument to assert_request_schema_confirm
  • Added private with_except_params helper; both apply and yield are inside the begin block under an ensure clause so that any partial dummy-value injection is always rolled back — even when apply itself raises (e.g. JSON::ParserError from an invalid request body)

Modified: test/data/openapi3/normal.yaml

Added test endpoints:

  • /get_endpoint_with_required_parameter — required string query param
  • /test_except_validation — two required query params (to verify partial except)
  • /get_endpoint_with_required_integer_query — required integer query param
  • /test_except_body_params — required string + integer JSON body params
  • /test_except_body_with_constraints — enum and date-time format constraints
  • /test_except_form_params — required form-encoded body params
  • /test_except_content_type_header — required Content-Type header
  • /test_except_vnd_json_bodyapplication/vnd.api+json body (verify +json variant handled as JSON)

Modified: test/test/methods_test.rb

Added 22 test cases covering:

  • Basic except for query, header, body parameters
  • Multiple parameter types excepted simultaneously
  • Partial except: non-excepted required parameters still raise errors
  • Type-aware dummies: integer query, integer header, enum body, date-time body
  • Form-encoded body: string and integer form params
  • Special rack headers: Content-TypeCONTENT_TYPE mapping
  • Does not overwrite existing parameter values (inject only when nil)
  • application/vnd.api+json body treated as JSON
  • Backward compatibility: assert_request_schema_confirm without except unchanged
  • Error recovery: injected params are fully restored even when apply raises mid-way (e.g. invalid JSON body)

Modified: README.md

Documented the except option with usage examples, parameter type table (including supported content types for body), and dummy value priority rules.

@ydah
Copy link
Member

ydah commented Feb 25, 2026

It looks like there are some trial-and-error commits here. Could you squash them into a single commit?

Add `except` option to `assert_request_schema_confirm` to support
testing error responses where required parameters are intentionally
omitted.

When testing error responses (e.g. 401 Unauthorized), required
parameters are often intentionally absent. Previously this caused
`assert_request_schema_confirm` to raise before the response could
be validated.

The `except` option temporarily injects dummy values for the specified
parameters during request validation and restores the original state
afterwards via an `ensure` clause.

  assert_request_schema_confirm(except: { headers: ['authorization'] })
  assert_response_schema_confirm(401)

Supported parameter types: headers, query, body (JSON,
application/x-www-form-urlencoded, multipart/form-data).

Dummy values are type/format/enum-aware for OpenAPI 3; OpenAPI 2 and
Hyper-Schema fall back to "dummy-{name}".
@sadahiro-ono
Copy link
Author

@ydah

It looks like there are some trial-and-error commits here. Could you squash them into a single commit?

Thank you for the feedback! I've squashed all the trial-and-error commits into a
single commit.

Copy link
Member

@geemus geemus left a comment

Choose a reason for hiding this comment

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

lgtm.

@ydah did you have any other questions or concerns?

# Resolve the OpenAPI3 operation object for the current request.
# Returns nil for non-OpenAPI3 schemas or any lookup failure.
def resolve_operation
schema = @committee_options[:schema]
Copy link
Member

Choose a reason for hiding this comment

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

ask) Since resolve_operation only references @committee_options[:schema], if schema_path is specified in committee_options during testing, won't the schema retrieval always result in nil?
As a result, the dummy value falls back to “dummy-*”. In this state, even if the required typed parameter is excluded, won't the request validation fail because the type of the inserted fallback value is incorrect?

operation = resolve_operation
return nil unless operation

params = operation.request_operation.operation_object&.parameters
Copy link
Member

Choose a reason for hiding this comment

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

ask) The parameter schema search only references operation_object.parameters and does not include parameters declared at the path item level.
In specifications that share query/header parameters in this way, except cannot find the schema and inserts the string fallback value. Therefore, wouldn't explicitly excepting a typed required parameter cause an InvalidRequest?

request_body = operation.request_operation.operation_object&.request_body
return nil unless request_body

request_body.content&.each_value do |media_type|
Copy link
Member

Choose a reason for hiding this comment

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

ask) body_param_schema scans all schemas within requestBody.content and returns the first matching property name without considering the actual request's media type. If an endpoint supports multiple body Content-Types and fields with the same name but different types exist, wouldn't except insert dummy values from the incorrect schema, potentially causing validation to fail for the actual Content-Type?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants