When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
diff --git a/resources/js/Pages/Profile/Show.vue b/resources/js/Pages/Profile/Show.vue
index be87898..0fc30fb 100644
--- a/resources/js/Pages/Profile/Show.vue
+++ b/resources/js/Pages/Profile/Show.vue
@@ -16,7 +16,7 @@ defineProps({
-
+
Profile
diff --git a/resources/js/Pages/Videos/Create.vue b/resources/js/Pages/Videos/Create.vue
new file mode 100644
index 0000000..cb8d391
--- /dev/null
+++ b/resources/js/Pages/Videos/Create.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Videos/Edit.vue b/resources/js/Pages/Videos/Edit.vue
new file mode 100644
index 0000000..113fe85
--- /dev/null
+++ b/resources/js/Pages/Videos/Edit.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+ Edit Video
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Videos/Index.vue b/resources/js/Pages/Videos/Index.vue
new file mode 100644
index 0000000..6607e06
--- /dev/null
+++ b/resources/js/Pages/Videos/Index.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ My Videos
+
+
+
+
+ Create Video
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Videos/Show.vue b/resources/js/Pages/Videos/Show.vue
new file mode 100644
index 0000000..bee0181
--- /dev/null
+++ b/resources/js/Pages/Videos/Show.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
{{ page.props.video.title }}
+
+
+
+ {{ page.props.video.user.name }}
+
+
+
+
+
{{ createdAt }}
+
+
+ {{ page.props.video.width }} x {{ page.props.video.height }}
+ {{ size }}
+ {{ time }}
+
+
+
{{ page.props.video.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Welcome.vue b/resources/js/Pages/Welcome.vue
deleted file mode 100644
index 55e8b8d..0000000
--- a/resources/js/Pages/Welcome.vue
+++ /dev/null
@@ -1,176 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Documentation
-
-
- Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
-
-
-
-
-
-
-
-
-
-
-
-
-
Laracasts
-
-
- Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
-
-
-
-
-
-
-
-
-
-
-
Laravel News
-
-
- Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
-
-
-
-
-
-
-
-
-
-
-
Vibrant Ecosystem
-
-
- Laravel's robust library of first-party tools and libraries, such as Forge, Vapor, Nova, and Envoyer help you take your projects to the next level. Pair them with powerful open source libraries like Cashier, Dusk, Echo, Horizon, Sanctum, Telescope, and more.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js
index 5f1390b..deb2e10 100644
--- a/resources/js/bootstrap.js
+++ b/resources/js/bootstrap.js
@@ -2,3 +2,11 @@ import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+
+/**
+ * Echo exposes an expressive API for subscribing to channels and listening
+ * for events that are broadcast by Laravel. Echo and event broadcasting
+ * allow your team to quickly build robust real-time web applications.
+ */
+
+import './echo';
diff --git a/resources/js/echo.js b/resources/js/echo.js
new file mode 100644
index 0000000..e533e92
--- /dev/null
+++ b/resources/js/echo.js
@@ -0,0 +1,11 @@
+import Echo from 'laravel-echo';
+
+import Pusher from 'pusher-js';
+window.Pusher = Pusher;
+
+window.Echo = new Echo({
+ broadcaster: 'pusher',
+ key: import.meta.env.VITE_PUSHER_APP_KEY,
+ cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
+ encrypted: true
+});
diff --git a/resources/js/utils/toHumanFormats.js b/resources/js/utils/toHumanFormats.js
new file mode 100644
index 0000000..476ecea
--- /dev/null
+++ b/resources/js/utils/toHumanFormats.js
@@ -0,0 +1,29 @@
+export function sizeToHuman (bytes) {
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ if (bytes === 0) return '0 Byte';
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
+ if (i === 0) return `${bytes} ${sizes[i]}`;
+ return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`;
+};
+
+export function timeToHuman (seconds) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const remainingSeconds = seconds % 60;
+
+ let final = '';
+
+ if (hours > 0) {
+ final += `${hours}h `;
+ }
+
+ if (minutes > 0) {
+ final += `${minutes}m `;
+ }
+
+ if (remainingSeconds > 0) {
+ final += `${remainingSeconds}s`;
+ }
+
+ return final;
+};
diff --git a/resources/tests/test_video.mp4 b/resources/tests/test_video.mp4
new file mode 100644
index 0000000..a2fff41
Binary files /dev/null and b/resources/tests/test_video.mp4 differ
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index 334627a..284f7f7 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -8,6 +8,7 @@
+
diff --git a/routes/channels.php b/routes/channels.php
new file mode 100644
index 0000000..df2ad28
--- /dev/null
+++ b/routes/channels.php
@@ -0,0 +1,7 @@
+id === (int) $id;
+});
diff --git a/routes/web.php b/routes/web.php
index 5bfb842..d02acfa 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,24 +1,27 @@
Route::has('login'),
- 'canRegister' => Route::has('register'),
- 'laravelVersion' => Application::VERSION,
- 'phpVersion' => PHP_VERSION,
- ]);
-});
+Route::get('/', [HomeController::class, 'index'])->name('home');
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
- Route::get('/dashboard', function () {
- return Inertia::render('Dashboard');
- })->name('dashboard');
+ Route::redirect('/dashboard', '/videos')->name('dashboard');
+
+ Route::resource('videos', VideoController::class);
+
+ Route::group(['prefix' => 'videos/{video}', 'as' => 'videos.'], function () {
+ Route::resource('comments', CommentController::class)->only(['store', 'destroy'])
+ ->middleware('can:create,App\Models\Comment,video');
+ });
+
+ Route::resource('tags', TagController::class);
});
diff --git a/tailwind.config.js b/tailwind.config.js
index e7fb848..e6b10ed 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -17,6 +17,13 @@ export default {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
+ keyframes: {
+ shimmer: {
+ "100%": {
+ transform: "translateX(100%)",
+ },
+ },
+ }
},
},
diff --git a/tests/Feature/Http/Controllers/CommentControllerTest.php b/tests/Feature/Http/Controllers/CommentControllerTest.php
new file mode 100644
index 0000000..3c312ee
--- /dev/null
+++ b/tests/Feature/Http/Controllers/CommentControllerTest.php
@@ -0,0 +1,121 @@
+user = User::factory()->create();
+ $this->video = Video::factory()->create([
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+ });
+
+ it('should create a comment for a video', function () {
+ $comment = [
+ 'content' => 'This is a comment',
+ ];
+
+ $response = $this->actingAs($this->user)
+ ->post(route('videos.comments.store', $this->video->id), $comment);
+
+ $response->assertStatus(302);
+
+ $this->assertDatabaseHas('comments', [
+ 'user_id' => $this->user->id,
+ 'video_id' => $this->video->id,
+ 'content' => $comment['content'],
+ ]);
+ });
+
+ it('should not create a comment for a video if the content is missing', function () {
+ $comment = [
+ 'content' => '',
+ ];
+
+ $response = $this->actingAs($this->user)
+ ->post(route('videos.comments.store', $this->video->id), $comment);
+
+ $response->assertSessionHasErrors('content');
+ });
+
+ it('should not create a comment for a video if the user is not authenticated', function () {
+ $comment = [
+ 'content' => 'This is a comment',
+ ];
+
+ $response = $this->post(route('videos.comments.store', $this->video->id), $comment);
+
+ $response->assertRedirect(route('login'));
+ });
+
+ it('should not create a comment for a video if the video does not exist', function () {
+ $comment = [
+ 'content' => 'This is a comment',
+ ];
+
+ $this->video->delete();
+
+ $response = $this->actingAs($this->user)
+ ->post(route('videos.comments.store', 1), $comment);
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('CommentController destroy', function () {
+ it('should delete a comment', function () {
+ $user = User::factory()->create();
+ $video = Video::factory()->create([
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $comment = Comment::factory()->create([
+ 'user_id' => $user->id,
+ 'video_id' => $video->id,
+ ]);
+
+ $route = route('videos.comments.destroy', [$video->id, $comment->id]);
+
+ $response = $this->actingAs($user)
+ ->delete($route);
+
+ $response->assertStatus(302);
+
+ $this->assertDatabaseMissing('comments', [
+ 'id' => $comment->id,
+ ]);
+ });
+
+ it('should not delete a comment if the user is not authenticated', function () {
+ $comment = Comment::factory()->create();
+
+ $response = $this->delete(route('videos.comments.destroy', [$comment->video_id, $comment->id]));
+
+ $response->assertRedirect(route('login'));
+ });
+
+ it('should not delete a comment if the user is not the owner', function () {
+ $user = User::factory()->create();
+ $video = Video::factory()->create();
+ $comment = Comment::factory()->create([
+ 'video_id' => $video->id,
+ ]);
+
+ $response = $this->actingAs($user)
+ ->delete(route('videos.comments.destroy', [$comment->video_id, $comment->id]));
+
+ $response->assertStatus(403);
+ });
+
+ it('should not delete a comment if the comment does not exist', function () {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)
+ ->delete(route('videos.comments.destroy', [1, 1]));
+
+ $response->assertStatus(404);
+ });
+});
diff --git a/tests/Feature/Http/Controllers/HomeControllerTest.php b/tests/Feature/Http/Controllers/HomeControllerTest.php
new file mode 100644
index 0000000..722224f
--- /dev/null
+++ b/tests/Feature/Http/Controllers/HomeControllerTest.php
@@ -0,0 +1,359 @@
+get(route('home'));
+
+ $response->assertInertia(function (AssertableInertia $page) {
+ $page->component('Home');
+ $page->has('canLogin');
+ $page->has('canRegister');
+ });
+});
+
+it('renders with videos correctly', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory()->count(3)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $response = $this
+ ->get(route('home'));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 3, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('user_id')
+ ->has('created_at')
+ ->has('comments_count')
+ ->has('status')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+});
+
+it('does not render videos that are not processed', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory()->count(3)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processing,
+ ]);
+
+ $response = $this
+ ->get(route('home'));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 0)
+ );
+});
+
+it('renders paginated videos', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory()->count(10)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'perpage' => 5,
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->where('videos.current_page', 1)
+ ->has('videos.data', 5, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+
+ $response = $this
+ ->get(route('home', [
+ 'perpage' => 5,
+ 'page' => 2,
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->where('videos.current_page', 2)
+ ->has('videos.data', 5, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+});
+
+it('filters by course title', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory()->count(3)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'search' => $videos[0]->title,
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 1, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->where('id', $videos[0]->id)
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+});
+
+describe('filter videos by size', function () {
+ it('filters by video size range', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'size' => 7 * 1024 * 1024,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'size' => 20 * 1024 * 1024,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'size_min' => '5',
+ 'size_max' => '10',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 1, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->where('id', $videos[0]->id)
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+ });
+
+ it('filters by video size min', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'size' => 7 * 1024 * 1024,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'size' => 20 * 1024 * 1024,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'size_min' => '5',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 6)
+ );
+ });
+
+ it('filters by video size max', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ // size is stored in bytes
+ 'size' => 7 * 1024 * 1024,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'size' => 20 * 1024 * 1024,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'size_max' => '10',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 1, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->where('id', $videos[0]->id)
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('created_at')
+ ->has('comments_count')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+ });
+});
+
+describe('filter videos by duration', function () {
+ it('filters by video duration range in minutes', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 7 * 60,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 20 * 60,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'duration_min' => '5',
+ 'duration_max' => '10',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 1, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->where('id', $videos[0]->id)
+ ->has('title')
+ ->has('thumbnail')
+ ->has('duration')
+ ->has('status')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+ });
+
+ it('filters by video duration min in minutes', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 7 * 60,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 20 * 60,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'duration_min' => '5',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 6)
+ );
+ });
+
+ it('filters by video duration max in minutes', function () {
+ $user = User::factory()->create();
+ $videos = Video::factory(1)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 7 * 60,
+ ]);
+
+ Video::factory(5)->create([
+ 'user_id' => $user->id,
+ 'status' => VideoStatusEnum::Processed,
+ 'duration' => 20 * 60,
+ ]);
+
+ $response = $this
+ ->get(route('home', [
+ 'duration_max' => '10',
+ ]));
+
+ $response->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('Home')
+ ->has('videos')
+ ->has('videos.data', 1, fn (AssertableInertia $page) => $page
+ ->has('id')
+ ->where('id', $videos[0]->id)
+ ->has('title')
+ ->has('thumbnail')
+ ->has('status')
+ ->has('duration')
+ ->has('user_id')
+ ->has('comments_count')
+ ->has('created_at')
+ ->has('user')
+ ->has('tags')
+ )
+ );
+ });
+});
diff --git a/tests/Feature/Http/Controllers/TagControllerTest.php b/tests/Feature/Http/Controllers/TagControllerTest.php
new file mode 100644
index 0000000..3e8e16f
--- /dev/null
+++ b/tests/Feature/Http/Controllers/TagControllerTest.php
@@ -0,0 +1,82 @@
+create();
+
+ $this->actingAs($user);
+});
+
+describe('tag store', function () {
+
+ it('should store a new tag', function () {
+ $response = $this->post(route('tags.store'), [
+ 'name' => 'Laravel',
+ 'color' => '#FF0000',
+ ]);
+
+ $response->assertStatus(201);
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'Laravel',
+ 'color' => '#FF0000',
+ ]);
+ });
+
+ it('should not store a new tag with the same name', function () {
+ $tag = Tag::factory()->create();
+
+ $response = $this->post(route('tags.store'), [
+ 'name' => $tag->name,
+ 'color' => '#FF0000',
+ ]);
+
+ $response->assertStatus(302);
+
+ $count = Tag::where('name', $tag->name)->count();
+ $this->assertEquals(1, $count);
+ });
+});
+
+describe('tag update', function () {
+ it('should update a tag', function () {
+ $tag = Tag::factory()->create();
+
+ $response = $this->put(route('tags.update', $tag), [
+ 'name' => 'Laravel',
+ 'color' => '#FF0000',
+ ]);
+
+ $response->assertStatus(200);
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'Laravel',
+ 'color' => '#FF0000',
+ ]);
+ });
+
+ it('should not update a tag with the same name', function () {
+ $tag1 = Tag::factory()->create();
+ $tag2 = Tag::factory()->create();
+
+ $response = $this->put(route('tags.update', $tag1), [
+ 'name' => $tag2->name,
+ 'color' => '#FF0000',
+ ]);
+
+ $response->assertStatus(302);
+ });
+});
+
+describe('tag delete', function () {
+ it('should delete a tag', function () {
+ $tag = Tag::factory()->create();
+
+ $response = $this->delete(route('tags.destroy', $tag));
+
+ $response->assertStatus(204);
+ $this->assertDatabaseMissing('tags', [
+ 'name' => $tag->name,
+ 'color' => $tag->color,
+ ]);
+ });
+});
diff --git a/tests/Feature/Http/Controllers/VideoControllerTest.php b/tests/Feature/Http/Controllers/VideoControllerTest.php
new file mode 100644
index 0000000..341edb4
--- /dev/null
+++ b/tests/Feature/Http/Controllers/VideoControllerTest.php
@@ -0,0 +1,458 @@
+user = User::factory()->create();
+
+ $this->mock(MultimediaService::class, function ($mock) {
+ $mock->shouldReceive('compressVideo')
+ ->andReturn('videos/compressed.mp4');
+
+ $mock->shouldReceive('generateVideoThumbnail')
+ ->andReturn('thumbnails/default.jpg');
+
+ $mock->shouldReceive('getVideoMetadata')
+ ->andReturn([
+ 'size' => 1048576,
+ 'duration' => 60,
+ 'width' => 1920,
+ 'height' => 1080,
+ ]);
+ });
+
+ Notification::fake();
+ });
+
+ describe('show endpoint', function () {
+ it('shows a video', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos/' . $video->id)
+ ->assertStatus(200);
+
+ $response->assertInertia(function (AssertableInertia $page) use ($video) {
+ $page->where('video.id', $video->id);
+ $page->where('video.title', $video->title);
+ $page->where('video.description', $video->description);
+ $page->where('video.url', $video->url);
+ });
+ });
+
+ it('show a video as a non owner', function () {
+ $video = Video::factory()->create([
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos/' . $video->id)
+ ->assertStatus(200);
+ });
+
+ it('renders with the video user', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos/' . $video->id)
+ ->assertStatus(200)
+ ->assertInertia(function (AssertableInertia $page) {
+ $page->where('video.user.id', $this->user->id);
+ });
+ });
+
+ it('renders the video tags', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $tag = Tag::factory()->create();
+ $video->tags()->attach($tag->id);
+
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos/' . $video->id)
+ ->assertStatus(200);
+
+ $response->assertInertia(function (AssertableInertia $page) use ($tag) {
+ $page->where('video.tags.0.name', $tag->name);
+ });
+ });
+
+ it('renders with comments', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ 'status' => VideoStatusEnum::Processed,
+ ]);
+
+ $video->comments()->create([
+ 'user_id' => $this->user->id,
+ 'content' => 'My comment',
+ ]);
+
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos/' . $video->id)
+ ->assertStatus(200)
+ ->assertInertia(function (AssertableInertia $page) {
+ $page->where('video.comments.0.content', 'My comment');
+ });
+ });
+
+ it('doesnt allow to see a video in processing status', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ 'status' => VideoStatusEnum::Processing,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->get(route('videos.show', $video))
+ ->assertStatus(403);
+ });
+ });
+
+ describe('index endpoint', function () {
+ it('lists videos', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos')
+ ->assertStatus(200);
+
+ $response->assertSee($video->title);
+ });
+
+ it('lists only the user videos', function () {
+ $video = Video::factory()->create();
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos')
+ ->assertStatus(200);
+
+ $response->assertDontSee($video->title);
+ });
+
+ it('lists videos with tags', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+
+ $tag = Tag::factory()->create();
+ $video->tags()->attach($tag->id);
+
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos')
+ ->assertStatus(200);
+
+ $response->assertSee($tag->name);
+ });
+
+ it('lists videos with comment count', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+
+ $video->comments()->create([
+ 'user_id' => $this->user->id,
+ 'content' => 'My comment',
+ ]);
+
+ $this->actingAs($this->user);
+
+ $response = $this->get('/videos')
+ ->assertStatus(200)
+ ->assertInertia(function (AssertableInertia $page) {
+ $page->count('videos', 1);
+ $page->where('videos.0.comments_count', 1);
+ });
+
+
+ });
+ });
+
+ describe('create endpoint', function () {
+ it('creates a video', function () {
+ $this->actingAs($this->user);
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ $response = $this->post('/videos', [
+ 'title' => 'My video',
+ 'description' => 'My video description',
+ 'file' => $file,
+ ]);
+
+ // redirects to the video list
+ $response->assertRedirect('/videos');
+
+ $this->assertDatabaseHas('videos', [
+ 'title' => 'My video',
+ 'url' => 'videos/compressed.mp4',
+ 'description' => 'My video description',
+ 'size' => 1048576,
+ 'duration' => 60,
+ 'user_id' => $this->user->id,
+ ]);
+ });
+
+ it('stores the video file', function () {
+ $this->actingAs($this->user);
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ Storage::fake('public');
+
+ $response = $this->post('/videos', [
+ 'title' => 'My video',
+ 'description' => 'My video description',
+ 'file' => $file,
+ ]);
+
+ Storage::disk('public')->assertExists('videos/' . $file->hashName());
+ });
+
+ it('calls the video process jobs', function () {
+ $this->actingAs($this->user);
+ Bus::fake();
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ $response = $this->post('/videos', [
+ 'title' => 'My video',
+ 'description' => 'My video description',
+ 'file' => $file,
+ ]);
+
+ Bus::assertChained([
+ ProcessVideo::class,
+ GenerateVideoThumbnail::class,
+ SaveVideoMetadata::class,
+ UpdateVideoStatus::class,
+ SendVideoProcessingCompletedNotification::class,
+ ]);
+ });
+
+ it('creates a video with tags', function () {
+ $this->actingAs($this->user);
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ $response = $this->post('/videos', [
+ 'title' => 'My video',
+ 'description' => 'My video description',
+ 'file' => $file,
+ 'tags' => ['Gaming', 'Music'],
+ ]);
+
+ $video = Video::where('title', 'My video')->first();
+
+ // created tags
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'gaming',
+ ]);
+
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'music',
+ ]);
+
+ $tags = Tag::whereIn('name', ['gaming', 'music'])->get();
+
+ $this->assertDatabaseHas('video_tag', [
+ 'tag_id' => $tags[0]->id,
+ 'video_id' => $video->id,
+ ]);
+
+ $this->assertDatabaseHas('video_tag', [
+ 'tag_id' => $tags[1]->id,
+ 'video_id' => $video->id,
+ ]);
+ });
+ });
+
+ describe('update endpoint', function () {
+ it('updates a video', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->put('/videos/' . $video->id, [
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ ]);
+
+ $response->assertRedirect('/videos');
+
+ $this->assertDatabaseHas('videos', [
+ 'id' => $video->id,
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ ]);
+ });
+
+ it('updates a video with a new file', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ Storage::fake('public');
+
+ $response = $this->put('/videos/' . $video->id, [
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ 'file' => $file,
+ ]);
+
+ Storage::disk('public')->assertExists('videos/' . $file->hashName());
+ });
+
+ it('calls the video process jobs when updating the file', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+ Bus::fake();
+
+ $file = UploadedFile::fake()->create('video.mp4', 1024, 'video/mp4');
+
+ $response = $this->put('/videos/' . $video->id, [
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ 'file' => $file,
+ ]);
+
+ Bus::assertChained([
+ ProcessVideo::class,
+ GenerateVideoThumbnail::class,
+ SaveVideoMetadata::class,
+ UpdateVideoStatus::class,
+ SendVideoProcessingCompletedNotification::class,
+ ]);
+ });
+
+ it('updates a video with new tags', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $tags = Tag::factory(2)->create();
+
+ $video->tags()->sync($tags->pluck('id'));
+
+ $response = $this->put('/videos/' . $video->id, [
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ 'tags' => [
+ 'Gaming',
+ 'Music',
+ ],
+ ]);
+
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'Gaming',
+ ]);
+
+ $this->assertDatabaseHas('tags', [
+ 'name' => 'Music',
+ ]);
+
+ $tags = Tag::whereIn('name', ['Gaming', 'Music'])->get();
+
+ $this->assertDatabaseHas('video_tag', [
+ 'tag_id' => $tags[0]->id,
+ 'video_id' => $video->id,
+ ]);
+
+ $this->assertDatabaseHas('video_tag', [
+ 'tag_id' => $tags[1]->id,
+ 'video_id' => $video->id,
+ ]);
+ });
+
+ it('doesnt updates other user videos', function () {
+ $video = Video::factory()->create();
+ $this->actingAs($this->user);
+
+ $response = $this->put('/videos/' . $video->id, [
+ 'title' => 'My updated video',
+ 'description' => 'My updated video description',
+ ]);
+
+ $response->assertStatus(403);
+ });
+ });
+
+ describe('delete endpoint', function () {
+ it('deletes a video', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $response = $this->delete('/videos/' . $video->id);
+
+ $response->assertRedirect('/videos');
+
+ $this->assertDatabaseMissing('videos', [
+ 'id' => $video->id,
+ ]);
+ });
+
+ it('deletes the video file', function () {
+ $video = Video::factory()->create([
+ 'user_id' => $this->user->id,
+ ]);
+ $this->actingAs($this->user);
+
+ $storage = Storage::fake('public');
+ $storage->put($video->url, 'video content');
+ $storage->put($video->thumbnail, 'thumbnail content');
+
+ $response = $this->delete('/videos/' . $video->id);
+
+ Storage::disk('public')->assertMissing($video->url);
+ Storage::disk('public')->assertMissing($video->thumbnail);
+ });
+
+ it('cant delete other user videos', function () {
+ $video = Video::factory()->create();
+ $this->actingAs($this->user);
+
+ $response = $this->delete('/videos/' . $video->id);
+
+ $response->assertStatus(403);
+ });
+ });
+});
diff --git a/tests/Feature/Jobs/GenerateVideoThumbnailTest.php b/tests/Feature/Jobs/GenerateVideoThumbnailTest.php
new file mode 100644
index 0000000..bf240b7
--- /dev/null
+++ b/tests/Feature/Jobs/GenerateVideoThumbnailTest.php
@@ -0,0 +1,36 @@
+create();
+ $job = new GenerateVideoThumbnail($video);
+
+ expect($job->video)->toBe($video);
+ });
+
+ it('calls FFmpegService to generate a thumbnail', function () {
+ $ffmpegService = Mockery::mock(FFmpegService::class, function ($mock) {
+ $mock->shouldReceive('generateVideoThumbnail')->once();
+ });
+ $video = Video::factory()->create();
+
+ $job = new GenerateVideoThumbnail($video);
+ $job->handle($ffmpegService);
+ });
+
+ it('sets the thumbnail url on the video model', function () {
+ $ffmpegService = Mockery::mock(FFmpegService::class, function ($mock) {
+ $mock->shouldReceive('generateVideoThumbnail')->once()->andReturn('videos/video-thumbnail.jpg');
+ });
+ $video = Video::factory()->create();
+
+ $job = new GenerateVideoThumbnail($video);
+ $job->handle($ffmpegService);
+
+ expect($video->thumbnail)->toBe('videos/video-thumbnail.jpg');
+ });
+});
diff --git a/tests/Feature/Jobs/ProcessVideoTest.php b/tests/Feature/Jobs/ProcessVideoTest.php
new file mode 100644
index 0000000..83b26f9
--- /dev/null
+++ b/tests/Feature/Jobs/ProcessVideoTest.php
@@ -0,0 +1,71 @@
+create([
+ 'url' => 'videos/video.mp4',
+ ]);
+
+ $multimediaService = mock(MultimediaService::class);
+ $multimediaService->shouldReceive('compressVideo')
+ ->once()
+ ->with($video->url)
+ ->andReturn('videos/video-compressed.mp4');
+
+ $job = new ProcessVideo($video);
+ $job->handle($multimediaService);
+
+ expect($video->url)->toBe('videos/video-compressed.mp4');
+ });
+
+ it('sets video status to failed if compression fails', function () {
+ Storage::fake('public');
+
+ $video = Video::factory()->create([
+ 'url' => 'videos/video.mp4',
+ ]);
+
+ $job = new ProcessVideo($video);
+ $job->failed(new Exception('Failed to compress video'));
+
+ expect($video->status)->toBe(VideoStatusEnum::Failed);
+ });
+
+ it('removes the video from the storage if compression fails', function () {
+ $storage = Storage::fake('public');
+
+ $video = Video::factory()->create([
+ 'url' => 'videos/video.mp4',
+ ]);
+
+ $storage->put('videos/video.mp4', 'video content');
+
+ $job = (new ProcessVideo($video))->withFakeQueueInteractions();
+ $job->failed(new Exception('Failed to compress video'));
+
+ $storage->assertMissing('videos/video.mp4');
+ });
+
+ it('sends a notification if compression fails', function () {
+ Notification::fake();
+
+ $video = Video::factory()->create([
+ 'url' => 'videos/video.mp4',
+ ]);
+
+ $job = new ProcessVideo($video);
+ $job->failed(new Exception('Failed to compress video'));
+
+ Notification::assertSentTo($video->user, VideoProcessingFailedNotification::class);
+ });
+});
diff --git a/tests/Feature/Jobs/SaveVideoMetadataTest.php b/tests/Feature/Jobs/SaveVideoMetadataTest.php
new file mode 100644
index 0000000..6c2f918
--- /dev/null
+++ b/tests/Feature/Jobs/SaveVideoMetadataTest.php
@@ -0,0 +1,37 @@
+create([
+ 'url' => 'videos/video.mp4',
+ ]);
+
+ $multimediaService = mock(MultimediaService::class);
+ $multimediaService->shouldReceive('getVideoMetadata')
+ ->once()
+ ->with($video->url)
+ ->andReturn([
+ 'duration' => 60,
+ 'size' => 1024,
+ 'width' => 1920,
+ 'height' => 1080,
+ ]);
+
+ $job = new SaveVideoMetadata($video);
+ $job->handle($multimediaService);
+
+ $video->refresh();
+
+ expect($video->duration)->toBe(60);
+ expect($video->size)->toBe(1024);
+ expect($video->width)->toBe(1920);
+ expect($video->height)->toBe(1080);
+ });
+});
diff --git a/tests/Feature/Jobs/SendVideoProcessingCompletedNotificationTest.php b/tests/Feature/Jobs/SendVideoProcessingCompletedNotificationTest.php
new file mode 100644
index 0000000..3e881f4
--- /dev/null
+++ b/tests/Feature/Jobs/SendVideoProcessingCompletedNotificationTest.php
@@ -0,0 +1,14 @@
+create();
+
+ $job = new SendVideoProcessingCompletedNotification($video);
+
+ expect($job->video->id)->toBe($video->id);
+});
diff --git a/tests/Feature/Jobs/UpdateVideoStatusTest.php b/tests/Feature/Jobs/UpdateVideoStatusTest.php
new file mode 100644
index 0000000..14e1333
--- /dev/null
+++ b/tests/Feature/Jobs/UpdateVideoStatusTest.php
@@ -0,0 +1,12 @@
+create();
+
+ UpdateVideoStatus::dispatch($video, VideoStatusEnum::Processed);
+
+ $this->assertEquals(VideoStatusEnum::Processed, $video->refresh()->status);
+});
diff --git a/tests/Feature/Models/CommentTest.php b/tests/Feature/Models/CommentTest.php
new file mode 100644
index 0000000..77cd9e7
--- /dev/null
+++ b/tests/Feature/Models/CommentTest.php
@@ -0,0 +1,20 @@
+create();
+ $video = Video::factory()->create();
+
+ $comment = Comment::create([
+ 'content' => 'This is a comment',
+ 'user_id' => $user->id,
+ 'video_id' => $video->id,
+ ]);
+
+ expect($comment->user_id)->toBe($user->id);
+ expect($comment->video_id)->toBe($video->id);
+ expect($comment->content)->toBe('This is a comment');
+});
diff --git a/tests/Feature/Models/TagTest.php b/tests/Feature/Models/TagTest.php
new file mode 100644
index 0000000..7c7edfe
--- /dev/null
+++ b/tests/Feature/Models/TagTest.php
@@ -0,0 +1,10 @@
+create();
+
+ expect($tag->name)->toBeString();
+ expect($tag->color)->toBeString();
+});
diff --git a/tests/Feature/Models/VideoTest.php b/tests/Feature/Models/VideoTest.php
new file mode 100644
index 0000000..0aafac2
--- /dev/null
+++ b/tests/Feature/Models/VideoTest.php
@@ -0,0 +1,32 @@
+create();
+ expect($video->id)->toBeInt();
+});
+
+it('can load comments count if specified', function () {
+ $video = Video::factory()->hasComments(3)->create();
+
+ $video = Video::withCommentsCount()->find($video->id);
+
+ expect($video->comments_count)->toBe(3);
+});
+
+it('does not load comments count by default', function () {
+ $video = Video::factory()->hasComments(3)->create();
+
+ $video = Video::find($video->id);
+
+ expect($video->comments_count)->toBeNull();
+});
+
+it('can load with minimal attributes', function () {
+ $video = Video::factory()->create();
+
+ $video = Video::withMinAttributes()->find($video->id);
+
+ expect($video->getAttributes())
+ ->toHaveKeys(['id', 'title', 'thumbnail', 'status', 'user_id', 'duration', 'created_at']);
+});
diff --git a/tests/Feature/Notifications/VideoProcessingCompletedTest.php b/tests/Feature/Notifications/VideoProcessingCompletedTest.php
new file mode 100644
index 0000000..efbd664
--- /dev/null
+++ b/tests/Feature/Notifications/VideoProcessingCompletedTest.php
@@ -0,0 +1,61 @@
+create();
+
+ $notification = new VideoProcessingCompleted($video);
+
+ expect($notification->video->id)->toBe($video->id);
+ });
+
+ it('sends a notification', function () {
+ Notification::fake();
+
+ $video = Video::factory()->create();
+
+ $video->user->notify(new VideoProcessingCompleted($video));
+
+ Notification::assertSentTo($video->user, VideoProcessingCompleted::class);
+ });
+
+ it('sends a notification via mail, database and broadcast', function () {
+ Notification::fake();
+
+ $video = Video::factory()->create();
+
+ $video->user->notify(new VideoProcessingCompleted($video));
+ // this notification is queued, so we need to wait for the job to finish
+
+ Notification::assertSentTo($video->user, VideoProcessingCompleted::class, function ($notification, $channels) {
+ return in_array('mail', $channels) && in_array('database', $channels) && in_array('broadcast', $channels);
+ });
+ });
+
+ it('return correctly the array representation', function () {
+ $video = Video::factory()->create();
+
+ $notification = new VideoProcessingCompleted($video);
+
+ expect($notification->toArray($video->user))->toBe([
+ 'user_id' => $video->user->id,
+ 'video_id' => $video->id,
+ 'video_title' => $video->title,
+ ]);
+ });
+
+ it('return correctly the mail representation', function () {
+ $video = Video::factory()->create();
+
+ $notification = new VideoProcessingCompleted($video);
+
+ // should be a message like "Your video 'Video Title' has been processed!"
+
+ expect($notification->toMail($video->user)->subject)->toBe('Your video has been processed!');
+ expect($notification->toMail($video->user)->introLines)->toBe(["Your video '{$video->title}' has been processed!"]);
+ });
+});
diff --git a/tests/Feature/Notifications/VideoProcessingFailedNotificationTest.php b/tests/Feature/Notifications/VideoProcessingFailedNotificationTest.php
new file mode 100644
index 0000000..2d4a5b1
--- /dev/null
+++ b/tests/Feature/Notifications/VideoProcessingFailedNotificationTest.php
@@ -0,0 +1,60 @@
+create();
+
+ $notification = new VideoProcessingFailedNotification($video);
+
+ expect($notification->video->id)->toBe($video->id);
+ });
+
+ it('sends a notification', function () {
+ Notification::fake();
+
+ $video = Video::factory()->create();
+
+ $video->user->notify(new VideoProcessingFailedNotification($video));
+
+ Notification::assertSentTo($video->user, VideoProcessingFailedNotification::class);
+ });
+
+ it('sends a notification via mail, database and broadcast', function () {
+ Notification::fake();
+
+ $video = Video::factory()->create();
+
+ $video->user->notify(new VideoProcessingFailedNotification($video));
+ // this notification is queued, so we need to wait for the job to finish
+
+ Notification::assertSentTo($video->user, VideoProcessingFailedNotification::class, function ($notification, $channels) {
+ return in_array('mail', $channels) && in_array('database', $channels) && in_array('broadcast', $channels);
+ });
+ });
+
+ it('return correctly the array representation', function () {
+ $video = Video::factory()->create();
+
+ $notification = new VideoProcessingFailedNotification($video);
+
+ expect($notification->toArray($video->user))->toBe([
+ 'user_id' => $video->user->id,
+ 'video_id' => $video->id,
+ 'video_title' => $video->title,
+ ]);
+ });
+
+ it('return correctly the mail representation', function () {
+ $video = Video::factory()->create();
+
+ $notification = new VideoProcessingFailedNotification($video);
+
+ // should be a message like "Your video 'Video Title' has failed to process!"
+ expect($notification->toMail($video->user)->subject)->toBe('Your video has failed to process!');
+ expect($notification->toMail($video->user)->introLines)->toBe(["Your video '{$video->title}' has failed to process!"]);
+ });
+});
diff --git a/tests/Feature/Services/FFmpegServiceTest.php b/tests/Feature/Services/FFmpegServiceTest.php
new file mode 100644
index 0000000..e9df1d0
--- /dev/null
+++ b/tests/Feature/Services/FFmpegServiceTest.php
@@ -0,0 +1,84 @@
+putFileAs('videos', $file, 'video.mp4');
+
+ $filepath = 'videos/video.mp4';
+
+ $compressed_filepath = $service->compressVideo($filepath);
+
+ $expected_compressed_filepath = 'videos/video-compressed.mp4';
+ expect($compressed_filepath)->toBe($expected_compressed_filepath);
+
+ $originalSize = $file->getSize();
+ $compressedSize = $storage->size($compressed_filepath);
+
+ expect($storage->exists($compressed_filepath))->toBeTrue();
+ expect($compressedSize)->toBeLessThan($originalSize);
+ expect($compressedSize)->toBeGreaterThan(0);
+
+ $storage->delete($filepath);
+ $storage->delete($compressed_filepath);
+ });
+ });
+
+ describe('generateVideoThumbnail', function () {
+ it('generateVideoThumbnail should generate a thumbnail from a video', function () {
+ $service = new FFmpegService();
+
+ $file = new File(resource_path('tests/test_video.mp4'));
+ // videos at public disk
+ $storage = Storage::disk('public');
+
+ $storage->putFileAs('videos', $file, 'video.mp4');
+
+ $filepath = 'videos/video.mp4';
+
+ $thumbnail_filepath = $service->generateVideoThumbnail($filepath);
+
+ $expected_thumbnail_filepath = 'videos/video-thumbnail.jpg';
+
+ expect($thumbnail_filepath)->toBe($expected_thumbnail_filepath);
+ expect($storage->exists($thumbnail_filepath))->toBeTrue();
+
+ $storage->delete($filepath);
+ $storage->delete($thumbnail_filepath);
+ });
+ });
+
+ describe('getVideoMetadata', function () {
+ it('getVideoMetadata should return the metadata of a video', function () {
+ $service = new FFmpegService();
+
+ $file = new File(resource_path('tests/test_video.mp4'));
+ // videos at public disk
+ $storage = Storage::disk('public');
+
+ $storage->putFileAs('videos', $file, 'video.mp4');
+
+ $filepath = 'videos/video.mp4';
+
+ $metadata = $service->getVideoMetadata($filepath);
+
+ expect($metadata)->toBeArray();
+ expect($metadata)->toHaveKey('duration');
+ expect($metadata)->toHaveKey('width');
+ expect($metadata)->toHaveKey('height');
+ expect($metadata)->toHaveKey('size');
+
+ $storage->delete($filepath);
+ });
+ });
+});
diff --git a/tests/TestCase.php b/tests/TestCase.php
index fe1ffc2..db6fd1a 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -2,9 +2,18 @@
namespace Tests;
+use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
- //
+ use RefreshDatabase;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->refreshDatabase();
+ }
+
}