From 7bd378c02655a85e74209a6dfd4e10eed1725f55 Mon Sep 17 00:00:00 2001 From: Yuriy Belenko Date: Mon, 16 Dec 2019 14:32:07 +0500 Subject: [PATCH] [Slim4] Data mocker for scalar types (#4751) * [Slim4] Add OpenApiDataMocker interface template * [Slim4] Implement scalar types in data mocker * [Slim4] Cleanup, remove unused variables I've rejected the idea to keep Composer dependencies in Java/codegen variables at some point of time. It seems that after Git rebase I forgot to delete them. * [Slim4] Refresh samples * [Slim4] Add pattern option to mockString method [ref] https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.8 * [Slim4] Add enum option to mockString method [ref] https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20 * [Slim4] Refactor mockInteger and mockNumber tests * [Slim4] Refactor mockString tests * [Slim4] Use null coalescing operator * [Slim4] Implement enum option in mockString method * [Slim4] Add tests for enum option of mockString --- .../languages/PhpSlim4ServerCodegen.java | 20 +- .../php-slim4-server/composer.mustache | 4 +- .../openapi_data_mocker.mustache | 269 +++++++++++ .../openapi_data_mocker_interface.mustache | 190 ++++++++ .../openapi_data_mocker_test.mustache | 420 ++++++++++++++++++ .../php-slim4-server/phpunit.xml.mustache | 4 + .../server/petstore/php-slim4/composer.json | 4 +- .../php-slim4/lib/Mock/OpenApiDataMocker.php | 260 +++++++++++ .../lib/Mock/OpenApiDataMockerInterface.php | 181 ++++++++ .../petstore/php-slim4/phpunit.xml.dist | 4 + .../test/Mock/OpenApiDataMockerTest.php | 411 +++++++++++++++++ 11 files changed, 1763 insertions(+), 4 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache create mode 100644 modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache create mode 100644 modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_test.mustache create mode 100644 samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php create mode 100644 samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php create mode 100644 samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java index c286e9e9732..b2b8fd31e76 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java @@ -40,8 +40,8 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen { public static final String PSR7_IMPLEMENTATION = "psr7Implementation"; protected String psr7Implementation = "slim-psr7"; - protected List> composerPackages = new ArrayList>(); - protected List> composerDevPackages = new ArrayList>(); + protected String mockDirName = "Mock"; + protected String mockPackage = ""; public PhpSlim4ServerCodegen() { super(); @@ -50,6 +50,7 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen { .stability(Stability.STABLE) .build(); + mockPackage = invokerPackage + "\\" + mockDirName; outputFolder = "generated-code" + File.separator + "slim4"; embeddedTemplateDir = templateDir = "php-slim4-server"; @@ -84,6 +85,16 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen { public void processOpts() { super.processOpts(); + if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + // Update the invokerPackage for the default mockPackage + mockPackage = invokerPackage + "\\" + mockDirName; + } + + // make mock src path available in mustache template + additionalProperties.put("mockPackage", mockPackage); + additionalProperties.put("mockSrcPath", "./" + toSrcPath(mockPackage, srcBasePath)); + additionalProperties.put("mockTestPath", "./" + toSrcPath(mockPackage, testBasePath)); + if (additionalProperties.containsKey(PSR7_IMPLEMENTATION)) { this.setPsr7Implementation((String) additionalProperties.get(PSR7_IMPLEMENTATION)); } @@ -116,6 +127,11 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen { // Slim 4 doesn't parse JSON body anymore we need to add suggested middleware // ref: https://www.slimframework.com/docs/v4/objects/request.html#the-request-body supportingFiles.add(new SupportingFile("json_body_parser_middleware.mustache", toSrcPath(invokerPackage + "\\Middleware", srcBasePath), "JsonBodyParserMiddleware.php")); + + // mocking feature + supportingFiles.add(new SupportingFile("openapi_data_mocker_interface.mustache", toSrcPath(mockPackage, srcBasePath), toInterfaceName("OpenApiDataMocker") + ".php")); + supportingFiles.add(new SupportingFile("openapi_data_mocker.mustache", toSrcPath(mockPackage, srcBasePath), "OpenApiDataMocker.php")); + supportingFiles.add(new SupportingFile("openapi_data_mocker_test.mustache", toSrcPath(mockPackage, testBasePath), "OpenApiDataMockerTest.php")); } /** diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache index 6d28900f862..cc4f128875f 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache @@ -42,10 +42,12 @@ "scripts": { "test": [ "@test-apis", - "@test-models" + "@test-models", + "@test-mock" ], "test-apis": "phpunit --testsuite Apis", "test-models": "phpunit --testsuite Models", + "test-mock": "phpunit --testsuite Mock", "phpcs": "phpcs", "phplint": "phplint ./ --exclude=vendor" } diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache new file mode 100644 index 00000000000..c2e6883b969 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache @@ -0,0 +1,269 @@ +mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + return $this->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + case IMocker::DATA_TYPE_STRING: + $minLength = $options['minLength'] ?? 0; + $maxLength = $options['maxLength'] ?? null; + return $this->mockString($dataFormat, $minLength, $maxLength); + case IMocker::DATA_TYPE_BOOLEAN: + return $this->mockBoolean(); + default: + throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ + IMocker::DATA_TYPE_INTEGER, + IMocker::DATA_TYPE_NUMBER, + IMocker::DATA_TYPE_STRING, + IMocker::DATA_TYPE_BOOLEAN, + ])); + } + } + + /** + * Shortcut to mock integer type + * Equivalent to mockData(DATA_TYPE_INTEGER); + * + * @param string|null $dataFormat (optional) int32 or int64 + * @param number|null $minimum (optional) Default is 0 + * @param number|null $maximum (optional) Default is mt_getrandmax() + * @param bool|null $exclusiveMinimum (optional) Default is false + * @param bool|null $exclusiveMaximum (optional) Default is false + * + * @throws \InvalidArgumentException when $maximum less than $minimum or invalid arguments provided + * + * @return int + */ + public function mockInteger( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + return $this->getRandomNumber($minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum, 0); + } + + /** + * Shortcut to mock number type + * Equivalent to mockData(DATA_TYPE_NUMBER); + * + * @param string|null $dataFormat (optional) float or double + * @param number|null $minimum (optional) Default is 0 + * @param number|null $maximum (optional) Default is mt_getrandmax() + * @param bool|null $exclusiveMinimum (optional) Default is false + * @param bool|null $exclusiveMaximum (optional) Default is false + * + * @throws \InvalidArgumentException when $maximum less than $minimum or invalid arguments provided + * + * @return float + */ + public function mockNumber( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + return $this->getRandomNumber($minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum, 4); + } + + /** + * Shortcut to mock string type + * Equivalent to mockData(DATA_TYPE_STRING); + * + * @param string|null $dataFormat (optional) one of byte, binary, date, date-time, password + * @param int|null $minLength (optional) Default is 0 + * @param int|null $maxLength (optional) Default is 100 chars + * @param array $enum (optional) This array should have at least one element. + * Elements in the array should be unique. + * @param string|null $pattern (optional) This string should be a valid regular expression, according to the ECMA 262 regular expression dialect. + * Recall: regular expressions are not implicitly anchored. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return string + */ + public function mockString( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null, + $pattern = null + ) { + if ($enum !== null) { + if ( + is_array($enum) === false + || empty($enum) + || count($enum) > count(array_unique($enum)) + ) { + throw new InvalidArgumentException('"enum" must be an array. This array should have at least one element. Elements in the array should be unique.'); + } + + // return random variant + return $enum[mt_rand(0, count($enum) - 1)]; + } + + if ($minLength !== 0 && $minLength !== null) { + if (is_int($minLength) === false) { + throw new InvalidArgumentException('"minLength" must be an integer'); + } elseif ($minLength < 0) { + throw new InvalidArgumentException('"minLength" must be greater than, or equal to, 0'); + } + } else { + $minLength = 0; + } + + if ($maxLength !== null) { + if (is_int($maxLength) === false) { + throw new InvalidArgumentException('"maxLength" must be an integer'); + } elseif ($maxLength < 0) { + throw new InvalidArgumentException('"maxLength" must be greater than, or equal to, 0'); + } + } else { + // since we don't need huge texts by default, lets cut them down to 100 chars + $maxLength = 100; + } + + if ($maxLength < $minLength) { + throw new InvalidArgumentException('"maxLength" value cannot be less than "minLength"'); + } + + return str_pad('', mt_rand($minLength, $maxLength), 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', \STR_PAD_RIGHT); + } + + /** + * Shortcut to mock boolean type + * Equivalent to mockData(DATA_TYPE_BOOLEAN); + * + * @return bool + */ + public function mockBoolean() + { + return (bool) mt_rand(0, 1); + } + + /** + * @internal + * + * @return float|int + */ + protected function getRandomNumber( + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $maxDecimals = 4 + ) { + $min = 0; + $max = mt_getrandmax(); + + if ($minimum !== null) { + if (is_numeric($minimum) === false) { + throw new InvalidArgumentException('"minimum" must be a number'); + } + $min = $minimum; + } + + if ($maximum !== null) { + if (is_numeric($maximum) === false) { + throw new InvalidArgumentException('"maximum" must be a number'); + } + $max = $maximum; + } + + if ($exclusiveMinimum !== false) { + if (is_bool($exclusiveMinimum) === false) { + throw new InvalidArgumentException('"exclusiveMinimum" must be a boolean'); + } elseif ($minimum === null) { + throw new InvalidArgumentException('If "exclusiveMinimum" is present, "minimum" must also be present'); + } + $min += 1; + } + + if ($exclusiveMaximum !== false) { + if (is_bool($exclusiveMaximum) === false) { + throw new InvalidArgumentException('"exclusiveMaximum" must be a boolean'); + } elseif ($maximum === null) { + throw new InvalidArgumentException('If "exclusiveMaximum" is present, "maximum" must also be present'); + } + $max -= 1; + } + + if ($max < $min) { + throw new InvalidArgumentException('"maximum" value cannot be less than "minimum"'); + } + + if ($maxDecimals > 0) { + return round($min + mt_rand() / mt_getrandmax() * ($max - $min), $maxDecimals); + } + return mt_rand($min, $max); + } +} +{{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache new file mode 100644 index 00000000000..399b48fe1a5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache @@ -0,0 +1,190 @@ +assertInternalType($expectedType, $mocker->mock($dataType)); + } + + public function provideMockCorrectArguments() + { + return [ + [IMocker::DATA_TYPE_INTEGER, null, null, IsType::TYPE_INT], + [IMocker::DATA_TYPE_NUMBER, null, null, IsType::TYPE_FLOAT], + [IMocker::DATA_TYPE_STRING, null, null, IsType::TYPE_STRING], + [IMocker::DATA_TYPE_BOOLEAN, null, null, IsType::TYPE_BOOL], + ]; + } + + /** + * @dataProvider provideMockIntegerCorrectArguments + * @covers ::mockInteger + */ + public function testMockIntegerWithCorrectArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $integer = $mocker->mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + + $this->internalAssertNumber( + $integer, + $minimum, + $maximum, + $exclusiveMinimum, + $exclusiveMaximum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockIntegerCorrectArguments() + { + $types = [ + IsType::TYPE_INT, + IsType::TYPE_NUMERIC, + IsType::TYPE_SCALAR, + ]; + $notTypes = [ + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_FLOAT, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_STRING, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, -100, 100, false, false, $types, $notTypes], + [null, -100, null, false, false, $types, $notTypes], + [null, null, 100, false, false, $types, $notTypes], + [null, -99.5, null, true, false, $types, $notTypes], + [null, null, 99.5, false, true, $types, $notTypes], + [null, -99.5, 99.5, true, true, $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockIntegerInvalidArguments + * @covers ::mockInteger + * @expectedException \InvalidArgumentException + */ + public function testMockIntegerWithInvalidArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + $mocker = new OpenApiDataMocker(); + $integer = $mocker->mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + + public function provideMockIntegerInvalidArguments() + { + return [ + [null, 'foo', null, false, false], + [null, null, false, false, false], + [null, null, null, true, false], + [null, null, null, false, true], + [null, 100, -100, false, false], + ]; + } + + /** + * @dataProvider provideMockNumberCorrectArguments + * @covers ::mockNumber + */ + public function testMockNumberWithCorrectArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $number = $mocker->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + + $this->internalAssertNumber( + $number, + $minimum, + $maximum, + $exclusiveMinimum, + $exclusiveMaximum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockNumberCorrectArguments() + { + $types = [ + IsType::TYPE_SCALAR, + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + ]; + $notTypes = [ + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_STRING, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, -100, 100, false, false, $types, $notTypes], + [null, -100, null, false, false, $types, $notTypes], + [null, null, 100, false, false, $types, $notTypes], + [null, -99.5, null, true, false, $types, $notTypes], + [null, null, 99.5, false, true, $types, $notTypes], + [null, -99.5, 99.5, true, true, $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockNumberInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockNumber + */ + public function testMockNumberWithInvalidArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + $mocker = new OpenApiDataMocker(); + $number = $mocker->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + + public function provideMockNumberInvalidArguments() + { + return [ + [null, 'foo', null, false, false], + [null, null, false, false, false], + [null, null, null, true, false], + [null, null, null, false, true], + [null, 100, -100, false, false], + ]; + } + + /** + * @dataProvider provideMockStringCorrectArguments + * @covers ::mockString + */ + public function testMockStringWithCorrectArguments( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $str = $mocker->mockString($dataFormat, $minLength, $maxLength, $enum); + + $this->internalAssertString( + $str, + $minLength, + $maxLength, + $enum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockStringCorrectArguments() + { + $types = [ + IsType::TYPE_SCALAR, + IsType::TYPE_STRING, + ]; + $notTypes = [ + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, 0, null, null, $types, $notTypes], + [null, 10, null, null, $types, $notTypes], + [null, 0, 100, null, $types, $notTypes], + [null, 10, 50, null, $types, $notTypes], + [null, 10, 10, null, $types, $notTypes], + [null, 0, 0, null, $types, $notTypes], + [null, null, null, null, $types, $notTypes], + [null, null, null, ['foobar', 'foobaz', 'hello world'], $types, $notTypes], + [null, null, null, ['foobar'], $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockStringInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockString + */ + public function testMockStringWithInvalidArguments( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null + ) { + $mocker = new OpenApiDataMocker(); + $str = $mocker->mockString($dataFormat, $minLength, $maxLength, $enum); + } + + public function provideMockStringInvalidArguments() + { + return [ + 'negative minLength' => [null, -10, null], + 'negative maxLength' => [null, 0, -10], + 'both minLength maxLength negative' => [null, -10, -10], + 'decimal minLength and maxLength' => [null, 0.5, 0.5], + 'string minLength' => [null, '10', null], + 'string maxLength' => [null, 0, '50'], + 'string minLength and maxLength' => [null, '10', '50'], + 'maxLength less than minLength' => [null, 50, 10], + 'enum is string' => [null, null, null, 'foobar'], + 'enum is empty array' => [null, null, null, []], + 'enum array is not unique' => [null, null, null, ['foobar', 'foobaz', 'foobar']], + ]; + } + + /** + * @covers ::mockBoolean + */ + public function testMockBoolean() + { + $mocker = new OpenApiDataMocker(); + $bool = $mocker->mockBoolean(); + + $matchingInternalTypes = [ + IsType::TYPE_SCALAR, + IsType::TYPE_BOOL, + ]; + + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $bool); + } + + $notMatchingInternalTypes = [ + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_STRING, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_CALLABLE, + ]; + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $bool); + } + } + + private function internalAssertNumber( + $number, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $number); + } + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $number); + } + + if ($minimum !== null) { + if ($exclusiveMinimum) { + $this->assertGreaterThan($minimum, $number); + } else { + $this->assertGreaterThanOrEqual($minimum, $number); + } + } + + if ($maximum !== null) { + if ($exclusiveMaximum) { + $this->assertLessThan($maximum, $number); + } else { + $this->assertLessThanOrEqual($maximum, $number); + } + } + } + + private function internalAssertString( + $str, + $minLength = null, + $maxLength = null, + $enum = null, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $str); + } + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $str); + } + + if ($minLength !== null) { + $this->assertGreaterThanOrEqual($minLength, mb_strlen($str, 'UTF-8')); + } + + if ($maxLength !== null) { + $this->assertLessThanOrEqual($maxLength, mb_strlen($str, 'UTF-8')); + } + + if (is_array($enum) && !empty($enum)) { + $this->assertContains($str, $enum); + } + } +} +{{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/phpunit.xml.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/phpunit.xml.mustache index b8852944a12..81e19e73605 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/phpunit.xml.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/phpunit.xml.mustache @@ -17,11 +17,15 @@ {{modelTestPath}} + + {{mockTestPath}} + {{apiSrcPath}} {{modelSrcPath}} + {{mockSrcPath}} diff --git a/samples/server/petstore/php-slim4/composer.json b/samples/server/petstore/php-slim4/composer.json index 3a82d6db53f..4a45af2568d 100644 --- a/samples/server/petstore/php-slim4/composer.json +++ b/samples/server/petstore/php-slim4/composer.json @@ -29,10 +29,12 @@ "scripts": { "test": [ "@test-apis", - "@test-models" + "@test-models", + "@test-mock" ], "test-apis": "phpunit --testsuite Apis", "test-models": "phpunit --testsuite Models", + "test-mock": "phpunit --testsuite Mock", "phpcs": "phpcs", "phplint": "phplint ./ --exclude=vendor" } diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php new file mode 100644 index 00000000000..190a08e1d65 --- /dev/null +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php @@ -0,0 +1,260 @@ +mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + return $this->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + case IMocker::DATA_TYPE_STRING: + $minLength = $options['minLength'] ?? 0; + $maxLength = $options['maxLength'] ?? null; + return $this->mockString($dataFormat, $minLength, $maxLength); + case IMocker::DATA_TYPE_BOOLEAN: + return $this->mockBoolean(); + default: + throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ + IMocker::DATA_TYPE_INTEGER, + IMocker::DATA_TYPE_NUMBER, + IMocker::DATA_TYPE_STRING, + IMocker::DATA_TYPE_BOOLEAN, + ])); + } + } + + /** + * Shortcut to mock integer type + * Equivalent to mockData(DATA_TYPE_INTEGER); + * + * @param string|null $dataFormat (optional) int32 or int64 + * @param number|null $minimum (optional) Default is 0 + * @param number|null $maximum (optional) Default is mt_getrandmax() + * @param bool|null $exclusiveMinimum (optional) Default is false + * @param bool|null $exclusiveMaximum (optional) Default is false + * + * @throws \InvalidArgumentException when $maximum less than $minimum or invalid arguments provided + * + * @return int + */ + public function mockInteger( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + return $this->getRandomNumber($minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum, 0); + } + + /** + * Shortcut to mock number type + * Equivalent to mockData(DATA_TYPE_NUMBER); + * + * @param string|null $dataFormat (optional) float or double + * @param number|null $minimum (optional) Default is 0 + * @param number|null $maximum (optional) Default is mt_getrandmax() + * @param bool|null $exclusiveMinimum (optional) Default is false + * @param bool|null $exclusiveMaximum (optional) Default is false + * + * @throws \InvalidArgumentException when $maximum less than $minimum or invalid arguments provided + * + * @return float + */ + public function mockNumber( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + return $this->getRandomNumber($minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum, 4); + } + + /** + * Shortcut to mock string type + * Equivalent to mockData(DATA_TYPE_STRING); + * + * @param string|null $dataFormat (optional) one of byte, binary, date, date-time, password + * @param int|null $minLength (optional) Default is 0 + * @param int|null $maxLength (optional) Default is 100 chars + * @param array $enum (optional) This array should have at least one element. + * Elements in the array should be unique. + * @param string|null $pattern (optional) This string should be a valid regular expression, according to the ECMA 262 regular expression dialect. + * Recall: regular expressions are not implicitly anchored. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return string + */ + public function mockString( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null, + $pattern = null + ) { + if ($enum !== null) { + if ( + is_array($enum) === false + || empty($enum) + || count($enum) > count(array_unique($enum)) + ) { + throw new InvalidArgumentException('"enum" must be an array. This array should have at least one element. Elements in the array should be unique.'); + } + + // return random variant + return $enum[mt_rand(0, count($enum) - 1)]; + } + + if ($minLength !== 0 && $minLength !== null) { + if (is_int($minLength) === false) { + throw new InvalidArgumentException('"minLength" must be an integer'); + } elseif ($minLength < 0) { + throw new InvalidArgumentException('"minLength" must be greater than, or equal to, 0'); + } + } else { + $minLength = 0; + } + + if ($maxLength !== null) { + if (is_int($maxLength) === false) { + throw new InvalidArgumentException('"maxLength" must be an integer'); + } elseif ($maxLength < 0) { + throw new InvalidArgumentException('"maxLength" must be greater than, or equal to, 0'); + } + } else { + // since we don't need huge texts by default, lets cut them down to 100 chars + $maxLength = 100; + } + + if ($maxLength < $minLength) { + throw new InvalidArgumentException('"maxLength" value cannot be less than "minLength"'); + } + + return str_pad('', mt_rand($minLength, $maxLength), 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', \STR_PAD_RIGHT); + } + + /** + * Shortcut to mock boolean type + * Equivalent to mockData(DATA_TYPE_BOOLEAN); + * + * @return bool + */ + public function mockBoolean() + { + return (bool) mt_rand(0, 1); + } + + /** + * @internal + * + * @return float|int + */ + protected function getRandomNumber( + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $maxDecimals = 4 + ) { + $min = 0; + $max = mt_getrandmax(); + + if ($minimum !== null) { + if (is_numeric($minimum) === false) { + throw new InvalidArgumentException('"minimum" must be a number'); + } + $min = $minimum; + } + + if ($maximum !== null) { + if (is_numeric($maximum) === false) { + throw new InvalidArgumentException('"maximum" must be a number'); + } + $max = $maximum; + } + + if ($exclusiveMinimum !== false) { + if (is_bool($exclusiveMinimum) === false) { + throw new InvalidArgumentException('"exclusiveMinimum" must be a boolean'); + } elseif ($minimum === null) { + throw new InvalidArgumentException('If "exclusiveMinimum" is present, "minimum" must also be present'); + } + $min += 1; + } + + if ($exclusiveMaximum !== false) { + if (is_bool($exclusiveMaximum) === false) { + throw new InvalidArgumentException('"exclusiveMaximum" must be a boolean'); + } elseif ($maximum === null) { + throw new InvalidArgumentException('If "exclusiveMaximum" is present, "maximum" must also be present'); + } + $max -= 1; + } + + if ($max < $min) { + throw new InvalidArgumentException('"maximum" value cannot be less than "minimum"'); + } + + if ($maxDecimals > 0) { + return round($min + mt_rand() / mt_getrandmax() * ($max - $min), $maxDecimals); + } + return mt_rand($min, $max); + } +} diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php new file mode 100644 index 00000000000..b9329ead1dc --- /dev/null +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php @@ -0,0 +1,181 @@ + ./test/Model + + ./test/Mock + ./lib/Api ./lib/Model + ./lib/Mock diff --git a/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php new file mode 100644 index 00000000000..2681522cb2a --- /dev/null +++ b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php @@ -0,0 +1,411 @@ +assertInternalType($expectedType, $mocker->mock($dataType)); + } + + public function provideMockCorrectArguments() + { + return [ + [IMocker::DATA_TYPE_INTEGER, null, null, IsType::TYPE_INT], + [IMocker::DATA_TYPE_NUMBER, null, null, IsType::TYPE_FLOAT], + [IMocker::DATA_TYPE_STRING, null, null, IsType::TYPE_STRING], + [IMocker::DATA_TYPE_BOOLEAN, null, null, IsType::TYPE_BOOL], + ]; + } + + /** + * @dataProvider provideMockIntegerCorrectArguments + * @covers ::mockInteger + */ + public function testMockIntegerWithCorrectArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $integer = $mocker->mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + + $this->internalAssertNumber( + $integer, + $minimum, + $maximum, + $exclusiveMinimum, + $exclusiveMaximum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockIntegerCorrectArguments() + { + $types = [ + IsType::TYPE_INT, + IsType::TYPE_NUMERIC, + IsType::TYPE_SCALAR, + ]; + $notTypes = [ + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_FLOAT, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_STRING, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, -100, 100, false, false, $types, $notTypes], + [null, -100, null, false, false, $types, $notTypes], + [null, null, 100, false, false, $types, $notTypes], + [null, -99.5, null, true, false, $types, $notTypes], + [null, null, 99.5, false, true, $types, $notTypes], + [null, -99.5, 99.5, true, true, $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockIntegerInvalidArguments + * @covers ::mockInteger + * @expectedException \InvalidArgumentException + */ + public function testMockIntegerWithInvalidArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + $mocker = new OpenApiDataMocker(); + $integer = $mocker->mockInteger($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + + public function provideMockIntegerInvalidArguments() + { + return [ + [null, 'foo', null, false, false], + [null, null, false, false, false], + [null, null, null, true, false], + [null, null, null, false, true], + [null, 100, -100, false, false], + ]; + } + + /** + * @dataProvider provideMockNumberCorrectArguments + * @covers ::mockNumber + */ + public function testMockNumberWithCorrectArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $number = $mocker->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + + $this->internalAssertNumber( + $number, + $minimum, + $maximum, + $exclusiveMinimum, + $exclusiveMaximum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockNumberCorrectArguments() + { + $types = [ + IsType::TYPE_SCALAR, + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + ]; + $notTypes = [ + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_STRING, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, -100, 100, false, false, $types, $notTypes], + [null, -100, null, false, false, $types, $notTypes], + [null, null, 100, false, false, $types, $notTypes], + [null, -99.5, null, true, false, $types, $notTypes], + [null, null, 99.5, false, true, $types, $notTypes], + [null, -99.5, 99.5, true, true, $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockNumberInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockNumber + */ + public function testMockNumberWithInvalidArguments( + $dataFormat = null, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false + ) { + $mocker = new OpenApiDataMocker(); + $number = $mocker->mockNumber($dataFormat, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + } + + public function provideMockNumberInvalidArguments() + { + return [ + [null, 'foo', null, false, false], + [null, null, false, false, false], + [null, null, null, true, false], + [null, null, null, false, true], + [null, 100, -100, false, false], + ]; + } + + /** + * @dataProvider provideMockStringCorrectArguments + * @covers ::mockString + */ + public function testMockStringWithCorrectArguments( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + $mocker = new OpenApiDataMocker(); + $str = $mocker->mockString($dataFormat, $minLength, $maxLength, $enum); + + $this->internalAssertString( + $str, + $minLength, + $maxLength, + $enum, + $matchingInternalTypes, + $notMatchingInternalTypes + ); + } + + public function provideMockStringCorrectArguments() + { + $types = [ + IsType::TYPE_SCALAR, + IsType::TYPE_STRING, + ]; + $notTypes = [ + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_BOOL, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_CALLABLE, + ]; + + return [ + [null, 0, null, null, $types, $notTypes], + [null, 10, null, null, $types, $notTypes], + [null, 0, 100, null, $types, $notTypes], + [null, 10, 50, null, $types, $notTypes], + [null, 10, 10, null, $types, $notTypes], + [null, 0, 0, null, $types, $notTypes], + [null, null, null, null, $types, $notTypes], + [null, null, null, ['foobar', 'foobaz', 'hello world'], $types, $notTypes], + [null, null, null, ['foobar'], $types, $notTypes], + ]; + } + + /** + * @dataProvider provideMockStringInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockString + */ + public function testMockStringWithInvalidArguments( + $dataFormat = null, + $minLength = 0, + $maxLength = null, + $enum = null + ) { + $mocker = new OpenApiDataMocker(); + $str = $mocker->mockString($dataFormat, $minLength, $maxLength, $enum); + } + + public function provideMockStringInvalidArguments() + { + return [ + 'negative minLength' => [null, -10, null], + 'negative maxLength' => [null, 0, -10], + 'both minLength maxLength negative' => [null, -10, -10], + 'decimal minLength and maxLength' => [null, 0.5, 0.5], + 'string minLength' => [null, '10', null], + 'string maxLength' => [null, 0, '50'], + 'string minLength and maxLength' => [null, '10', '50'], + 'maxLength less than minLength' => [null, 50, 10], + 'enum is string' => [null, null, null, 'foobar'], + 'enum is empty array' => [null, null, null, []], + 'enum array is not unique' => [null, null, null, ['foobar', 'foobaz', 'foobar']], + ]; + } + + /** + * @covers ::mockBoolean + */ + public function testMockBoolean() + { + $mocker = new OpenApiDataMocker(); + $bool = $mocker->mockBoolean(); + + $matchingInternalTypes = [ + IsType::TYPE_SCALAR, + IsType::TYPE_BOOL, + ]; + + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $bool); + } + + $notMatchingInternalTypes = [ + IsType::TYPE_NUMERIC, + IsType::TYPE_FLOAT, + IsType::TYPE_INT, + IsType::TYPE_ARRAY, + IsType::TYPE_STRING, + IsType::TYPE_NULL, + IsType::TYPE_OBJECT, + IsType::TYPE_RESOURCE, + IsType::TYPE_CALLABLE, + ]; + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $bool); + } + } + + private function internalAssertNumber( + $number, + $minimum = null, + $maximum = null, + $exclusiveMinimum = false, + $exclusiveMaximum = false, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $number); + } + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $number); + } + + if ($minimum !== null) { + if ($exclusiveMinimum) { + $this->assertGreaterThan($minimum, $number); + } else { + $this->assertGreaterThanOrEqual($minimum, $number); + } + } + + if ($maximum !== null) { + if ($exclusiveMaximum) { + $this->assertLessThan($maximum, $number); + } else { + $this->assertLessThanOrEqual($maximum, $number); + } + } + } + + private function internalAssertString( + $str, + $minLength = null, + $maxLength = null, + $enum = null, + $matchingInternalTypes = [], + $notMatchingInternalTypes = [] + ) { + foreach ($matchingInternalTypes as $matchType) { + $this->assertInternalType($matchType, $str); + } + + foreach ($notMatchingInternalTypes as $notMatchType) { + $this->assertNotInternalType($notMatchType, $str); + } + + if ($minLength !== null) { + $this->assertGreaterThanOrEqual($minLength, mb_strlen($str, 'UTF-8')); + } + + if ($maxLength !== null) { + $this->assertLessThanOrEqual($maxLength, mb_strlen($str, 'UTF-8')); + } + + if (is_array($enum) && !empty($enum)) { + $this->assertContains($str, $enum); + } + } +}