diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php index dadf3a3c90ca0a258ae26ac4e49516e0103fc5d9..406d5d5af7825068bca31f40e9582438e93bd8c8 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php +++ b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php @@ -14,7 +14,12 @@ class BookingsDelete extends JsonApiController public function __invoke(Request $request, Response $response, $args) { - $json = $this->validate($request); + $body = (string) $request->getBody(); + if ($body) { + $json = $this->validate($request); + } else { + $json = []; + } $booking = \ConsultationBooking::find($args['id']); if (!$booking) { diff --git a/tests/jsonapi/ConsultationHelper.php b/tests/jsonapi/ConsultationHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..f7992e74111f22e36c89acf61201bd69394df401 --- /dev/null +++ b/tests/jsonapi/ConsultationHelper.php @@ -0,0 +1,184 @@ +<?php +use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse; +use WoohooLabs\Yang\JsonApi\Schema\Document; +use WoohooLabs\Yang\JsonApi\Schema\Resource\ResourceObject; + +// Required for consultation mailer +require_once 'vendor/flexi/flexi.php'; + +trait ConsultationHelper +{ + /** + * @var \UnitTester + */ + protected $tester; + + protected function _before() + { + \DBManager::getInstance()->setConnection('studip', $this->getModule('\\Helper\\StudipDb')->dbh); + } + + protected static $BLOCK_DATA = [ + 'room' => 'Testraum', + 'calendar_events' => false, + 'show_participants' => false, + 'require_reason' => 'no', + 'confirmation_text' => null, + 'note' => 'Testnotiz für Block', + 'size' => 1, + ]; + + protected static $SLOT_DATA = [ + 'note' => 'Testnotiz für Slot', + ]; + + protected static $BOOKING_DATA = [ + 'reason' => 'Test reason', + ]; + + protected function getUserForCredentials(array $credentials): User + { + return User::find($credentials['id']); + } + + protected function createBlockWithSlotsForRange(Range $range): ConsultationBlock + { + $blocks = ConsultationBlock::generateBlocks( + $range, + strtotime('today 8:00'), + strtotime('today 10:00'), + date('w'), + 1 + ); + $blocks = iterator_to_array($blocks); + + $block = reset($blocks); + $block->setData(self::$BLOCK_DATA); + + $block->createSlots(15); + foreach ($block->slots as $slot) { + $slot->setData(self::$SLOT_DATA['note']); + } + + $block->store(); + + return ConsultationBlock::find($block->id); + } + + protected function getSlotFromBlock(ConsultationBlock $block): ConsultationSlot + { + return $block->slots->first(); + } + + protected function withStudipEnv(array $credentials, callable $fn) + { + // Create global template factory if neccessary + $has_template_factory = isset($GLOBALS['template_factory']); + if (!$has_template_factory) { + $GLOBALS['template_factory'] = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/templates'); + } + + $result = $this->tester->withPHPLib($credentials, $fn); + + if (!$has_template_factory) { + unset($GLOBALS['template_factory']); + } + + return $result; + } + + protected function createBookingForSlot(array $credentials, ConsultationSlot $slot, User $user): ConsultationBooking + { + return $this->withStudipEnv( + $credentials, + function () use ($slot, $user): ConsultationBooking { + $booking = new ConsultationBooking(); + $booking->slot_id = $slot->id; + $booking->user_id = $user->id; + + $booking->setData(self::$BOOKING_DATA); + + $booking->store(); + + return $booking; + } + ); + } + + protected function sendMockRequest(string $route, string $handler, array $credentials, array $variables = [], array $options = []): JsonApiResponse + { + $options = array_merge([ + 'method' => 'GET', + 'considered_successful' => [200], + 'json_body' => null, + ], $options); + + $app = $this->tester->createApp( + $credentials, + strtolower($options['method']), + $route, + $handler + ); + + $evaluated_route = preg_replace_callback( + '/\{(.+?)(:[^}]+)?}/', + function ($match) use ($variables) { + $key = $match[1]; + if (!isset($variables[$key])) { + throw new Exception("No variable '{$key}' defined"); + } + return $variables[$key]; + }, + $route + ); + + $requestBuilder = $this->tester->createRequestBuilder($credentials); + $requestBuilder->setUri($evaluated_route)->setMethod(strtoupper($options['method'])); + + if (isset($options['json_body'])) { + $requestBuilder->setJsonApiBody($options['json_body']); + + } + + /** @var JsonApiResponse $response */ + $response = $this->withStudipEnv($credentials, function () use ($app, $requestBuilder) { + return $this->tester->sendMockRequest($app, $requestBuilder->getRequest()); + }); + + if ($options['considered_successful']) { + $this->assertTrue( + $response->isSuccessful($options['considered_successful']), + 'Actual status code is ' . $response->getStatusCode() + ); + } + + return $response; + } + + protected function getSingleResourceDocument(JsonApiResponse $response): Document + { + $this->assertTrue($response->hasDocument()); + + $document = $response->document(); + $this->assertTrue($document->isSingleResourceDocument()); + + return $document; + } + + protected function getResourceCollectionDocument(JsonApiResponse $response): Document + { + $this->assertTrue($response->hasDocument()); + + $document = $response->document(); + $this->assertTrue($document->isResourceCollectionDocument()); + + return $document; + } + + protected function assertHasRelations(ResourceObject $resource, ...$relations) + { + foreach ($relations as $relation) { + $this->assertTrue($resource->hasRelationship($relation)); + } + } +} diff --git a/tests/jsonapi/ConsultationsBlockShowTest.php b/tests/jsonapi/ConsultationsBlockShowTest.php new file mode 100644 index 0000000000000000000000000000000000000000..62c7ed700d9dcad5752b0228f53630a67ee453f3 --- /dev/null +++ b/tests/jsonapi/ConsultationsBlockShowTest.php @@ -0,0 +1,43 @@ +<?php +use JsonApi\Routes\Consultations\BlockShow; +use JsonApi\Schemas\ConsultationBlock as Schema; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBlockShowTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testFetchBlock(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + + $response = $this->sendMockRequest( + '/consultation-blocks/{id}', + BlockShow::class, + $credentials, + ['id' => $block->id] + ); + $document = $this->getSingleResourceDocument($response); + + $resourceObject = $document->primaryResource(); + $this->assertTrue(is_string($resourceObject->id())); + $this->assertSame($block->id, $resourceObject->id()); + $this->assertSame(Schema::TYPE, $resourceObject->type()); + + $this->assertEquals($block->start, strtotime($resourceObject->attribute('start'))); + $this->assertEquals($block->end, strtotime($resourceObject->attribute('end'))); + + $this->assertSame(self::$BLOCK_DATA['room'], $resourceObject->attribute('room')); + $this->assertSame(self::$BLOCK_DATA['show_participants'], $resourceObject->attribute('show-participants')); + $this->assertSame(self::$BLOCK_DATA['require_reason'], $resourceObject->attribute('require-reason')); + $this->assertSame(self::$BLOCK_DATA['confirmation_text'], $resourceObject->attribute('confirmation-text')); + $this->assertSame(self::$BLOCK_DATA['note'], $resourceObject->attribute('note')); + $this->assertSame(self::$BLOCK_DATA['size'], $resourceObject->attribute('size')); + + $this->assertHasRelations($resourceObject, Schema::REL_RANGE, Schema::REL_SLOTS); + } +} diff --git a/tests/jsonapi/ConsultationsBlocksByRangeIndexTest.php b/tests/jsonapi/ConsultationsBlocksByRangeIndexTest.php new file mode 100644 index 0000000000000000000000000000000000000000..89300b522c75425a12404661115b3df9a3edee1b --- /dev/null +++ b/tests/jsonapi/ConsultationsBlocksByRangeIndexTest.php @@ -0,0 +1,40 @@ +<?php +use JsonApi\Routes\Consultations\BlocksByRangeIndex; + +require_once __DIR__ . '/ConsultationHelper.php'; + +// TODO: Activate consultations on institute for testing +class ConsultationsBlocksByRangeIndexTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public static function rangeProvider(): array + { + return [ + 'Course' => ['course', 'a07535cf2f8a72df33c12ddfa4b53dde'], + 'User' => ['user', '205f3efb7997a0fc9755da2b535038da'], + ]; + } + + /** + * @dataProvider rangeProvider + */ + public function testFetchBlocksByRangeIndex(string $range_type, string $range_id): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = RangeFactory::createRange($range_type, $range_id); + + $this->createBlockWithSlotsForRange($range); + + $response = $this->sendMockRequest( + "/{type:courses|institutes|users}/{id}/consultations", + BlocksByRangeIndex::class, + $credentials, + ['type' => "{$range_type}s", 'id' => $range_id] + ); + $document = $this->getResourceCollectionDocument($response); + + $resources = $document->primaryResources(); + $this->tester->assertCount(1, $resources); + } +} diff --git a/tests/jsonapi/ConsultationsBookingCreateBySlotIndexTest.php b/tests/jsonapi/ConsultationsBookingCreateBySlotIndexTest.php new file mode 100644 index 0000000000000000000000000000000000000000..058e5dd570c3514425bb5692d1a421f43fc4e48b --- /dev/null +++ b/tests/jsonapi/ConsultationsBookingCreateBySlotIndexTest.php @@ -0,0 +1,101 @@ +<?php +use JsonApi\Routes\Consultations\BookingsCreate; +use JsonApi\Schemas\ConsultationBooking as Schema; +use JsonAPi\Schemas\User as UserSchema; +use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBookingCreateBySlotIndexTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testAutorMayCreateBooking(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + [201] + ); + } + + public function testSlotIsOccupied(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + [201] + ); + + $response = $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + null + ); + + $this->assertEquals(409, $response->getStatusCode()); + } + + public function testRootMayNotCreateBooking(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $response = $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForRoot()['id'], + null + ); + + $this->assertEquals(403, $response->getStatusCode()); + } + + private function createBooking(array $credentials, ConsultationSlot $slot, string $user_id, ?array $considered_succssfull): JsonApiResponse + { + return $this->sendMockRequest( + '/consultation-slots/{id}/bookings', + BookingsCreate::class, + $credentials, + ['id' => $slot->id], + [ + 'considered_successful' => $considered_succssfull, + 'method' => 'POST', + 'json_body' => [ + 'data' => [ + 'type' => Schema::TYPE, + 'attributes' => [ + 'reason' => self::$BOOKING_DATA['reason'], + ], + 'relationships' => [ + Schema::REL_USER => [ + 'data' => [ + 'type' => UserSchema::TYPE, + 'id' => $user_id, + ], + ] + ] + ] + ] + ] + ); + } +} diff --git a/tests/jsonapi/ConsultationsBookingCreateTest.php b/tests/jsonapi/ConsultationsBookingCreateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..493bf97efa83f8baf7dd3aa7844f6a3a871f8e81 --- /dev/null +++ b/tests/jsonapi/ConsultationsBookingCreateTest.php @@ -0,0 +1,108 @@ +<?php +use JsonApi\Routes\Consultations\BookingsCreate; +use JsonApi\Schemas\ConsultationBooking as Schema; +use JsonAPi\Schemas\User as UserSchema; +use JsonAPi\Schemas\ConsultationSlot as SlotSchema; +use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBookingCreateTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testAutorMayCreateBooking(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + [201] + ); + } + + public function testSlotIsOccupied(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + [201] + ); + + $response = $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForTestAutor()['id'], + null + ); + + $this->assertEquals(409, $response->getStatusCode()); + } + + public function testRootMayNotCreateBooking(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $response = $this->createBooking( + $credentials, + $slot, + $this->tester->getCredentialsForRoot()['id'], + null + ); + + $this->assertEquals(403, $response->getStatusCode()); + } + + private function createBooking(array $credentials, ConsultationSlot $slot, string $user_id, ?array $considered_succssfull): JsonApiResponse + { + return $this->sendMockRequest( + '/consultation-bookings', + BookingsCreate::class, + $credentials, + [], + [ + 'considered_successful' => $considered_succssfull, + 'method' => 'POST', + 'json_body' => [ + 'data' => [ + 'type' => Schema::TYPE, + 'attributes' => [ + 'reason' => self::$BOOKING_DATA['reason'], + ], + 'relationships' => [ + Schema::REL_SLOT => [ + 'data' => [ + 'type' => SlotSchema::TYPE, + 'id' => $slot->id, + ], + ], + Schema::REL_USER => [ + 'data' => [ + 'type' => UserSchema::TYPE, + 'id' => $user_id, + ], + ], + ] + ] + ] + ] + ); + } +} diff --git a/tests/jsonapi/ConsultationsBookingDeleteTest.php b/tests/jsonapi/ConsultationsBookingDeleteTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2e154debd6a52bc7a873f4d02b7c4cd9f334017f --- /dev/null +++ b/tests/jsonapi/ConsultationsBookingDeleteTest.php @@ -0,0 +1,66 @@ +<?php +use JsonApi\Routes\Consultations\BookingsDelete; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBookingDeleteTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testDeleteBookingWithoutReason(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + $booking = $this->createBookingForSlot( + $credentials, + $slot, + $this->getUserForCredentials($this->tester->getCredentialsForTestAutor()) + ); + + $this->sendMockRequest( + '/consultation-bookings/{id}', + BookingsDelete::class, + $credentials, + ['id' => $booking->id], + [ + 'considered_successful' => [204], + 'method' => 'DELETE', + ] + ); + } + + public function testDeleteBookingWithReason(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + $booking = $this->createBookingForSlot( + $credentials, + $slot, + $this->getUserForCredentials($this->tester->getCredentialsForTestAutor()) + ); + + $this->sendMockRequest( + '/consultation-bookings/{id}', + BookingsDelete::class, + $credentials, + ['id' => $booking->id], + [ + 'considered_successful' => [204], + 'method' => 'DELETE', + 'json_body' => [ + 'data' => [ + 'attributes' => [ + 'reason' => self::$BOOKING_DATA['reason'], + ] + ], + ], + ] + ); + } +} diff --git a/tests/jsonapi/ConsultationsBookingShowTest.php b/tests/jsonapi/ConsultationsBookingShowTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8788b77205e0b5df0d8995766a3528d99fb09574 --- /dev/null +++ b/tests/jsonapi/ConsultationsBookingShowTest.php @@ -0,0 +1,41 @@ +<?php +use JsonApi\Routes\Consultations\BookingsShow; +use JsonApi\Schemas\ConsultationBooking as Schema; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBookingShowTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testFetchBlock(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + $booking = $this->createBookingForSlot( + $credentials, + $slot, + $this->getUserForCredentials($this->tester->getCredentialsForTestAutor()) + ); + + $response = $this->sendMockRequest( + '/consultation-bookings/{id}', + BookingsShow::class, + $credentials, + ['id' => $booking->id] + ); + $document = $this->getSingleResourceDocument($response); + + $resourceObject = $document->primaryResource(); + $this->assertTrue(is_string($resourceObject->id())); + $this->assertSame($booking->id, $resourceObject->id()); + $this->assertSame(Schema::TYPE, $resourceObject->type()); + + $this->assertEquals(self::$BOOKING_DATA['reason'], $resourceObject->attribute('reason')); + + $this->assertHasRelations($resourceObject, Schema::REL_SLOT, Schema::REL_USER); + } +} diff --git a/tests/jsonapi/ConsultationsBookingsBySlotIndexTest.php b/tests/jsonapi/ConsultationsBookingsBySlotIndexTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e927aad4f546713d328a35fdd9f001283b9774f5 --- /dev/null +++ b/tests/jsonapi/ConsultationsBookingsBySlotIndexTest.php @@ -0,0 +1,34 @@ +<?php +use JsonApi\Routes\Consultations\BookingsBySlotIndex; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsBookingsBySlotIndexTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testFetchSlots(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = $this->getUserForCredentials($credentials); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + $this->createBookingForSlot( + $credentials, + $slot, + $this->getUserForCredentials($this->tester->getCredentialsForTestAutor()) + ); + + $response = $this->sendMockRequest( + '/consultation-slots/{id}/bookings', + BookingsBySlotIndex::class, + $credentials, + ['id' => $slot->id] + ); + $document = $this->getResourceCollectionDocument($response); + + $resources = $document->primaryResources(); + $this->tester->assertCount(1, $resources); + } +} diff --git a/tests/jsonapi/ConsultationsSlotShowTest.php b/tests/jsonapi/ConsultationsSlotShowTest.php new file mode 100644 index 0000000000000000000000000000000000000000..be8f5b7e4273f5a839d9cc0bd038c0686d56ca0b --- /dev/null +++ b/tests/jsonapi/ConsultationsSlotShowTest.php @@ -0,0 +1,45 @@ +<?php +use JsonApi\Routes\Consultations\SlotShow; +use JsonApi\Schemas\ConsultationSlot as Schema; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsSlotShowTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testFetchBlock(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + $slot = $this->getSlotFromBlock($block); + + $response = $this->sendMockRequest( + '/consultation-slots/{id}', + SlotShow::class, + $credentials, + ['id' => $slot->id] + ); + $document = $this->getSingleResourceDocument($response); + + $resourceObject = $document->primaryResource(); + $this->assertTrue(is_string($resourceObject->id())); + $this->assertSame($slot->id, $resourceObject->id()); + $this->assertSame(Schema::TYPE, $resourceObject->type()); + + $this->assertEquals($slot->start_time, strtotime($resourceObject->attribute('start_time'))); + $this->assertEquals($slot->end_time, strtotime($resourceObject->attribute('end_time'))); + + $this->assertHasRelations($resourceObject, Schema::REL_BLOCK, Schema::REL_BOOKINGS); + +// +// $this->assertSame(self::$BLOCK_DATA['room'], $resourceObject->attribute('room')); +// $this->assertSame(self::$BLOCK_DATA['show_participants'], $resourceObject->attribute('show-participants')); +// $this->assertSame(self::$BLOCK_DATA['require_reason'], $resourceObject->attribute('require-reason')); +// $this->assertSame(self::$BLOCK_DATA['confirmation_text'], $resourceObject->attribute('confirmation-text')); +// $this->assertSame(self::$BLOCK_DATA['note'], $resourceObject->attribute('note')); +// $this->assertSame(self::$BLOCK_DATA['size'], $resourceObject->attribute('size')); + } +} diff --git a/tests/jsonapi/ConsultationsSlotsByBlockIndexTest.php b/tests/jsonapi/ConsultationsSlotsByBlockIndexTest.php new file mode 100644 index 0000000000000000000000000000000000000000..70bb6d9a6dd5733c70eb730f6624b467436cdbf8 --- /dev/null +++ b/tests/jsonapi/ConsultationsSlotsByBlockIndexTest.php @@ -0,0 +1,28 @@ +<?php +use JsonApi\Routes\Consultations\SlotsByBlockIndex; + +require_once __DIR__ . '/ConsultationHelper.php'; + +class ConsultationsSlotsByBlockIndexTest extends Codeception\Test\Unit +{ + use ConsultationHelper; + + public function testFetchSlots(): void + { + $credentials = $this->tester->getCredentialsForTestDozent(); + $range = User::find($credentials['id']); + + $block = $this->createBlockWithSlotsForRange($range); + + $response = $this->sendMockRequest( + '/consultation-blocks/{id}/slots', + SlotsByBlockIndex::class, + $credentials, + ['id' => $block->id] + ); + $document = $this->getResourceCollectionDocument($response); + + $resources = $document->primaryResources(); + $this->tester->assertCount(8, $resources); + } +}