From 06d9d7375565044fd583bdb4ba1490cab47dc07a Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 19 Feb 2026 11:46:43 -0300 Subject: [PATCH] feat(marketplace): add public endpoint GET /api/public/v1/marketplace/trainings --- .gitignore | 3 +- .../Marketplace/TrainingApiController.php | 33 ++ .../Marketplace/ProjectSerializer.php | 26 ++ .../TrainingCourseLevelSerializer.php | 25 ++ .../TrainingCoursePrerequisiteSerializer.php | 24 ++ .../TrainingCourseScheduleSerializer.php | 63 ++++ .../TrainingCourseScheduleTimeSerializer.php | 41 +++ .../Marketplace/TrainingCourseSerializer.php | 116 +++++++ .../TrainingCourseTypeSerializer.php | 24 ++ .../Marketplace/TrainingServiceSerializer.php | 41 ++- app/ModelSerializers/SerializerRegistry.php | 14 + .../Marketplace/ITrainingRepository.php | 19 ++ app/Models/Foundation/Marketplace/Project.php | 67 ++++ .../Foundation/Marketplace/TrainingCourse.php | 187 ++++++++++ .../Marketplace/TrainingCourseLevel.php | 54 +++ .../TrainingCoursePrerequisite.php | 47 +++ .../Marketplace/TrainingCourseSchedule.php | 99 ++++++ .../TrainingCourseScheduleTime.php | 90 +++++ .../Marketplace/TrainingCourseType.php | 44 +++ .../Marketplace/TrainingService.php | 23 +- .../DoctrineTrainingRepository.php | 27 ++ app/Repositories/RepositoriesProvider.php | 8 +- routes/public_api.php | 4 + tests/TrainingApiTest.php | 90 +++++ tests/TrainingSerializerTest.php | 319 ++++++++++++++++++ 25 files changed, 1481 insertions(+), 7 deletions(-) create mode 100644 app/Http/Controllers/Apis/Marketplace/TrainingApiController.php create mode 100644 app/ModelSerializers/Marketplace/ProjectSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCourseLevelSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCoursePrerequisiteSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCourseScheduleSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCourseScheduleTimeSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCourseSerializer.php create mode 100644 app/ModelSerializers/Marketplace/TrainingCourseTypeSerializer.php create mode 100644 app/Models/Foundation/Marketplace/ITrainingRepository.php create mode 100644 app/Models/Foundation/Marketplace/Project.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCourse.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCourseLevel.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCoursePrerequisite.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCourseSchedule.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCourseScheduleTime.php create mode 100644 app/Models/Foundation/Marketplace/TrainingCourseType.php create mode 100644 app/Repositories/Marketplace/DoctrineTrainingRepository.php create mode 100644 tests/TrainingApiTest.php create mode 100644 tests/TrainingSerializerTest.php diff --git a/.gitignore b/.gitignore index 3ebf72290..7881753f4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ routes.txt /ss.sql phpunit.xml .phpunit.result.cache -.phpunit.cache/ \ No newline at end of file +.phpunit.cache/ +.claude \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Marketplace/TrainingApiController.php b/app/Http/Controllers/Apis/Marketplace/TrainingApiController.php new file mode 100644 index 000000000..65291a427 --- /dev/null +++ b/app/Http/Controllers/Apis/Marketplace/TrainingApiController.php @@ -0,0 +1,33 @@ + 'name:json_string', + 'Description' => 'description:json_string', + 'Codename' => 'codename:json_string', + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCourseLevelSerializer.php b/app/ModelSerializers/Marketplace/TrainingCourseLevelSerializer.php new file mode 100644 index 000000000..49e654159 --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCourseLevelSerializer.php @@ -0,0 +1,25 @@ + 'level:json_string', + 'Order' => 'order:json_int', + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCoursePrerequisiteSerializer.php b/app/ModelSerializers/Marketplace/TrainingCoursePrerequisiteSerializer.php new file mode 100644 index 000000000..cf909fa70 --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCoursePrerequisiteSerializer.php @@ -0,0 +1,24 @@ + 'name:json_string', + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCourseScheduleSerializer.php b/app/ModelSerializers/Marketplace/TrainingCourseScheduleSerializer.php new file mode 100644 index 000000000..e39595c2e --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCourseScheduleSerializer.php @@ -0,0 +1,63 @@ + 'city:json_string', + 'State' => 'state:json_string', + 'Country' => 'country:json_string', + ]; + + protected static $allowed_relations = [ + 'times', + ]; + + /** + * @param null $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) + { + $schedule = $this->object; + if (!$schedule instanceof TrainingCourseSchedule) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + + if (in_array('times', $relations) && !isset($values['times'])) { + $times = []; + foreach ($schedule->getTimes() as $t) { + $times[] = $t->getId(); + } + $values['times'] = $times; + } + + return $values; + } + + protected static $expand_mappings = [ + 'times' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getTimes', + ], + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCourseScheduleTimeSerializer.php b/app/ModelSerializers/Marketplace/TrainingCourseScheduleTimeSerializer.php new file mode 100644 index 000000000..e087cbe15 --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCourseScheduleTimeSerializer.php @@ -0,0 +1,41 @@ + 'start_date:datetime_epoch', + 'EndDate' => 'end_date:datetime_epoch', + 'Link' => 'link:json_string', + 'LocationId' => 'location_id:json_int', + ]; + + protected static $allowed_relations = [ + 'location', + ]; + + protected static $expand_mappings = [ + 'location' => [ + 'type' => One2ManyExpandSerializer::class, + 'original_attribute' => 'location_id', + 'getter' => 'getLocation', + 'has' => 'hasLocation', + ], + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCourseSerializer.php b/app/ModelSerializers/Marketplace/TrainingCourseSerializer.php new file mode 100644 index 000000000..e829bad48 --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCourseSerializer.php @@ -0,0 +1,116 @@ + 'name:json_string', + 'Link' => 'link:json_string', + 'Description' => 'description:json_string', + 'Paid' => 'is_paid:json_boolean', + 'Online' => 'is_online:json_boolean', + 'TypeId' => 'type_id:json_int', + 'LevelId' => 'level_id:json_int', + 'TrainingServiceId' => 'training_service_id:json_int', + ]; + + protected static $allowed_relations = [ + 'type', + 'level', + 'training_service', + 'schedules', + 'projects', + 'prerequisites', + ]; + + /** + * @param null $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) + { + $course = $this->object; + if (!$course instanceof TrainingCourse) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + + if (in_array('schedules', $relations) && !isset($values['schedules'])) { + $schedules = []; + foreach ($course->getSchedules() as $s) { + $schedules[] = $s->getId(); + } + $values['schedules'] = $schedules; + } + + if (in_array('projects', $relations) && !isset($values['projects'])) { + $projects = []; + foreach ($course->getProjects() as $p) { + $projects[] = $p->getId(); + } + $values['projects'] = $projects; + } + + if (in_array('prerequisites', $relations) && !isset($values['prerequisites'])) { + $prerequisites = []; + foreach ($course->getPrerequisites() as $p) { + $prerequisites[] = $p->getId(); + } + $values['prerequisites'] = $prerequisites; + } + + return $values; + } + + protected static $expand_mappings = [ + 'type' => [ + 'type' => One2ManyExpandSerializer::class, + 'original_attribute' => 'type_id', + 'getter' => 'getType', + 'has' => 'hasType', + ], + 'level' => [ + 'type' => One2ManyExpandSerializer::class, + 'original_attribute' => 'level_id', + 'getter' => 'getLevel', + 'has' => 'hasLevel', + ], + 'training_service' => [ + 'type' => One2ManyExpandSerializer::class, + 'original_attribute' => 'training_service_id', + 'getter' => 'getTrainingService', + 'has' => 'hasTrainingService', + ], + 'schedules' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getSchedules', + ], + 'projects' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getProjects', + ], + 'prerequisites' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getPrerequisites', + ], + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingCourseTypeSerializer.php b/app/ModelSerializers/Marketplace/TrainingCourseTypeSerializer.php new file mode 100644 index 000000000..d7c58e255 --- /dev/null +++ b/app/ModelSerializers/Marketplace/TrainingCourseTypeSerializer.php @@ -0,0 +1,24 @@ + 'type:json_string', + ]; +} diff --git a/app/ModelSerializers/Marketplace/TrainingServiceSerializer.php b/app/ModelSerializers/Marketplace/TrainingServiceSerializer.php index 74c4def4a..2348b902c 100644 --- a/app/ModelSerializers/Marketplace/TrainingServiceSerializer.php +++ b/app/ModelSerializers/Marketplace/TrainingServiceSerializer.php @@ -1,5 +1,5 @@ object; + if (!$training_service instanceof TrainingService) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + + if (in_array('courses', $relations) && !isset($values['courses'])) { + $courses = []; + foreach ($training_service->getCourses() as $c) { + $courses[] = $c->getId(); + } + $values['courses'] = $courses; + } + + return $values; + } -} \ No newline at end of file + protected static $expand_mappings = [ + 'courses' => [ + 'type' => Many2OneExpandSerializer::class, + 'getter' => 'getCourses', + ], + ]; +} diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index 421c3729a..8a6ca822b 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -61,6 +61,13 @@ use App\ModelSerializers\Marketplace\ServiceOfferedTypeSerializer; use App\ModelSerializers\Marketplace\SpokenLanguageSerializer; use App\ModelSerializers\Marketplace\SupportChannelTypeSerializer; +use App\ModelSerializers\Marketplace\ProjectSerializer; +use App\ModelSerializers\Marketplace\TrainingCourseLevelSerializer; +use App\ModelSerializers\Marketplace\TrainingCoursePrerequisiteSerializer; +use App\ModelSerializers\Marketplace\TrainingCourseScheduleSerializer; +use App\ModelSerializers\Marketplace\TrainingCourseScheduleTimeSerializer; +use App\ModelSerializers\Marketplace\TrainingCourseSerializer; +use App\ModelSerializers\Marketplace\TrainingCourseTypeSerializer; use App\ModelSerializers\Marketplace\TrainingServiceSerializer; use App\ModelSerializers\PushNotificationMessageSerializer; use App\ModelSerializers\ResourceServer\ApiEndpointAuthzGroupSerializer; @@ -661,6 +668,13 @@ private function __construct() $this->registry['RemoteCloudService'] = RemoteCloudServiceSerializer::class; $this->registry['CloudServiceOffered'] = CloudServiceOfferedSerializer::class; $this->registry['TrainingService'] = TrainingServiceSerializer::class; + $this->registry['TrainingCourse'] = TrainingCourseSerializer::class; + $this->registry['TrainingCourseType'] = TrainingCourseTypeSerializer::class; + $this->registry['TrainingCourseLevel'] = TrainingCourseLevelSerializer::class; + $this->registry['TrainingCoursePrerequisite'] = TrainingCoursePrerequisiteSerializer::class; + $this->registry['TrainingCourseSchedule'] = TrainingCourseScheduleSerializer::class; + $this->registry['TrainingCourseScheduleTime'] = TrainingCourseScheduleTimeSerializer::class; + $this->registry['Project'] = ProjectSerializer::class; $this->registry['AvailabilityZone'] = AvailabilityZoneSerializer::class; // software diff --git a/app/Models/Foundation/Marketplace/ITrainingRepository.php b/app/Models/Foundation/Marketplace/ITrainingRepository.php new file mode 100644 index 000000000..3bf9bd6a1 --- /dev/null +++ b/app/Models/Foundation/Marketplace/ITrainingRepository.php @@ -0,0 +1,19 @@ +name; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getCodename(): ?string + { + return $this->codename; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCourse.php b/app/Models/Foundation/Marketplace/TrainingCourse.php new file mode 100644 index 000000000..68885f490 --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCourse.php @@ -0,0 +1,187 @@ + 'type', + 'getLevelId' => 'level', + 'getTrainingServiceId' => 'training_service', + ]; + + protected $hasPropertyMappings = [ + 'hasType' => 'type', + 'hasLevel' => 'level', + 'hasTrainingService' => 'training_service', + ]; + + const ClassName = 'TrainingCourse'; + + /** + * @return string + */ + public function getClassName():string + { + return self::ClassName; + } + + /** + * @ORM\Column(name="Name", type="string") + * @var string + */ + protected $name; + + /** + * @ORM\Column(name="Link", type="string") + * @var string + */ + protected $link; + + /** + * @ORM\Column(name="Description", type="string") + * @var string + */ + protected $description; + + /** + * @ORM\Column(name="Paid", type="boolean") + * @var bool + */ + protected $is_paid; + + /** + * @ORM\Column(name="Online", type="boolean") + * @var bool + */ + protected $is_online; + + /** + * @ORM\ManyToOne(targetEntity="TrainingCourseType") + * @ORM\JoinColumn(name="TypeID", referencedColumnName="ID") + * @var TrainingCourseType + */ + protected $type; + + /** + * @ORM\ManyToOne(targetEntity="TrainingCourseLevel") + * @ORM\JoinColumn(name="LevelID", referencedColumnName="ID") + * @var TrainingCourseLevel + */ + protected $level; + + /** + * @ORM\ManyToOne(targetEntity="TrainingService",inversedBy="courses", fetch="LAZY") + * @ORM\JoinColumn(name="TrainingServiceID", referencedColumnName="ID") + * @var TrainingService + */ + protected $training_service; + + /** + * @ORM\OneToMany(targetEntity="TrainingCourseSchedule", mappedBy="course", cascade={"persist"}, orphanRemoval=true) + * @var TrainingCourseSchedule[] + */ + protected $schedules; + + + /** + * @ORM\ManyToMany(targetEntity="Project", fetch="EXTRA_LAZY") + * @ORM\JoinTable(name="TrainingCourse_Projects", + * joinColumns={@ORM\JoinColumn(name="TrainingCourseID", referencedColumnName="ID")}, + * inverseJoinColumns={@ORM\JoinColumn(name="ProjectID", referencedColumnName="ID")} + * ) + * @var Project[] + */ + private $projects; + + /** + * @ORM\ManyToMany(targetEntity="TrainingCoursePrerequisite", fetch="EXTRA_LAZY") + * @ORM\JoinTable(name="TrainingCourse_Prerequisites", + * joinColumns={@ORM\JoinColumn(name="TrainingCourseID", referencedColumnName="ID")}, + * inverseJoinColumns={@ORM\JoinColumn(name="TrainingCoursePrerequisiteID", referencedColumnName="ID")} + * ) + * @var TrainingCoursePrerequisite[] + */ + private $prerequisites; + + public function getName(): ?string + { + return $this->name; + } + + public function getLink(): ?string + { + return $this->link; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function isPaid(): bool + { + return $this->is_paid; + } + + public function isOnline(): bool + { + return $this->is_online; + } + + public function getType(): TrainingCourseType + { + return $this->type; + } + + public function getLevel(): TrainingCourseLevel + { + return $this->level; + } + + public function getTrainingService(): TrainingService + { + return $this->training_service; + } + + public function __construct(){ + parent::__construct(); + $this->schedules = new ArrayCollection(); + $this->projects = new ArrayCollection(); + $this->prerequisites = new ArrayCollection(); + } + + public function getSchedules(){ + return $this->schedules; + } + + public function getProjects(){ + return $this->projects; + } + + public function getPrerequisites(){ + return $this->prerequisites; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCourseLevel.php b/app/Models/Foundation/Marketplace/TrainingCourseLevel.php new file mode 100644 index 000000000..1d23a998d --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCourseLevel.php @@ -0,0 +1,54 @@ +level; + } + + + /** + * @ORM\Column(name="SortOrder", type="integer") + * @var int + */ + protected $order; + + public function getOrder():?int{ + return $this->order; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCoursePrerequisite.php b/app/Models/Foundation/Marketplace/TrainingCoursePrerequisite.php new file mode 100644 index 000000000..c748e97b7 --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCoursePrerequisite.php @@ -0,0 +1,47 @@ +name; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCourseSchedule.php b/app/Models/Foundation/Marketplace/TrainingCourseSchedule.php new file mode 100644 index 000000000..e338125c3 --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCourseSchedule.php @@ -0,0 +1,99 @@ +course; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function getState(): ?string + { + return $this->state; + } + + public function getCountry(): ?string + { + return $this->country; + } + + public function __construct(){ + parent::__construct(); + $this->times = new ArrayCollection(); + } + + /** + * @return TrainingCourseScheduleTime[] + */ + public function getTimes() + { + return $this->times; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCourseScheduleTime.php b/app/Models/Foundation/Marketplace/TrainingCourseScheduleTime.php new file mode 100644 index 000000000..d5ac718db --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCourseScheduleTime.php @@ -0,0 +1,90 @@ + 'location', + ]; + + protected $hasPropertyMappings = [ + 'hasLocation' => 'location', + ]; + + const ClassName = 'TrainingCourseScheduleTime'; + + /** + * @return string + */ + public function getClassName():string + { + return self::ClassName; + } + + /** + * @var \DateTime + * @ORM\Column(name="StartDate", type="datetime", nullable=false) + */ + protected $start_date; + + /** + * @var \DateTime + * @ORM\Column(name="EndDate", type="datetime", nullable=false) + */ + protected $end_date; + + /** + * @ORM\Column(name="Link", type="string") + * @var string + */ + protected $link; + + /** + * @ORM\ManyToOne(targetEntity="TrainingCourseSchedule",inversedBy="times", fetch="LAZY") + * @ORM\JoinColumn(name="LocationID", referencedColumnName="ID") + * @var TrainingCourseSchedule + */ + protected $location; + + public function getStartDate(): ?\DateTime + { + return $this->start_date; + } + + public function getEndDate(): ?\DateTime + { + return $this->end_date; + } + + public function getLink(): ?string + { + return $this->link; + } + + public function getLocation(): TrainingCourseSchedule + { + return $this->location; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingCourseType.php b/app/Models/Foundation/Marketplace/TrainingCourseType.php new file mode 100644 index 000000000..9f953162a --- /dev/null +++ b/app/Models/Foundation/Marketplace/TrainingCourseType.php @@ -0,0 +1,44 @@ +type; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Marketplace/TrainingService.php b/app/Models/Foundation/Marketplace/TrainingService.php index 84ec65010..216745c57 100644 --- a/app/Models/Foundation/Marketplace/TrainingService.php +++ b/app/Models/Foundation/Marketplace/TrainingService.php @@ -12,9 +12,10 @@ * limitations under the License. **/ +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping AS ORM; /** - * @ORM\Entity + * @ORM\Entity(repositoryClass="App\Repositories\Marketplace\DoctrineTrainingRepository") * @ORM\Table(name="TrainingService") * Class Distribution * @package App\Models\Foundation\Marketplace @@ -30,4 +31,24 @@ public function getClassName():string { return self::ClassName; } + + /** + * @ORM\OneToMany(targetEntity="TrainingCourse", mappedBy="training_service", cascade={"persist"}, orphanRemoval=true) + * @var TrainingCourse[] + */ + protected $courses; + + public function __construct() + { + parent::__construct(); + $this->courses = new ArrayCollection(); + } + + /** + * @return TrainingCourse[] + */ + public function getCourses() + { + return $this->courses; + } } \ No newline at end of file diff --git a/app/Repositories/Marketplace/DoctrineTrainingRepository.php b/app/Repositories/Marketplace/DoctrineTrainingRepository.php new file mode 100644 index 000000000..c5d4a6bc1 --- /dev/null +++ b/app/Repositories/Marketplace/DoctrineTrainingRepository.php @@ -0,0 +1,27 @@ + 'public-clouds'), function () { Route::get('', 'PublicCloudsApiController@getAll'); }); + + Route::group(array('prefix' => 'trainings'), function () { + Route::get('', 'TrainingApiController@getAll'); + }); }); // countries diff --git a/tests/TrainingApiTest.php b/tests/TrainingApiTest.php new file mode 100644 index 000000000..521128b2d --- /dev/null +++ b/tests/TrainingApiTest.php @@ -0,0 +1,90 @@ + 1, + 'per_page' => 100, + ]; + + $response = $this->action( + "GET", + "TrainingApiController@getAll", + $params, + [], + [], + [], + [] + ); + + $content = $response->getContent(); + $trainings = json_decode($content); + $this->assertTrue(!is_null($trainings)); + $this->assertResponseStatus(200); + } + + public function testGetAllTrainingsWithExpand() + { + $params = [ + 'page' => 1, + 'per_page' => 100, + 'expand' => 'company,courses', + ]; + + $response = $this->action( + "GET", + "TrainingApiController@getAll", + $params, + [], + [], + [], + [] + ); + + $content = $response->getContent(); + $trainings = json_decode($content); + $this->assertTrue(!is_null($trainings)); + $this->assertResponseStatus(200); + } + + public function testGetAllTrainingsWithFilter() + { + $params = [ + 'filter' => 'name@@test', + 'order' => '+name', + ]; + + $response = $this->action( + "GET", + "TrainingApiController@getAll", + $params, + [], + [], + [], + [] + ); + + $content = $response->getContent(); + $trainings = json_decode($content); + $this->assertTrue(!is_null($trainings)); + $this->assertResponseStatus(200); + } +} diff --git a/tests/TrainingSerializerTest.php b/tests/TrainingSerializerTest.php new file mode 100644 index 000000000..36c4b0c55 --- /dev/null +++ b/tests/TrainingSerializerTest.php @@ -0,0 +1,319 @@ +resource_server_context = Mockery::mock(IResourceServerContext::class); + } + + public function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + /** + * Helper to create a base entity mock with standard SilverstripeBaseModel getters. + */ + private function mockBaseEntity(string $class, int $id = 1): \Mockery\MockInterface + { + $mock = Mockery::mock($class)->makePartial(); + $mock->shouldReceive('getId')->andReturn($id); + $mock->shouldReceive('getIdentifier')->andReturn($id); + $mock->shouldReceive('getCreatedUTC')->andReturn(new \DateTime('2026-01-01 00:00:00')); + $mock->shouldReceive('getLastEditedUTC')->andReturn(new \DateTime('2026-01-02 00:00:00')); + return $mock; + } + + // --- ProjectSerializer --- + + public function testProjectSerializer() + { + $project = $this->mockBaseEntity(Project::class, 10); + $project->shouldReceive('getName')->andReturn('OpenStack Compute'); + $project->shouldReceive('getDescription')->andReturn('Compute service'); + $project->shouldReceive('getCodename')->andReturn('Nova'); + + $serializer = new SerializerDecorator(new ProjectSerializer($project, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(10, $values['id']); + $this->assertEquals('OpenStack Compute', $values['name']); + $this->assertEquals('Compute service', $values['description']); + $this->assertEquals('Nova', $values['codename']); + } + + // --- TrainingCourseTypeSerializer --- + + public function testTrainingCourseTypeSerializer() + { + $type = $this->mockBaseEntity(TrainingCourseType::class, 5); + $type->shouldReceive('getType')->andReturn('Instructor Led'); + $type->shouldReceive('getClassName')->andReturn('TrainingCourseType'); + + $serializer = new SerializerDecorator(new TrainingCourseTypeSerializer($type, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(5, $values['id']); + $this->assertEquals('Instructor Led', $values['type']); + } + + // --- TrainingCourseLevelSerializer --- + + public function testTrainingCourseLevelSerializer() + { + $level = $this->mockBaseEntity(TrainingCourseLevel::class, 3); + $level->shouldReceive('getLevel')->andReturn('Intermediate'); + $level->shouldReceive('getOrder')->andReturn(2); + $level->shouldReceive('getClassName')->andReturn('TrainingCourseLevel'); + + $serializer = new SerializerDecorator(new TrainingCourseLevelSerializer($level, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(3, $values['id']); + $this->assertEquals('Intermediate', $values['level']); + $this->assertEquals(2, $values['order']); + } + + // --- TrainingCoursePrerequisiteSerializer --- + + public function testTrainingCoursePrerequisiteSerializer() + { + $prerequisite = $this->mockBaseEntity(TrainingCoursePrerequisite::class, 7); + $prerequisite->shouldReceive('getName')->andReturn('Basic Linux Skills'); + $prerequisite->shouldReceive('getClassName')->andReturn('TrainingCoursePrerequisite'); + + $serializer = new SerializerDecorator(new TrainingCoursePrerequisiteSerializer($prerequisite, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(7, $values['id']); + $this->assertEquals('Basic Linux Skills', $values['name']); + } + + // --- TrainingCourseScheduleTimeSerializer --- + + public function testTrainingCourseScheduleTimeSerializer() + { + $time = $this->mockBaseEntity(TrainingCourseScheduleTime::class, 20); + $time->shouldReceive('getStartDate')->andReturn(new \DateTime('2026-03-01 09:00:00')); + $time->shouldReceive('getEndDate')->andReturn(new \DateTime('2026-03-01 17:00:00')); + $time->shouldReceive('getLink')->andReturn('https://example.com/register'); + $time->shouldReceive('getLocationId')->andReturn(50); + $time->shouldReceive('getClassName')->andReturn('TrainingCourseScheduleTime'); + + $serializer = new SerializerDecorator(new TrainingCourseScheduleTimeSerializer($time, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(20, $values['id']); + $this->assertIsInt($values['start_date']); + $this->assertIsInt($values['end_date']); + $this->assertEquals('https://example.com/register', $values['link']); + $this->assertEquals(50, $values['location_id']); + } + + // --- TrainingCourseScheduleSerializer --- + + public function testTrainingCourseScheduleSerializer() + { + $time1 = $this->mockBaseEntity(TrainingCourseScheduleTime::class, 20); + $time2 = $this->mockBaseEntity(TrainingCourseScheduleTime::class, 21); + + $schedule = $this->mockBaseEntity(TrainingCourseSchedule::class, 50); + $schedule->shouldReceive('getCity')->andReturn('Austin'); + $schedule->shouldReceive('getState')->andReturn('TX'); + $schedule->shouldReceive('getCountry')->andReturn('US'); + $schedule->shouldReceive('getTimes')->andReturn([$time1, $time2]); + $schedule->shouldReceive('getClassName')->andReturn('TrainingCourseSchedule'); + + $serializer = new SerializerDecorator(new TrainingCourseScheduleSerializer($schedule, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(50, $values['id']); + $this->assertEquals('Austin', $values['city']); + $this->assertEquals('TX', $values['state']); + $this->assertEquals('US', $values['country']); + $this->assertIsArray($values['times']); + $this->assertCount(2, $values['times']); + $this->assertEquals([20, 21], $values['times']); + } + + // --- TrainingCourseSerializer --- + + public function testTrainingCourseSerializer() + { + $schedule1 = $this->mockBaseEntity(TrainingCourseSchedule::class, 50); + $project1 = $this->mockBaseEntity(Project::class, 10); + $prereq1 = $this->mockBaseEntity(TrainingCoursePrerequisite::class, 7); + + $course = $this->mockBaseEntity(TrainingCourse::class, 100); + $course->shouldReceive('getName')->andReturn('OpenStack Administration'); + $course->shouldReceive('getLink')->andReturn('https://example.com/course'); + $course->shouldReceive('getDescription')->andReturn('Admin course'); + $course->shouldReceive('isPaid')->andReturn(true); + $course->shouldReceive('isOnline')->andReturn(false); + $course->shouldReceive('getTypeId')->andReturn(5); + $course->shouldReceive('getLevelId')->andReturn(3); + $course->shouldReceive('getTrainingServiceId')->andReturn(200); + $course->shouldReceive('getSchedules')->andReturn([$schedule1]); + $course->shouldReceive('getProjects')->andReturn([$project1]); + $course->shouldReceive('getPrerequisites')->andReturn([$prereq1]); + $course->shouldReceive('getClassName')->andReturn('TrainingCourse'); + + $serializer = new SerializerDecorator(new TrainingCourseSerializer($course, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(100, $values['id']); + $this->assertEquals('OpenStack Administration', $values['name']); + $this->assertEquals('https://example.com/course', $values['link']); + $this->assertEquals('Admin course', $values['description']); + $this->assertTrue($values['is_paid']); + $this->assertFalse($values['is_online']); + $this->assertEquals(5, $values['type_id']); + $this->assertEquals(3, $values['level_id']); + $this->assertEquals(200, $values['training_service_id']); + $this->assertEquals([50], $values['schedules']); + $this->assertEquals([10], $values['projects']); + $this->assertEquals([7], $values['prerequisites']); + } + + public function testTrainingCourseSerializerWithoutRelations() + { + $course = $this->mockBaseEntity(TrainingCourse::class, 100); + $course->shouldReceive('getName')->andReturn('OpenStack Administration'); + $course->shouldReceive('getLink')->andReturn('https://example.com/course'); + $course->shouldReceive('getDescription')->andReturn('Admin course'); + $course->shouldReceive('isPaid')->andReturn(true); + $course->shouldReceive('isOnline')->andReturn(false); + $course->shouldReceive('getTypeId')->andReturn(5); + $course->shouldReceive('getLevelId')->andReturn(3); + $course->shouldReceive('getTrainingServiceId')->andReturn(200); + $course->shouldReceive('getClassName')->andReturn('TrainingCourse'); + + $serializer = new SerializerDecorator(new TrainingCourseSerializer($course, $this->resource_server_context)); + // pass empty relations to skip collection serialization + $values = $serializer->serialize(null, [], []); + + $this->assertIsArray($values); + $this->assertEquals(100, $values['id']); + $this->assertEquals('OpenStack Administration', $values['name']); + $this->assertArrayNotHasKey('schedules', $values); + $this->assertArrayNotHasKey('projects', $values); + $this->assertArrayNotHasKey('prerequisites', $values); + } + + // --- TrainingServiceSerializer --- + + public function testTrainingServiceSerializer() + { + $course1 = $this->mockBaseEntity(TrainingCourse::class, 100); + $course2 = $this->mockBaseEntity(TrainingCourse::class, 101); + + $service = $this->mockBaseEntity(TrainingService::class, 200); + $service->shouldReceive('getClassName')->andReturn('TrainingService'); + $service->shouldReceive('getName')->andReturn('Cloud Training Inc.'); + $service->shouldReceive('getOverview')->andReturn('Training overview'); + $service->shouldReceive('getCall2ActionUrl')->andReturn('https://example.com/action'); + $service->shouldReceive('getSlug')->andReturn('cloud-training-inc'); + $service->shouldReceive('getCompanyId')->andReturn(1); + $service->shouldReceive('getTypeId')->andReturn(2); + $service->shouldReceive('getCourses')->andReturn([$course1, $course2]); + // parent CompanyServiceSerializer relations + $service->shouldReceive('getCaseStudies')->andReturn([]); + $service->shouldReceive('getVideos')->andReturn([]); + $service->shouldReceive('getApprovedReviews')->andReturn([]); + $service->shouldReceive('getResources')->andReturn([]); + + $serializer = new SerializerDecorator(new TrainingServiceSerializer($service, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + $this->assertEquals(200, $values['id']); + $this->assertEquals('Cloud Training Inc.', $values['name']); + $this->assertEquals('Training overview', $values['overview']); + $this->assertEquals('cloud-training-inc', $values['slug']); + $this->assertEquals(1, $values['company_id']); + $this->assertEquals(2, $values['type_id']); + $this->assertIsArray($values['courses']); + $this->assertCount(2, $values['courses']); + $this->assertEquals([100, 101], $values['courses']); + } + + public function testTrainingServiceSerializerInheritsParentRelations() + { + $service = $this->mockBaseEntity(TrainingService::class, 200); + $service->shouldReceive('getClassName')->andReturn('TrainingService'); + $service->shouldReceive('getName')->andReturn('Cloud Training Inc.'); + $service->shouldReceive('getOverview')->andReturn('Training overview'); + $service->shouldReceive('getCall2ActionUrl')->andReturn('https://example.com/action'); + $service->shouldReceive('getSlug')->andReturn('cloud-training-inc'); + $service->shouldReceive('getCompanyId')->andReturn(1); + $service->shouldReceive('getTypeId')->andReturn(2); + $service->shouldReceive('getCourses')->andReturn([]); + $service->shouldReceive('getCaseStudies')->andReturn([]); + $service->shouldReceive('getVideos')->andReturn([]); + $service->shouldReceive('getApprovedReviews')->andReturn([]); + $service->shouldReceive('getResources')->andReturn([]); + + $serializer = new SerializerDecorator(new TrainingServiceSerializer($service, $this->resource_server_context)); + $values = $serializer->serialize(); + + $this->assertIsArray($values); + // Parent CompanyServiceSerializer relations + $this->assertArrayHasKey('case_studies', $values); + $this->assertArrayHasKey('videos', $values); + $this->assertArrayHasKey('reviews', $values); + $this->assertArrayHasKey('resources', $values); + // Own relation + $this->assertArrayHasKey('courses', $values); + } +}