From 928cc382dc0fadc8b60c1fe70218847b6576f0ca Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Tue, 29 Aug 2023 10:23:16 +0200 Subject: [PATCH] fix: noshrink response body (#39) --- README.md | 2 +- examples/users/users.json | 157 ++++++++++++++++++++++++++++++++++++++ src/restcheck.erl | 4 +- src/restcheck_backend.erl | 5 ++ src/restcheck_schema.erl | 8 +- src/restcheck_suite.erl | 19 ++++- src/restcheck_triq.erl | 5 +- 7 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 examples/users/users.json diff --git a/README.md b/README.md index 6b75abb..eca3b05 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![restcheck ci](https://github.com/nomasystems/restcheck/actions/workflows/ci.yml/badge.svg)](https://github.com/nomasystems/restcheck/actions/workflows/ci.yml) [![restcheck docs](https://github.com/nomasystems/restcheck/actions/workflows/docs.yml/badge.svg)](https://nomasystems.github.io/restcheck) -An automatic REST API contract testing tool based on property-based testing techniques. +A REST API fuzzing tool based on property-based testing techniques. ## Contributing diff --git a/examples/users/users.json b/examples/users/users.json new file mode 100644 index 0000000..e3ab02e --- /dev/null +++ b/examples/users/users.json @@ -0,0 +1,157 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Users REST API", + "version": "1.0.0", + "description": "A REST API for a simple user management service." + }, + "paths": { + "/users": { + "post": { + "operationId": "createUser", + "summary": "Creates a User", + "requestBody": { + "description": "A user creation request", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "minLength": 3, + "maxLength": 9 + }, + "password": { + "type": "string", + "minLength": 6, + "maxLength": 12 + } + }, + "required": [ + "username", + "password" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/users/{userId}": { + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "get": { + "operationId": "getUser", + "summary": "Gets a User", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteUser", + "summary": "Deletes a User", + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "userId": { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string", + "minLength": 3, + "maxLength": 9 + }, + "password": { + "type": "string", + "minLength": 6, + "maxLength": 12 + } + } + }, + "Error": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/src/restcheck.erl b/src/restcheck.erl index 0d904a2..2e1e5b3 100644 --- a/src/restcheck.erl +++ b/src/restcheck.erl @@ -263,8 +263,10 @@ format_error({method_not_allowed, {_Path, _Method}}) -> "Method not allowed"; format_error({server_error, _StatusCode}) -> "Server error"; -format_error({invalid_response_body, _RespondeBody}) -> +format_error({invalid_response_body, _ResponseBody}) -> "Invalid response body"; +format_error({invalid_response_status, _ResponseStatus}) -> + "Invalid response status"; format_error(Reason) -> erlang:term_to_binary(Reason). diff --git a/src/restcheck_backend.erl b/src/restcheck_backend.erl index 96b6bfa..d07ca2f 100644 --- a/src/restcheck_backend.erl +++ b/src/restcheck_backend.erl @@ -34,6 +34,11 @@ ForAll :: restcheck_pbt:property(). %% Wraps a forall property. +-callback noshrink(Generator) -> NoShrinkGenerator when + Generator :: restcheck_pbt:generator(), + NoShrinkGenerator :: restcheck_pbt:generator(). +%% Prevents a generator from shrinking. + -callback quickcheck(Property, NumTests, OutputFun) -> Result when Property :: restcheck_pbt:property(), NumTests :: restcheck_pbt:num_tests(), diff --git a/src/restcheck_schema.erl b/src/restcheck_schema.erl index a405bc2..093c7f1 100644 --- a/src/restcheck_schema.erl +++ b/src/restcheck_schema.erl @@ -262,18 +262,22 @@ complement(#{<<"type">> := <<"object">>} = Schema) -> true -> undefined; false -> + OldRequired = maps:get(<<"required">>, Schema, []), PropertyName = new_property_name(maps:keys(Properties)), Schema#{ <<"properties">> => Properties#{ PropertyName => #{} - } + }, + <<"required">> => [PropertyName | OldRequired] }; AdditionalSchema -> + OldRequired = maps:get(<<"required">>, Schema, []), PropertyName = new_property_name(maps:keys(Properties)), Schema#{ <<"properties">> => Properties#{ PropertyName => complement(AdditionalSchema) - } + }, + <<"required">> => [PropertyName | OldRequired] } end, Schemas = lists:filter( diff --git a/src/restcheck_suite.erl b/src/restcheck_suite.erl index b7011a2..b0cb03f 100644 --- a/src/restcheck_suite.erl +++ b/src/restcheck_suite.erl @@ -99,10 +99,17 @@ generate(API) -> RequestBodySchema = schema_ast(maps:get(RequestBody, Schemas)), RequestBodyGenerator = erl_syntax:application( erl_syntax:atom(restcheck_pbt), - erl_syntax:atom(dto), + erl_syntax:atom(noshrink), [ erl_syntax:variable('Backend'), - RequestBodySchema + erl_syntax:application( + erl_syntax:atom(restcheck_pbt), + erl_syntax:atom(dto), + [ + erl_syntax:variable('Backend'), + RequestBodySchema + ] + ) ] ), {Values, Generators} = {[RequestBodyValue | RawValues], [ @@ -315,7 +322,13 @@ prop_ast(RawPath, Method, Parameters, RequestBody, Responses) -> DefaultResponse = case maps:get('*', Responses, undefined) of undefined -> - erl_syntax:atom(false); + erl_syntax:tuple([ + erl_syntax:atom(false), + erl_syntax:tuple([ + erl_syntax:atom(invalid_response_status), + erl_syntax:variable('ResponseStatus') + ]) + ]); Ref -> erl_syntax:case_expr( erl_syntax:application( diff --git a/src/restcheck_triq.erl b/src/restcheck_triq.erl index 2d55279..ad6813a 100644 --- a/src/restcheck_triq.erl +++ b/src/restcheck_triq.erl @@ -390,10 +390,7 @@ string(Schema) -> Length :: non_neg_integer(), FormatGenerator :: restcheck_pbt:generator(). string_format(undefined, Length) -> - triq_dom:vector( - Length, - triq_dom:unicode_char() - ); + triq_dom:unicode_binary(Length); string_format(<<"base64">>, Length) -> 0 = (Length rem 4), triq_dom:vector(