diff --git a/codeception.yml b/codeception.yml index b709ae286..734072c03 100644 --- a/codeception.yml +++ b/codeception.yml @@ -8,8 +8,9 @@ suites: - REST: url: '%STUDIP_REST_URL%' depends: PhpBrowser + - Asserts - \Helper\Api: - opencast_rest_url: '%OPENCAST_REST_URL%' + opencast_url: '%OPENCAST_URL%' api_token: '%API_TOKEN%' opencast_admin_user: '%OPENCAST_ADMIN_NAME%' opencast_admin_password: '%OPENCAST_ADMIN_PASSWORD%' diff --git a/composer.json b/composer.json index 8d40b202d..57abf2d2d 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "codeception/module-phpbrowser": "^1.0.0", "codeception/module-asserts": "^1.0.0", "codeception/module-rest": "^1.0.0", - "vlucas/phpdotenv": "^5.6" + "vlucas/phpdotenv": "^5.6", + "guzzlehttp/guzzle": ">=7.5.1" } } diff --git a/tests/.env b/tests/.env index 16d49e760..7c63c1fe8 100644 --- a/tests/.env +++ b/tests/.env @@ -1,5 +1,5 @@ STUDIP_REST_URL=http://localhost/plugins.php/opencastv3/api -OPENCAST_REST_URL=http://127.0.0.1:8081/api +OPENCAST_URL=http://127.0.0.1:8081 # Opencast server ID CONFIG_ID=1 diff --git a/tests/AclCest.php b/tests/AclCest.php deleted file mode 100644 index faa0ace3a..000000000 --- a/tests/AclCest.php +++ /dev/null @@ -1,159 +0,0 @@ -getConfig(); - - $this->opencast_rest_url = $config['opencast_rest_url']; - $this->config_id = $config['config_id']; - $this->api_token = $config['api_token']; - $this->opencast_admin_user = $config['opencast_admin_user']; - $this->opencast_admin_password = $config['opencast_admin_password']; - $this->dozent_name = $config['dozent_name']; - $this->course_student = $config['course_student']; - $this->course_id = $config['course_id']; - - $I->amHttpAuthenticated($config['dozent_name'], $config['dozent_password']); - } - - // tests - public function testPlaylistAcl(ApiTester $I) - { - $playlist = [ - 'title' => 'Meine Videos' , - 'description' => 'Videoliste', - 'visibility' => 'internal', - 'config_id' => $this->config_id, - ]; - - $response = $I->sendPostAsJson('/playlists', $playlist); - - $I->seeResponseCodeIs(201); - $I->seeResponseIsJson(); - - $I->seeResponseContainsJson($playlist); - $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); - - list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); - - // Check if user has correct playlist role - $response = $I->sendGetAsJson('/opencast/user/' . $this->dozent_name, ['token' => $this->api_token]); - - $I->seeResponseCodeIs(200); - $I->seeResponseIsJson(); - - $I->seeResponseContainsJson([ - 'username' => $this->dozent_name, - 'roles' => [ - 'PLAYLIST_' . $service_playlist_id . '_write', - ] - ]); - - // Check ACLs in Opencast - - // Login as opencast admin - $I->amHttpAuthenticated($this->opencast_admin_user, $this->opencast_admin_password); - - $response = $I->sendGetAsJson($this->opencast_rest_url . '/playlists/' . $service_playlist_id); - $I->seeResponseContainsJson(['accessControlEntries' => [ - ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_read', 'action' => 'read'], - ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_write', 'action' => 'read'], - ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_write', 'action' => 'write'], - ]]); - } - - public function testCoursePlaylistAcl(ApiTester $I) - { - // Create a playlist - $playlist = [ - 'title' => 'Meine Videos' , - 'description' => 'Videoliste', - 'visibility' => 'internal', - 'config_id' => $this->config_id, - ]; - - $response = $I->sendPostAsJson('/playlists', $playlist); - $I->seeResponseCodeIs(201); - $I->seeResponseIsJson(); - - $I->seeResponseContainsJson($playlist); - $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); - - list($token) = $I->grabDataFromResponseByJsonPath('$.token'); - list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); - - // Add playlist to course - $response = $I->sendPost('/courses/' . $this->course_id . '/playlist/' . $token); - $I->seeResponseCodeIs(204); - - // Check if student of course has read access only - $response = $I->sendGetAsJson('/opencast/user/' . $this->course_student, ['token' => $this->api_token]); - - $I->seeResponseCodeIs(200); - $I->seeResponseIsJson(); - - $I->seeResponseContainsJson([ - 'username' => $this->course_student, - 'roles' => [ - 'PLAYLIST_' . $service_playlist_id . '_read', - ] - ]); - $I->dontSeeResponseContainsJson(['roles' => [ - 'PLAYLIST_' . $service_playlist_id . '_write', - ]]); - } - - public function testRemoveCoursePlaylistAcl(ApiTester $I) - { - // Create a playlist - $playlist = [ - 'title' => 'Meine Videos' , - 'description' => 'Videoliste', - 'visibility' => 'internal', - 'config_id' => $this->config_id, - ]; - - $response = $I->sendPostAsJson('/playlists', $playlist); - $I->seeResponseCodeIs(201); - $I->seeResponseIsJson(); - - $I->seeResponseContainsJson($playlist); - $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); - - list($token) = $I->grabDataFromResponseByJsonPath('$.token'); - list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); - - // Add playlist to course - $response = $I->sendPost('/courses/' . $this->course_id . '/playlist/' . $token); - $I->seeResponseCodeIs(204); - - // Remove playlist from course - $response = $I->sendDelete('/courses/' . $this->course_id . '/playlist/' . $token); - $I->seeResponseCodeIs(204); - - // Check if student of course has no access - $response = $I->sendGetAsJson('/opencast/user/' . $this->course_student, ['token' => $this->api_token]); - - $I->seeResponseCodeIs(200); - $I->seeResponseIsJson(); - - $I->dontseeResponseContainsJson([ - 'username' => $this->course_student, - 'roles' => [ - 'PLAYLIST_' . $service_playlist_id . '_read', - 'PLAYLIST_' . $service_playlist_id . '_write', - ] - ]); - } -} diff --git a/tests/CoursesCest.php b/tests/CoursesCest.php index 2e257081e..10094778e 100644 --- a/tests/CoursesCest.php +++ b/tests/CoursesCest.php @@ -3,6 +3,8 @@ class CoursesCest { private $config_id; + private $api_token; + private $course_student; private $course_id; public function _before(ApiTester $I) @@ -10,6 +12,8 @@ public function _before(ApiTester $I) $config = $I->getConfig(); $this->config_id = $config['config_id']; + $this->api_token = $config['api_token']; + $this->course_student = $config['course_student']; $this->course_id = $config['course_id']; $I->amHttpAuthenticated($config['dozent_name'], $config['dozent_password']); @@ -68,10 +72,27 @@ public function testAddPlaylist(ApiTester $I) $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); list($token) = $I->grabDataFromResponseByJsonPath('$.token'); + list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); // Add playlist to course $response = $I->sendPost('/courses/' . $this->course_id . '/playlist/' . $token); $I->seeResponseCodeIs(204); + + // Check if student of course has read access only + $response = $I->sendGetAsJson('/opencast/user/' . $this->course_student, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson([ + 'username' => $this->course_student, + 'roles' => [ + 'PLAYLIST_' . $service_playlist_id . '_read', + ] + ]); + $I->dontSeeResponseContainsJson(['roles' => [ + 'PLAYLIST_' . $service_playlist_id . '_write', + ]]); } public function testRemovePlaylist(ApiTester $I) @@ -92,6 +113,7 @@ public function testRemovePlaylist(ApiTester $I) $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); list($token) = $I->grabDataFromResponseByJsonPath('$.token'); + list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); // Add playlist to course $response = $I->sendPost('/courses/' . $this->course_id . '/playlist/' . $token); @@ -100,6 +122,20 @@ public function testRemovePlaylist(ApiTester $I) // Remove playlist from course $response = $I->sendDelete('/courses/' . $this->course_id . '/playlist/' . $token); $I->seeResponseCodeIs(204); + + // Check if student of course has no access + $response = $I->sendGetAsJson('/opencast/user/' . $this->course_student, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $roles = ['PLAYLIST_' . $service_playlist_id . '_read', 'PLAYLIST_' . $service_playlist_id . '_write']; + foreach ($roles as $role) { + $I->dontseeResponseContainsJson([ + 'username' => $this->course_student, + 'roles' => [$role] + ]); + } } } diff --git a/tests/PlaylistsCest.php b/tests/PlaylistsCest.php index 9037e5edf..1d6cfba9c 100644 --- a/tests/PlaylistsCest.php +++ b/tests/PlaylistsCest.php @@ -2,8 +2,13 @@ class PlaylistsCest { + private $opencast_url; private $config_id; + private $api_token; + private $opencast_admin_user; + private $opencast_admin_password; + private $dozent_name; private $author_name; private $author_password; @@ -11,7 +16,12 @@ public function _before(ApiTester $I) { $config = $I->getConfig(); + $this->opencast_url = $config['opencast_url']; $this->config_id = $config['config_id']; + $this->api_token = $config['api_token']; + $this->opencast_admin_user = $config['opencast_admin_user']; + $this->opencast_admin_password = $config['opencast_admin_password']; + $this->dozent_name = $config['dozent_name']; $this->author_name = $config['author_name']; $this->author_password = $config['author_password']; @@ -34,6 +44,33 @@ public function testCreatePlaylist(ApiTester $I) $I->seeResponseContainsJson($playlist); $I->seeResponseContainsJson(['users' => [['perm' => 'owner']]]); + + list($service_playlist_id) = $I->grabDataFromResponseByJsonPath('$.service_playlist_id'); + + // Check if user has correct playlist role + $response = $I->sendGetAsJson('/opencast/user/' . $this->dozent_name, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson([ + 'username' => $this->dozent_name, + 'roles' => [ + 'PLAYLIST_' . $service_playlist_id . '_write', + ] + ]); + + // Check ACLs in Opencast + + // Login as opencast admin + $I->amHttpAuthenticated($this->opencast_admin_user, $this->opencast_admin_password); + + $response = $I->sendGetAsJson($this->opencast_url . '/api/playlists/' . $service_playlist_id); + $I->seeResponseContainsJson(['accessControlEntries' => [ + ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_read', 'action' => 'read'], + ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_write', 'action' => 'read'], + ['allow' => true, 'role' => 'PLAYLIST_' . $service_playlist_id . '_write', 'action' => 'write'], + ]]); } public function testDeletePlaylist(ApiTester $I) diff --git a/tests/VideoCest.php b/tests/VideoCest.php new file mode 100644 index 000000000..16714d31f --- /dev/null +++ b/tests/VideoCest.php @@ -0,0 +1,242 @@ + 'presenter/source', + 'title' => 'Test with Audio', + 'creator' => 'Test Dozent', + 'identifier' => 'ID-test', + 'config_id' => null, + 'token' => null, + ]; + private $playlist_token; + + const CRONJOB_DISCOVER = 'Opencast: Katalogisiert neue Videos aus Opencast.'; + const CRONJOB_QUEUE = 'Opencast: Arbeitet vorgemerkte Aufgaben ab, wie Aktualisierung der Metadaten, ACLs (Sichtbarkeit), etc.'; + + public function _before(ApiTester $I) + { + $config = $I->getConfig(); + + $this->opencast_url = $config['opencast_url']; + $this->config_id = $config['config_id']; + $this->api_token = $config['api_token']; + $this->opencast_admin_user = $config['opencast_admin_user']; + $this->opencast_admin_password = $config['opencast_admin_password']; + $this->dozent_name = $config['dozent_name']; + $this->course_student = $config['course_student']; + $this->course_id = $config['course_id']; + + $this->video['config_id'] = $this->config_id; + + $I->amHttpAuthenticated($config['dozent_name'], $config['dozent_password']); + } + + // tests + public function testIngestVideo(ApiTester $I) + { + // Ingest video to opencast + $client = new Client(); + $response = $client->request('POST', $this->opencast_url . '/ingest/addMediaPackage/fast', [ + 'auth' => [$this->opencast_admin_user, $this->opencast_admin_password], + 'multipart' => [ + ['name' => 'flavor', 'contents' => $this->video['flavor']], + ['name' => 'title', 'contents' => $this->video['title']], + ['name' => 'creator', 'contents' => $this->video['creator']], + ['name' => 'identifier', 'contents' => $this->video['identifier']], + ['name' => 'BODY', 'contents' => fopen(codecept_data_dir('test-with-audio.mp4'), 'r')], + ] + ]); + + $I->assertIsScalar($response->getStatusCode(), 200, 'Video is ingested'); + + // Add video to studip, fails if video is already added + $response = $I->sendPostAsJson('/videos/' . $this->video['identifier'], [ + 'event' => $this->video, + ]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson(['event' => [ + 'title' => $this->video['title'], + 'episode' => $this->video['identifier'], + 'config_id' => $this->video['config_id'], + ]]); + $this->video['token'] = $I->grabDataFromResponseByJsonPath('$.event.token')[0]; + + $I->seeVideoIsProcessed($this->video['identifier']); + + // Start cronjobs + $success = $I->runCronjob(self::CRONJOB_DISCOVER); + $I->assertTrue($success, 'Cronjob run successful'); + + $success = $I->runCronjob(self::CRONJOB_QUEUE); + $I->assertTrue($success, 'Cronjob run successful'); + + $I->seeVideoIsProcessed($this->video['identifier']); + } + + /** + * @depends testIngestVideo + */ + public function testVideoAcl(ApiTester $I) + { + // Check if user has correct role + $response = $I->sendGetAsJson('/opencast/user/' . $this->dozent_name, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson([ + 'username' => $this->dozent_name, + 'roles' => [ + $this->video['identifier'] . '_write', + ] + ]); + + // Check ACLs in Opencast + + // Login as opencast admin + $I->amHttpAuthenticated($this->opencast_admin_user, $this->opencast_admin_password); + + $response = $I->sendGetAsJson($this->opencast_url . '/api/events/' . $this->video['identifier'] . '/acl'); + $I->seeResponseContainsJson([ + ['allow' => true, 'role' => $this->video['identifier'] . '_read', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'write'], + ]); + } + + /** + * @depends testIngestVideo + */ + public function testCourseVideoAcl(ApiTester $I) + { + // Add video to course + + // Create a playlist + $playlist = [ + 'title' => 'Meine Videos' , + 'description' => 'Videoliste', + 'visibility' => 'internal', + 'config_id' => $this->config_id, + ]; + + $response = $I->sendPostAsJson('/playlists', $playlist); + $I->seeResponseCodeIs(201); + $I->seeResponseIsJson(); + + list($this->playlist_token) = $I->grabDataFromResponseByJsonPath('$.token'); + + // Add playlist to course + $response = $I->sendPost('/courses/' . $this->course_id . '/playlist/' . $this->playlist_token); + $I->seeResponseCodeIs(204); + + // Add video to course playlist + $I->sendPut('/playlists/' . $this->playlist_token . '/videos', [ + 'videos' => [$this->video['token']], + 'course_id' => $this->course_id, + ]); + $I->seeResponseCodeIs(204); + + + // Check if lecturer of course has instructor role + $response = $I->sendGetAsJson('/opencast/user/' . $this->dozent_name, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson([ + 'username' => $this->dozent_name, + 'roles' => [ + $this->course_id . '_Instructor', + ] + ]); + + $I->dontSeeResponseContainsJson(['roles' => [ + $this->course_id . '_Learner', + ]]); + + // Check if student of course has learner role only + $response = $I->sendGetAsJson('/opencast/user/' . $this->course_student, ['token' => $this->api_token]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + + $I->seeResponseContainsJson([ + 'username' => $this->course_student, + 'roles' => [ + $this->course_id . '_Learner', + ] + ]); + + $I->dontSeeResponseContainsJson(['roles' => [ + $this->course_id . '_Instructor', + ]]); + + + // Check ACLs in Opencast + + // Ensure video is processed and acls are set + $I->seeVideoIsProcessed($this->video['identifier']); + + // Login as opencast admin + $I->amHttpAuthenticated($this->opencast_admin_user, $this->opencast_admin_password); + + $response = $I->sendGetAsJson($this->opencast_url . '/api/events/' . $this->video['identifier'] . '/acl'); + $I->seeResponseContainsJson([ + ['allow' => true, 'role' => $this->video['identifier'] . '_read', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'write'], + ['allow' => true, 'role' => $this->course_id . '_Learner', 'action' => 'read'], + ['allow' => true, 'role' => $this->course_id . '_Instructor', 'action' => 'read'], + ['allow' => true, 'role' => $this->course_id . '_Instructor', 'action' => 'write'], + ]); + } + + /** + * @depends testCourseVideoAcl + */ + public function testRemoveCourseVideoAcl(ApiTester $I) + { + // Remove video from course playlist + $I->sendPatch('/playlists/' . $this->playlist_token . '/videos', json_encode([ + 'videos' => [$this->video['token']], + 'course_id' => $this->course_id, + ])); + $I->seeResponseCodeIs(204); + + + // Check ACLs in Opencast + + // Ensure video is processed and acls are set + $I->seeVideoIsProcessed($this->video['identifier']); + + // Login as opencast admin + $I->amHttpAuthenticated($this->opencast_admin_user, $this->opencast_admin_password); + + $response = $I->sendGetAsJson($this->opencast_url . '/api/events/' . $this->video['identifier'] . '/acl'); + $I->seeResponseContainsJson([ + ['allow' => true, 'role' => $this->video['identifier'] . '_read', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'read'], + ['allow' => true, 'role' => $this->video['identifier'] . '_write', 'action' => 'write'], + ]); + + $I->dontSeeResponseContainsJson(['allow' => true, 'role' => $this->course_id . '_Learner', 'action' => 'read']); + $I->dontSeeResponseContainsJson(['allow' => true, 'role' => $this->course_id . '_Instructor', 'action' => 'read']); + $I->dontSeeResponseContainsJson(['allow' => true, 'role' => $this->course_id . '_Instructor', 'action' => 'write']); + } +} diff --git a/tests/_data/test-with-audio.mp4 b/tests/_data/test-with-audio.mp4 new file mode 100644 index 000000000..92d09ceee Binary files /dev/null and b/tests/_data/test-with-audio.mp4 differ diff --git a/tests/_support/Helper/Api.php b/tests/_support/Helper/Api.php index f38fa1daa..3515518e3 100644 --- a/tests/_support/Helper/Api.php +++ b/tests/_support/Helper/Api.php @@ -4,10 +4,12 @@ // here you can define custom actions // all public methods declared in helper class will be available in $I +use GuzzleHttp\Client; + class Api extends \Codeception\Module { protected $requiredFields = [ - 'opencast_rest_url', + 'opencast_url', 'config_id', 'api_token', 'opencast_admin_user', @@ -20,7 +22,55 @@ class Api extends \Codeception\Module 'course_id', ]; + const STUDIP_DIR = __DIR__ . '/../../../../../../../'; + public function getConfig(): array { return $this->config; } + + /** + * Wait until video is successfully processed + * + * @return bool true, if video processing is finished + */ + public function seeVideoIsProcessed($id): bool { + $client = new Client(); + $tries = 0; + + while (true) { + $this->assertLessThan(30, $tries, 'Number of attempts under limit'); + + $response = $client->request('GET', $this->config['opencast_url'] . '/api/events/' . $id, [ + 'auth' => [$this->config['opencast_admin_user'], $this->config['opencast_admin_password']], + ]); + + $this->assertEquals(200, $response->getStatusCode(), 'Successfully fetched video processing state'); + + $event = json_decode($response->getBody(), true); + if ($event['processing_state'] == 'SUCCEEDED') { + return true; + } + + $tries++; + sleep(5); + } + } + + /** + * Run studip cronjob + * + * @param string $cronjob cronjob description + * @return bool success? + */ + public function runCronjob(string $cronjob): bool + { + $studip_cli = self::STUDIP_DIR . "cli/studip"; + exec( + "php $studip_cli cronjobs:execute $(php $studip_cli cronjobs:list | grep '$cronjob' | awk '{print $1}')", + $output, + $result_code + ); + + return $result_code === 0; + } }