rootContext = new Context(['isTestingRoot' => true]); $this->rootAnnotation = new OA\OpenApi(['_context' => $this->rootContext]); } public function testCreateContextSetsParentContext() { $context = Util::createContext([], $this->rootContext); $this->assertSame($this->rootContext, $context->getRootContext()); } public function testCreateContextWithProperties() { $context = Util::createContext(['testing' => 'trait']); $this->assertTrue($context->is('testing')); $this->assertSame('trait', $context->testing); } public function testCreateChild() { $info = Util::createChild($this->rootAnnotation, OA\Info::class); $this->assertInstanceOf(OA\Info::class, $info); } public function testCreateChildHasContext() { $info = Util::createChild($this->rootAnnotation, OA\Info::class); $this->assertInstanceOf(Context::class, $info->_context); } public function testCreateChildHasNestedContext() { $path = Util::createChild($this->rootAnnotation, OA\PathItem::class); $this->assertIsNested($this->rootAnnotation, $path); $parameter = Util::createChild($path, OA\Parameter::class); $this->assertIsNested($path, $parameter); $schema = Util::createChild($parameter, OA\Schema::class); $this->assertIsNested($parameter, $schema); $this->assertIsConnectedToRootContext($schema); } public function testCreateChildWithEmptyProperties() { $properties = []; /** @var OA\Info $info */ $info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties); $properties = array_filter(get_object_vars($info), function ($key) { return 0 !== strpos($key, '_'); }, ARRAY_FILTER_USE_KEY); $this->assertEquals([UNDEFINED], array_unique(array_values($properties))); $this->assertIsNested($this->rootAnnotation, $info); $this->assertIsConnectedToRootContext($info); } public function testCreateChildWithProperties() { $properties = ['title' => 'testing', 'version' => '999', 'x' => new \stdClass()]; /** @var OA\Info $info */ $info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties); $this->assertSame($info->title, $properties['title']); $this->assertSame($info->version, $properties['version']); $this->assertSame($info->x, $properties['x']); $this->assertIsNested($this->rootAnnotation, $info); $this->assertIsConnectedToRootContext($info); } public function testCreateCollectionItemAddsCreatedItemToCollection() { $collection = 'paths'; $class = OA\PathItem::class; $p1 = Util::createCollectionItem($this->rootAnnotation, $collection, $class); $this->assertSame(0, $p1); $this->assertCount(1, $this->rootAnnotation->{$collection}); $this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p1]); $this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p1]); $this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p1]); $p2 = Util::createCollectionItem($this->rootAnnotation, $collection, $class); $this->assertSame(1, $p2); $this->assertCount(2, $this->rootAnnotation->{$collection}); $this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p2]); $this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p2]); $this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p2]); $this->rootAnnotation->components = Util::createChild($this->rootAnnotation, OA\Components::class); $collection = 'schemas'; $class = OA\Schema::class; $d1 = Util::createCollectionItem($this->rootAnnotation->components, $collection, $class); $this->assertSame(0, $d1); $this->assertCount(1, $this->rootAnnotation->components->{$collection}); $this->assertInstanceOf($class, $this->rootAnnotation->components->{$collection}[$d1]); $this->assertIsNested($this->rootAnnotation->components, $this->rootAnnotation->components->{$collection}[$d1]); $this->assertIsConnectedToRootContext($this->rootAnnotation->components->{$collection}[$d1]); } public function testCreateCollectionItemDoesNotAddToUnknownProperty() { $collection = 'foobars'; $class = OA\Info::class; $expectedRegex = "/Property \"{$collection}\" doesn't exist .*/"; set_error_handler(function ($_, $err) { echo $err; }); $this->expectOutputRegex($expectedRegex); Util::createCollectionItem($this->rootAnnotation, $collection, $class); $this->expectOutputRegex($expectedRegex); $this->assertNull($this->rootAnnotation->{$collection}); restore_error_handler(); } public function testSearchCollectionItem() { $item1 = new \stdClass(); $item1->prop1 = 'item 1 prop 1'; $item1->prop2 = 'item 1 prop 2'; $item1->prop3 = 'item 1 prop 3'; $item2 = new \stdClass(); $item2->prop1 = 'item 2 prop 1'; $item2->prop2 = 'item 2 prop 2'; $item2->prop3 = 'item 2 prop 3'; $collection = [ $item1, $item2, ]; $this->assertSame(0, Util::searchCollectionItem($collection, get_object_vars($item1))); $this->assertSame(1, Util::searchCollectionItem($collection, get_object_vars($item2))); $this->assertNull(Util::searchCollectionItem( $collection, array_merge(get_object_vars($item2), ['prop3' => 'foobar']) )); $search = ['baz' => 'foobar']; $this->expectOutputString('Undefined property: stdClass::$baz'); try { Util::searchCollectionItem($collection, array_merge(get_object_vars($item2), $search)); } catch (\Exception $e) { echo $e->getMessage(); } // no exception on empty collection $this->assertNull(Util::searchCollectionItem([], get_object_vars($item2))); } /** * @dataProvider provideIndexedCollectionData */ public function testSearchIndexedCollectionItem($setup, $asserts) { foreach ($asserts as $collection => $items) { foreach ($items as $assert) { $setupCollection = empty($assert['components']) ? ($setup[$collection] ?? []) : (OA\UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []); // get the indexing correct within haystack preparation $properties = array_fill(0, \count($setupCollection), null); // prepare the haystack array foreach ($items as $assertItem) { // e.g. $properties[1] = new OA\PathItem(['path' => 'path 1']) $properties[$assertItem['index']] = new $assertItem['class']([ $assertItem['key'] => $assertItem['value'], ]); } $this->assertSame( $assert['index'], Util::searchIndexedCollectionItem($properties, $assert['key'], $assert['value']), sprintf('Failed to get the correct index for %s', print_r($assert, true)) ); } } } /** * @dataProvider provideIndexedCollectionData */ public function testGetIndexedCollectionItem($setup, $asserts) { $parent = new $setup['class'](array_merge( $this->getSetupPropertiesWithoutClass($setup), ['_context' => $this->rootContext] )); foreach ($asserts as $collection => $items) { foreach ($items as $assert) { $itemParent = empty($assert['components']) ? $parent : $parent->components; $child = Util::getIndexedCollectionItem( $itemParent, $assert['class'], $assert['value'] ); $this->assertInstanceOf($assert['class'], $child); $this->assertSame($child->{$assert['key']}, $assert['value']); $this->assertSame( $itemParent->{$collection}[$assert['index']], $child ); $setupHaystack = empty($assert['components']) ? $setup[$collection] ?? [] : $setup['components']->{$collection} ?? []; // the children created within provider are not connected if (!\in_array($child, $setupHaystack, true)) { $this->assertIsNested($itemParent, $child); $this->assertIsConnectedToRootContext($child); } } } } public function provideIndexedCollectionData(): array { return [[ 'setup' => [ 'class' => OA\OpenApi::class, 'paths' => [ new OA\PathItem(['path' => 'path 0']), ], 'components' => new OA\Components([ 'parameters' => [ new OA\Parameter(['parameter' => 'parameter 0']), new OA\Parameter(['parameter' => 'parameter 1']), ], ]), ], 'assert' => [ // one fixed within setup and one dynamically created 'paths' => [ [ 'index' => 0, 'class' => OA\PathItem::class, 'key' => 'path', 'value' => 'path 0', ], [ 'index' => 1, 'class' => OA\PathItem::class, 'key' => 'path', 'value' => 'path 1', ], ], // search indexes out of order followed by dynamically created 'parameters' => [ [ 'index' => 1, 'class' => OA\Parameter::class, 'key' => 'parameter', 'value' => 'parameter 1', 'components' => true, ], [ 'index' => 0, 'class' => OA\Parameter::class, 'key' => 'parameter', 'value' => 'parameter 0', 'components' => true, ], [ 'index' => 2, 'class' => OA\Parameter::class, 'key' => 'parameter', 'value' => 'parameter 2', 'components' => true, ], ], // two dynamically created 'responses' => [ [ 'index' => 0, 'class' => OA\Response::class, 'key' => 'response', 'value' => 'response 0', 'components' => true, ], [ 'index' => 1, 'class' => OA\Response::class, 'key' => 'response', 'value' => 'response 1', 'components' => true, ], ], // for sake of completeness 'securitySchemes' => [ [ 'index' => 0, 'class' => OA\SecurityScheme::class, 'key' => 'securityScheme', 'value' => 'securityScheme 0', 'components' => true, ], ], ], ]]; } /** * @dataProvider provideChildData */ public function testGetChild($setup, $asserts) { $parent = new $setup['class'](array_merge( $this->getSetupPropertiesWithoutClass($setup), ['_context' => $this->rootContext] )); foreach ($asserts as $key => $assert) { if (array_key_exists('exceptionMessage', $assert)) { $this->expectExceptionMessage($assert['exceptionMessage']); } $child = Util::getChild($parent, $assert['class'], $assert['props']); $this->assertInstanceOf($assert['class'], $child); $this->assertSame($child, $parent->{$key}); if (\array_key_exists($key, $setup)) { $this->assertSame($setup[$key], $parent->{$key}); } $this->assertEquals($assert['props'], $this->getNonDefaultProperties($child)); } } public function provideChildData(): array { return [[ 'setup' => [ 'class' => OA\PathItem::class, 'get' => new OA\Get([]), ], 'assert' => [ // fixed within setup 'get' => [ 'class' => OA\Get::class, 'props' => [], ], // create new without props 'put' => [ 'class' => OA\Put::class, 'props' => [], ], // create new with multiple props 'delete' => [ 'class' => OA\Delete::class, 'props' => [ 'summary' => 'testing delete', 'deprecated' => true, ], ], ], ], [ 'setup' => [ 'class' => OA\Parameter::class, ], 'assert' => [ // create new with multiple props 'schema' => [ 'class' => OA\Schema::class, 'props' => [ 'ref' => '#/testing/schema', 'minProperties' => 0, 'enum' => [null, 'check', 999, false], ], ], ], ], [ 'setup' => [ 'class' => OA\Parameter::class, ], 'assert' => [ // externalDocs triggers invalid argument exception 'schema' => [ 'class' => OA\Schema::class, 'props' => [ 'externalDocs' => [], ], 'exceptionMessage' => 'Nesting Annotations is not supported.', ], ], ]]; } public function testGetOperationParameterReturnsExisting() { $name = 'operation name'; $in = 'operation in'; $parameter = new OA\Parameter(['name' => $name, 'in' => $in]); $operation = new OA\Get(['parameters' => [ new OA\Parameter([]), new OA\Parameter(['name' => 'foo']), new OA\Parameter(['in' => 'bar']), new OA\Parameter(['name' => $name, 'in' => 'bar']), new OA\Parameter(['name' => 'foo', 'in' => $in]), $parameter, ]]); $actual = Util::getOperationParameter($operation, $name, $in); $this->assertSame($parameter, $actual); } public function testGetOperationParameterCreatesWithNameAndIn() { $name = 'operation name'; $in = 'operation in'; $operation = new OA\Get(['parameters' => [ new OA\Parameter([]), new OA\Parameter(['name' => 'foo']), new OA\Parameter(['in' => 'bar']), new OA\Parameter(['name' => $name, 'in' => 'bar']), new OA\Parameter(['name' => 'foo', 'in' => $in]), ]]); $actual = Util::getOperationParameter($operation, $name, $in); $this->assertInstanceOf(OA\Parameter::class, $actual); $this->assertSame($name, $actual->name); $this->assertSame($in, $actual->in); } public function testGetOperationReturnsExisting() { $get = new OA\Get([]); $path = new OA\PathItem(['get' => $get]); $this->assertSame($get, Util::getOperation($path, 'get')); } public function testGetOperationCreatesWithPath() { $pathStr = '/testing/get/path'; $path = new OA\PathItem(['path' => $pathStr]); $get = Util::getOperation($path, 'get'); $this->assertInstanceOf(OA\Get::class, $get); $this->assertSame($pathStr, $get->path); } public function testMergeWithEmptyArray() { $api = new OA\OpenApi([]); $expected = json_encode($api); Util::merge($api, [], false); $actual = json_encode($api); $this->assertSame($expected, $actual); Util::merge($api, [], true); $actual = json_encode($api); $this->assertSame($expected, $actual); } /** * @dataProvider provideMergeData */ public function testMerge($setup, $merge, $assert) { $api = new OA\OpenApi($setup); Util::merge($api, $merge, false); $this->assertTrue($api->validate()); $actual = json_decode(json_encode($api), true); $this->assertEquals($assert, $actual); } public function provideMergeData(): array { $no = 'do not overwrite'; $yes = 'do overwrite'; $requiredInfo = ['title' => '', 'version' => '']; $setupDefaults = [ 'info' => new OA\Info($requiredInfo), 'paths' => [], ]; $assertDefaults = [ 'info' => $requiredInfo, 'openapi' => '3.0.0', 'paths' => [], ]; return [[ // simple child merge 'setup' => [ 'info' => new OA\Info(['version' => $no]), 'paths' => [], ], 'merge' => [ 'info' => ['title' => $yes, 'version' => $yes], ], 'assert' => [ 'info' => ['title' => $yes, 'version' => $no], ] + $assertDefaults, ], [ // indexed collection merge 'setup' => [ 'components' => new OA\Components([ 'schemas' => [ new OA\Schema(['schema' => $no, 'title' => $no]), ], ]), ] + $setupDefaults, 'merge' => [ 'components' => [ 'schemas' => [ $no => ['title' => $yes, 'description' => $yes], ], ], ], 'assert' => [ 'components' => [ 'schemas' => [ $no => ['title' => $no, 'description' => $yes], ], ], ] + $assertDefaults, ], [ // collection merge 'setup' => [ 'tags' => [new OA\Tag(['name' => $no])], ] + $setupDefaults, 'merge' => [ 'tags' => [ // this is actually appending right now, no clue if this is wanted, // but the complete NelmioApiDocBundle test suite is not upset by this fact ['name' => $yes], // this should not append since a tag with exactly the same properties // is already present ['name' => $no], // this does, but should not append since the name already exists, and the // docs in Tag state that the tag names must be unique, but it is complicated // and $api->validate() does not complain either ['name' => $no, 'description' => $yes], ], ], 'assert' => [ 'tags' => [ ['name' => $no], ['name' => $yes], ['name' => $no, 'description' => $yes], ], ] + $assertDefaults, ], [ // heavy nested merge array 'setup' => $setupDefaults, 'merge' => $merge = [ 'servers' => [ ['url' => 'http'], ['url' => 'https'], ], 'paths' => [ '/path/to/resource' => [ 'get' => [ 'responses' => [ '200' => [ '$ref' => '#/components/responses/default', ], ], 'requestBody' => [ 'description' => 'request foo', 'content' => [ 'foo-request' => [ 'schema' => [ 'type' => 'object', 'required' => ['baz', 'bar'], ], ], ], ], ], ], ], 'tags' => [ ['name' => 'baz'], ['name' => 'foo'], ['name' => 'baz'], ['name' => 'foo'], ['name' => 'foo'], ], 'components' => [ 'responses' => [ 'default' => [ 'description' => 'default response', 'headers' => [ 'foo-header' => [ 'schema' => [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => ['foo', 'bar', 'baz'], ], ], ], ], ], ], ], ], 'assert' => array_merge( $assertDefaults, $merge, ['tags' => \array_slice($merge['tags'], 0, 2, true)] ), ], [ // heavy nested merge array object 'setup' => $setupDefaults, 'merge' => new \ArrayObject([ 'servers' => [ ['url' => 'http'], ['url' => 'https'], ], 'paths' => [ '/path/to/resource' => [ 'get' => new \ArrayObject([ 'responses' => [ '200' => [ '$ref' => '#/components/responses/default', ], ], 'requestBody' => new \ArrayObject([ 'description' => 'request foo', 'content' => [ 'foo-request' => [ 'schema' => [ 'required' => ['baz', 'bar'], 'type' => 'object', ], ], ], ]), ]), ], ], 'tags' => new \ArrayObject([ ['name' => 'baz'], ['name' => 'foo'], new \ArrayObject(['name' => 'baz']), ['name' => 'foo'], ['name' => 'foo'], ]), 'components' => new \ArrayObject([ 'responses' => [ 'default' => [ 'description' => 'default response', 'headers' => new \ArrayObject([ 'foo-header' => new \ArrayObject([ 'schema' => new \ArrayObject([ 'type' => 'array', 'items' => new \ArrayObject([ 'type' => 'string', 'enum' => ['foo', 'bar', 'baz'], ]), ]), ]), ]), ], ], ]), ]), 'assert' => array_merge( $assertDefaults, $merge, ['tags' => \array_slice($merge['tags'], 0, 2, true)] ), ], [ // heavy nested merge swagger instance 'setup' => $setupDefaults, 'merge' => new OA\OpenApi([ 'servers' => [ new OA\Server(['url' => 'http']), new OA\Server(['url' => 'https']), ], 'paths' => [ new OA\PathItem([ 'path' => '/path/to/resource', 'get' => new OA\Get([ 'responses' => [ new OA\Response([ 'response' => '200', 'ref' => '#/components/responses/default', ]), ], 'requestBody' => new OA\RequestBody([ 'description' => 'request foo', 'content' => [ new OA\MediaType([ 'mediaType' => 'foo-request', 'schema' => new OA\Schema([ 'type' => 'object', 'required' => ['baz', 'bar'], ]), ]), ], ]), ]), ]), ], 'tags' => [ new OA\Tag(['name' => 'baz']), new OA\Tag(['name' => 'foo']), new OA\Tag(['name' => 'baz']), new OA\Tag(['name' => 'foo']), new OA\Tag(['name' => 'foo']), ], 'components' => new OA\Components([ 'responses' => [ new OA\Response([ 'response' => 'default', 'description' => 'default response', 'headers' => [ new OA\Header([ 'header' => 'foo-header', 'schema' => new OA\Schema([ 'type' => 'array', 'items' => new OA\Items([ 'type' => 'string', 'enum' => ['foo', 'bar', 'baz'], ]), ]), ]), ], ]), ], ]), ]), 'assert' => array_merge( $assertDefaults, $merge, ['tags' => \array_slice($merge['tags'], 0, 2, true)] ), ], ]; } public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child) { self::assertTrue($child->_context->is('nested')); self::assertSame($parent, $child->_context->nested); } public function assertIsConnectedToRootContext(OA\AbstractAnnotation $annotation) { $this->assertSame($this->rootContext, $annotation->_context->getRootContext()); } private function getSetupPropertiesWithoutClass(array $setup) { return array_filter($setup, function ($k) {return 'class' !== $k; }, ARRAY_FILTER_USE_KEY); } private function getNonDefaultProperties($object) { $objectVars = \get_object_vars($object); $classVars = \get_class_vars(\get_class($object)); $props = []; foreach ($objectVars as $key => $value) { if ($value !== $classVars[$key] && 0 !== \strpos($key, '_')) { $props[$key] = $value; } } return $props; } }