From bb62df65ac6aa71a757b58a01f9cb95a859a38f9 Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Fri, 17 Jun 2022 07:39:22 +0000
Subject: [PATCH] implement tests for consultation jsonapi routes

Closes #1174

Merge request studip/studip!696
---
 .../Routes/Consultations/BookingsDelete.php   |   7 +-
 tests/jsonapi/ConsultationHelper.php          | 184 ++++++++++++++++++
 tests/jsonapi/ConsultationsBlockShowTest.php  |  43 ++++
 .../ConsultationsBlocksByRangeIndexTest.php   |  40 ++++
 ...sultationsBookingCreateBySlotIndexTest.php | 101 ++++++++++
 .../ConsultationsBookingCreateTest.php        | 108 ++++++++++
 .../ConsultationsBookingDeleteTest.php        |  66 +++++++
 .../jsonapi/ConsultationsBookingShowTest.php  |  41 ++++
 .../ConsultationsBookingsBySlotIndexTest.php  |  34 ++++
 tests/jsonapi/ConsultationsSlotShowTest.php   |  45 +++++
 .../ConsultationsSlotsByBlockIndexTest.php    |  28 +++
 11 files changed, 696 insertions(+), 1 deletion(-)
 create mode 100644 tests/jsonapi/ConsultationHelper.php
 create mode 100644 tests/jsonapi/ConsultationsBlockShowTest.php
 create mode 100644 tests/jsonapi/ConsultationsBlocksByRangeIndexTest.php
 create mode 100644 tests/jsonapi/ConsultationsBookingCreateBySlotIndexTest.php
 create mode 100644 tests/jsonapi/ConsultationsBookingCreateTest.php
 create mode 100644 tests/jsonapi/ConsultationsBookingDeleteTest.php
 create mode 100644 tests/jsonapi/ConsultationsBookingShowTest.php
 create mode 100644 tests/jsonapi/ConsultationsBookingsBySlotIndexTest.php
 create mode 100644 tests/jsonapi/ConsultationsSlotShowTest.php
 create mode 100644 tests/jsonapi/ConsultationsSlotsByBlockIndexTest.php

diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php
index dadf3a3c90c..406d5d5af78 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 00000000000..f7992e74111
--- /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 00000000000..62c7ed700d9
--- /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 00000000000..89300b522c7
--- /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 00000000000..058e5dd570c
--- /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 00000000000..493bf97efa8
--- /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 00000000000..2e154debd6a
--- /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 00000000000..8788b77205e
--- /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 00000000000..e927aad4f54
--- /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 00000000000..be8f5b7e427
--- /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 00000000000..70bb6d9a6dd
--- /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);
+    }
+}
-- 
GitLab