diff --git a/app/Http/Controllers/Apis/Marketplace/DriversApiController.php b/app/Http/Controllers/Apis/Marketplace/DriversApiController.php new file mode 100644 index 000000000..a9f8436f5 --- /dev/null +++ b/app/Http/Controllers/Apis/Marketplace/DriversApiController.php @@ -0,0 +1,97 @@ +repository = $repository; + $this->resource_server_context = $resource_server_context; + } + + protected function getResourceServerContext(): IResourceServerContext + { + return $this->resource_server_context; + } + + protected function getRepository(): IBaseRepository + { + return $this->repository; + } + + /** + * @return mixed + */ + public function getAll() + { + return $this->_getAll( + function () { + return [ + 'name' => ['=@', '==', '@@'], + 'project' => ['=@', '==', '@@'], + 'vendor' => ['=@', '==', '@@'], + 'release' => ['=@', '==', '@@'], + ]; + }, + function () { + return [ + 'name' => 'sometimes|string', + 'project' => 'sometimes|string', + 'vendor' => 'sometimes|string', + 'release' => 'sometimes|string', + ]; + }, + function () { + return [ + 'id', + 'name', + 'project', + 'vendor', + ]; + }, + function ($filter) { + return $filter; + }, + function () { + return SerializerRegistry::SerializerType_Public; + } + ); + } +} diff --git a/app/ModelSerializers/Marketplace/DriverReleaseSerializer.php b/app/ModelSerializers/Marketplace/DriverReleaseSerializer.php new file mode 100644 index 000000000..203da323d --- /dev/null +++ b/app/ModelSerializers/Marketplace/DriverReleaseSerializer.php @@ -0,0 +1,27 @@ + 'name:json_string', + 'Url' => 'url:json_string', + 'Active' => 'active:json_boolean', + ]; +} diff --git a/app/ModelSerializers/Marketplace/DriverSerializer.php b/app/ModelSerializers/Marketplace/DriverSerializer.php new file mode 100644 index 000000000..e33278a07 --- /dev/null +++ b/app/ModelSerializers/Marketplace/DriverSerializer.php @@ -0,0 +1,68 @@ + 'name:json_string', + 'Description' => 'description:json_string', + 'Project' => 'project:json_string', + 'Vendor' => 'vendor:json_string', + 'Url' => 'url:json_string', + 'Tested' => 'tested:json_boolean', + 'Active' => 'active:json_boolean', + ]; + + /** + * @param $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) + { + $driver = $this->object; + if (!$driver instanceof Driver) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + + if (in_array('releases', $relations) && !isset($values['releases'])) { + $releases = []; + foreach ($driver->getReleases() as $r) { + $releases[] = $r->getId(); + } + $values['releases'] = $releases; + } + return $values; + } + + protected static $allowed_relations = [ + 'releases', + ]; + + protected static $expand_mappings = [ + 'releases' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getReleases', + ], + ]; +} diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index 8a6ca822b..60a4025d4 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -69,6 +69,8 @@ use App\ModelSerializers\Marketplace\TrainingCourseSerializer; use App\ModelSerializers\Marketplace\TrainingCourseTypeSerializer; use App\ModelSerializers\Marketplace\TrainingServiceSerializer; +use App\ModelSerializers\Marketplace\DriverSerializer; +use App\ModelSerializers\Marketplace\DriverReleaseSerializer; use App\ModelSerializers\PushNotificationMessageSerializer; use App\ModelSerializers\ResourceServer\ApiEndpointAuthzGroupSerializer; use App\ModelSerializers\ResourceServer\ApiEndpointSerializer; @@ -668,6 +670,8 @@ private function __construct() $this->registry['RemoteCloudService'] = RemoteCloudServiceSerializer::class; $this->registry['CloudServiceOffered'] = CloudServiceOfferedSerializer::class; $this->registry['TrainingService'] = TrainingServiceSerializer::class; + $this->registry['Driver'] = DriverSerializer::class; + $this->registry['DriverRelease'] = DriverReleaseSerializer::class; $this->registry['TrainingCourse'] = TrainingCourseSerializer::class; $this->registry['TrainingCourseType'] = TrainingCourseTypeSerializer::class; $this->registry['TrainingCourseLevel'] = TrainingCourseLevelSerializer::class; diff --git a/app/Models/Foundation/Marketplace/Driver.php b/app/Models/Foundation/Marketplace/Driver.php new file mode 100644 index 000000000..b0f471d4a --- /dev/null +++ b/app/Models/Foundation/Marketplace/Driver.php @@ -0,0 +1,235 @@ +releases = new ArrayCollection(); + } + + /** + * @return string + */ + public function getClassName(): string + { + return self::ClassName; + } + + /** + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @return string|null + */ + public function getProject(): ?string + { + return $this->project; + } + + /** + * @return string|null + */ + public function getVendor(): ?string + { + return $this->vendor; + } + + /** + * @return string|null + */ + public function getUrl(): ?string + { + return $this->url; + } + + /** + * @return bool + */ + public function isTested(): bool + { + return $this->tested; + } + + /** + * @return bool + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @param string|null $description + */ + public function setDescription(?string $description): void + { + $this->description = $description; + } + + /** + * @param string|null $project + */ + public function setProject(?string $project): void + { + $this->project = $project; + } + + /** + * @param string|null $vendor + */ + public function setVendor(?string $vendor): void + { + $this->vendor = $vendor; + } + + /** + * @param string|null $url + */ + public function setUrl(?string $url): void + { + $this->url = $url; + } + + /** + * @param bool $tested + */ + public function setTested(bool $tested): void + { + $this->tested = $tested; + } + + /** + * @param bool $active + */ + public function setActive(bool $active): void + { + $this->active = $active; + } + + /** + * @param DriverRelease $release + */ + public function addRelease(DriverRelease $release): void + { + if (!$this->releases->contains($release)) { + $this->releases->add($release); + $release->getDrivers()->add($this); + } + } + + /** + * @return ArrayCollection|DriverRelease[] + */ + public function getReleases() + { + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('active', true)); + return $this->releases->matching($criteria); + } + + /** + * @return ArrayCollection|DriverRelease[] + */ + public function getAllReleases() + { + return $this->releases; + } +} diff --git a/app/Models/Foundation/Marketplace/DriverRelease.php b/app/Models/Foundation/Marketplace/DriverRelease.php new file mode 100644 index 000000000..2c51ea543 --- /dev/null +++ b/app/Models/Foundation/Marketplace/DriverRelease.php @@ -0,0 +1,143 @@ +drivers = new ArrayCollection(); + } + + /** + * @return string + */ + public function getClassName(): string + { + return self::ClassName; + } + + /** + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @return string|null + */ + public function getUrl(): ?string + { + return $this->url; + } + + /** + * @return \DateTime|null + */ + public function getStart(): ?\DateTime + { + return $this->start; + } + + /** + * @return bool + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @param string $name + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @param string|null $url + */ + public function setUrl(?string $url): void + { + $this->url = $url; + } + + /** + * @param \DateTime|null $start + */ + public function setStart(?\DateTime $start): void + { + $this->start = $start; + } + + /** + * @param bool $active + */ + public function setActive(bool $active): void + { + $this->active = $active; + } + + /** + * @return ArrayCollection|Driver[] + */ + public function getDrivers() + { + return $this->drivers; + } +} diff --git a/app/Models/Foundation/Marketplace/IDriverRepository.php b/app/Models/Foundation/Marketplace/IDriverRepository.php new file mode 100644 index 000000000..9054aed7c --- /dev/null +++ b/app/Models/Foundation/Marketplace/IDriverRepository.php @@ -0,0 +1,23 @@ +andWhere('e.active = 1'); + return $query; + } + + /** + * @return array + */ + protected function getFilterMappings() + { + return [ + 'name' => 'e.name', + 'project' => 'e.project', + 'vendor' => 'e.vendor', + 'release' => new DoctrineJoinFilterMapping( + 'e.releases', + 'r', + "r.name :operator :value" + ), + ]; + } + + /** + * @return array + */ + protected function getOrderMappings() + { + return [ + 'id' => 'e.id', + 'name' => 'e.name', + 'project' => 'e.project', + 'vendor' => 'e.vendor', + ]; + } +} diff --git a/app/Repositories/RepositoriesProvider.php b/app/Repositories/RepositoriesProvider.php index c547239fc..72543b05b 100644 --- a/app/Repositories/RepositoriesProvider.php +++ b/app/Repositories/RepositoriesProvider.php @@ -23,6 +23,7 @@ use App\Models\Foundation\Main\Repositories\ISupportingCompanyRepository; use App\Models\Foundation\Main\Repositories\IUserStoryRepository; use App\Models\Foundation\Marketplace\ICompanyServiceRepository; +use App\Models\Foundation\Marketplace\IDriverRepository; use App\Models\Foundation\Marketplace\IReviewRepository; use App\Models\Foundation\Marketplace\ITrainingRepository; use App\Models\Foundation\Marketplace\MarketPlaceReview; @@ -370,6 +371,12 @@ function () { return EntityManager::getRepository(\App\Models\Foundation\Marketplace\RemoteCloudService::class); }); + App::singleton( + 'App\Models\Foundation\Marketplace\IDriverRepository', + function () { + return EntityManager::getRepository(\App\Models\Foundation\Marketplace\Driver::class); + }); + App::singleton( 'models\main\IFolderRepository', function () { diff --git a/routes/public_api.php b/routes/public_api.php index 39315253c..0ea35f28c 100644 --- a/routes/public_api.php +++ b/routes/public_api.php @@ -275,6 +275,10 @@ Route::group(array('prefix' => 'trainings'), function () { Route::get('', 'TrainingApiController@getAll'); }); + + Route::group(array('prefix' => 'drivers'), function () { + Route::get('', 'DriversApiController@getAll'); + }); }); // countries diff --git a/tests/OAuth2DriversApiTest.php b/tests/OAuth2DriversApiTest.php new file mode 100644 index 000000000..a5f41825e --- /dev/null +++ b/tests/OAuth2DriversApiTest.php @@ -0,0 +1,388 @@ +isOpen()) { + self::$em = Registry::resetManager(SilverstripeBaseModel::EntityManager); + } + + // clean up any leftover test data + self::clearDriverTestData(); + + // create releases + self::$release1 = new DriverRelease(); + self::$release1->setName('Rocky'); + self::$release1->setUrl('https://releases.openstack.org/rocky'); + self::$release1->setActive(true); + + self::$release2 = new DriverRelease(); + self::$release2->setName('Stein'); + self::$release2->setUrl('https://releases.openstack.org/stein'); + self::$release2->setActive(true); + + self::$em->persist(self::$release1); + self::$em->persist(self::$release2); + + // create active drivers + self::$driver1 = new Driver(); + self::$driver1->setName('Test Driver Nova'); + self::$driver1->setDescription('A test nova driver'); + self::$driver1->setProject('Nova'); + self::$driver1->setVendor('TestVendorA'); + self::$driver1->setUrl('https://example.com/driver1'); + self::$driver1->setTested(true); + self::$driver1->setActive(true); + + self::$driver2 = new Driver(); + self::$driver2->setName('Test Driver Cinder'); + self::$driver2->setDescription('A test cinder driver'); + self::$driver2->setProject('Cinder'); + self::$driver2->setVendor('TestVendorB'); + self::$driver2->setUrl('https://example.com/driver2'); + self::$driver2->setTested(false); + self::$driver2->setActive(true); + + // create inactive driver (should not appear in results) + self::$inactiveDriver = new Driver(); + self::$inactiveDriver->setName('Inactive Driver'); + self::$inactiveDriver->setProject('Nova'); + self::$inactiveDriver->setVendor('InactiveVendor'); + self::$inactiveDriver->setActive(false); + + // link releases to drivers + self::$driver1->addRelease(self::$release1); + self::$driver1->addRelease(self::$release2); + self::$driver2->addRelease(self::$release1); + + self::$em->persist(self::$driver1); + self::$em->persist(self::$driver2); + self::$em->persist(self::$inactiveDriver); + self::$em->flush(); + } + + protected function tearDown(): void + { + self::clearDriverTestData(); + parent::tearDown(); + } + + private static function clearDriverTestData(): void + { + // clean junction table first, then entities + DB::connection('model')->delete("DELETE FROM Driver_Releases"); + DB::connection('model')->delete("DELETE FROM Driver WHERE Name LIKE 'Test Driver%' OR Name = 'Inactive Driver'"); + DB::connection('model')->delete("DELETE FROM DriverRelease WHERE Name IN ('Rocky','Stein')"); + if (isset(self::$em) && self::$em->isOpen()) { + self::$em->clear(); + } + } + + public function testGetAllDrivers() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + // should contain at least our 2 active drivers, but NOT the inactive one + $this->assertTrue($page->total >= 2); + + // verify inactive driver is not in results + $names = array_map(function ($d) { return $d->name; }, $page->data); + $this->assertNotContains('Inactive Driver', $names); + } + + public function testGetAllDriversWithExpandReleases() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'expand' => 'releases', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 2); + + // find our test driver1 and verify it has expanded releases + $driver1 = null; + foreach ($page->data as $d) { + if ($d->name === 'Test Driver Nova') { + $driver1 = $d; + break; + } + } + $this->assertNotNull($driver1); + $this->assertTrue(is_array($driver1->releases)); + $this->assertTrue(count($driver1->releases) >= 2); + } + + public function testGetDriversFilterByProject() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'filter' => 'project==Nova', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 1); + + // all returned drivers should have project Nova + foreach ($page->data as $d) { + $this->assertEquals('Nova', $d->project); + } + } + + public function testGetDriversFilterByVendor() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'filter' => 'vendor@@TestVendorA', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 1); + + foreach ($page->data as $d) { + $this->assertStringContainsString('TestVendorA', $d->vendor); + } + } + + public function testGetDriversFilterByName() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'filter' => 'name=@Cinder', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 1); + + foreach ($page->data as $d) { + $this->assertStringContainsString('Cinder', $d->name); + } + } + + public function testGetDriversFilterByRelease() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'filter' => 'release==Rocky', + 'expand' => 'releases', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + // both test drivers have Rocky release + $this->assertTrue($page->total >= 2); + } + + public function testGetDriversOrderByName() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'order' => '+name', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 2); + + // verify ascending order + for ($i = 1; $i < count($page->data); $i++) { + $this->assertTrue( + strcmp($page->data[$i - 1]->name, $page->data[$i]->name) <= 0 + ); + } + } + + public function testGetDriversOrderByProject() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'order' => '+project', + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total >= 2); + } + + public function testGetDriversResponseFields() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + ]; + + $response = $this->action( + "GET", + "DriversApiController@getAll", + $params, + [], [], [], [] + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + + // find our test driver and verify serialized fields + $driver = null; + foreach ($page->data as $d) { + if ($d->name === 'Test Driver Nova') { + $driver = $d; + break; + } + } + $this->assertNotNull($driver); + $this->assertEquals('Test Driver Nova', $driver->name); + $this->assertEquals('A test nova driver', $driver->description); + $this->assertEquals('Nova', $driver->project); + $this->assertEquals('TestVendorA', $driver->vendor); + $this->assertEquals('https://example.com/driver1', $driver->url); + $this->assertTrue($driver->tested); + $this->assertTrue($driver->active); + } +}