diff --git a/.env.ci b/.env.ci new file mode 100644 index 00000000..d040cedf --- /dev/null +++ b/.env.ci @@ -0,0 +1,2 @@ +APP_ENV=testing +APP_KEY=base64:Zfo+2dkSaHvem5LCiZS/baYHv2Pv1vrSc0F2NaF29Ec= diff --git a/README.md b/README.md index 03a4799f..b1519da5 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ TryPost

-

TryPost

-

The open-source social media scheduling platform

diff --git a/app/Http/Controllers/WorkspaceController.php b/app/Http/Controllers/WorkspaceController.php index 9a2aef7b..d9b699cd 100644 --- a/app/Http/Controllers/WorkspaceController.php +++ b/app/Http/Controllers/WorkspaceController.php @@ -59,7 +59,10 @@ public function store(StoreWorkspaceRequest $request): RedirectResponse ->with('message', 'Subscribe to create more workspaces.'); } - $workspace = $user->workspaces()->create($request->validated()); + $workspace = $user->workspaces()->create([ + ...$request->validated(), + 'timezone' => config('app.timezone', 'UTC'), + ]); // Set as current workspace $user->switchWorkspace($workspace); @@ -156,7 +159,7 @@ public function destroy(Request $request, Workspace $workspace): RedirectRespons $user->decrementWorkspaceQuantity(); } - return redirect()->route('dashboard') + return redirect()->route('workspaces.index') ->with('success', 'Workspace deleted successfully!'); } } diff --git a/config/mail.php b/config/mail.php index 9ee0d369..0db38742 100644 --- a/config/mail.php +++ b/config/mail.php @@ -111,8 +111,8 @@ */ 'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'address' => env('MAIL_FROM_ADDRESS', 'no-reply@trypost.it'), + 'name' => env('MAIL_FROM_NAME', 'TryPost'), ], ]; diff --git a/database/factories/WorkspaceInviteFactory.php b/database/factories/WorkspaceInviteFactory.php index e39c3b10..c685aa6e 100644 --- a/database/factories/WorkspaceInviteFactory.php +++ b/database/factories/WorkspaceInviteFactory.php @@ -28,18 +28,9 @@ public function definition(): array 'token' => Str::random(64), 'role' => Role::Member, 'status' => InviteStatus::Pending, - 'expires_at' => now()->addDays(7), ]; } - public function expired(): static - { - return $this->state(fn (array $attributes) => [ - 'expires_at' => now()->subDay(), - 'status' => InviteStatus::Expired, - ]); - } - public function accepted(): static { return $this->state(fn (array $attributes) => [ @@ -47,11 +38,4 @@ public function accepted(): static 'accepted_at' => now(), ]); } - - public function cancelled(): static - { - return $this->state(fn (array $attributes) => [ - 'status' => InviteStatus::Cancelled, - ]); - } } diff --git a/database/migrations/2026_01_14_233835_create_workspace_invites_table.php b/database/migrations/2026_01_14_233835_create_workspace_invites_table.php index 7fa92a15..d9ea1463 100644 --- a/database/migrations/2026_01_14_233835_create_workspace_invites_table.php +++ b/database/migrations/2026_01_14_233835_create_workspace_invites_table.php @@ -19,7 +19,6 @@ public function up(): void $table->string('token', 64)->unique(); $table->string('role')->default('member'); $table->string('status')->default('pending'); - $table->timestamp('expires_at'); $table->timestamp('accepted_at')->nullable(); $table->timestamps(); diff --git a/resources/js/layouts/GuestLayout.vue b/resources/js/layouts/GuestLayout.vue index 283de67c..c20c675f 100644 --- a/resources/js/layouts/GuestLayout.vue +++ b/resources/js/layouts/GuestLayout.vue @@ -1,7 +1,4 @@ - - diff --git a/resources/js/pages/legal/Privacy.vue b/resources/js/pages/legal/Privacy.vue deleted file mode 100644 index fa4c6498..00000000 --- a/resources/js/pages/legal/Privacy.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - diff --git a/resources/js/pages/legal/Terms.vue b/resources/js/pages/legal/Terms.vue deleted file mode 100644 index cc8a7009..00000000 --- a/resources/js/pages/legal/Terms.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - diff --git a/routes/web.php b/routes/web.php index 50a7bfb2..029e606b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,22 +22,10 @@ use App\Http\Controllers\WorkspaceLabelController; use App\Http\Middleware\EnsureUserSetupIsComplete; use Illuminate\Support\Facades\Route; -use Inertia\Inertia; -use Laravel\Fortify\Features; Route::get('/', function () { - return Inertia::render('Welcome', [ - 'canRegister' => Features::enabled(Features::registration()), - ]); -})->name('home'); - -Route::get('/privacy', function () { - return Inertia::render('legal/Privacy'); -})->name('privacy'); - -Route::get('/terms', function () { - return Inertia::render('legal/Terms'); -})->name('terms'); + return redirect()->route('calendar'); +})->name('home')->middleware(['auth', 'verified']); // Subscription selection (requires auth but not subscription) Route::middleware(['auth', 'verified'])->group(function () { diff --git a/tests/Feature/BillingControllerTest.php b/tests/Feature/BillingControllerTest.php new file mode 100644 index 00000000..13bdc8f3 --- /dev/null +++ b/tests/Feature/BillingControllerTest.php @@ -0,0 +1,96 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Subscribe tests +test('subscribe requires authentication', function () { + $response = $this->get(route('subscribe')); + + $response->assertRedirect(route('login')); +}); + +test('subscribe shows subscription page', function () { + $response = $this->actingAs($this->user)->get(route('subscribe')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('billing/Subscribe', false) + ->has('trialDays') + ); +}); + +// Index tests +test('billing index requires authentication', function () { + $response = $this->get(route('billing.index')); + + $response->assertRedirect(route('login')); +}); + +test('billing index shows billing dashboard', function () { + $response = $this->actingAs($this->user)->get(route('billing.index')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('billing/Index', false) + ->has('hasSubscription') + ->has('workspacesCount') + ); +}); + +// Processing tests +test('billing processing requires authentication', function () { + $response = $this->get(route('billing.processing')); + + $response->assertRedirect(route('login')); +}); + +test('billing processing shows processing page', function () { + $response = $this->actingAs($this->user)->get(route('billing.processing')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('billing/Processing', false) + ->has('userId') + ->has('status') + ); +}); + +test('billing processing accepts status parameter', function () { + $response = $this->actingAs($this->user)->get(route('billing.processing', ['status' => 'success'])); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('status', 'success') + ); +}); + +test('billing processing validates status parameter', function () { + $response = $this->actingAs($this->user)->get(route('billing.processing', ['status' => 'invalid'])); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('status', 'processing') + ); +}); + +// Checkout tests +test('checkout requires authentication', function () { + $response = $this->post(route('billing.checkout')); + + $response->assertRedirect(route('login')); +}); + +// Portal tests +test('portal requires authentication', function () { + $response = $this->get(route('billing.portal')); + + $response->assertRedirect(route('login')); +}); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index a5762793..eb11b08f 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,7 +1,17 @@ get(route('home')); - $response->assertOk(); -}); \ No newline at end of file + $response->assertRedirect(route('login')); +}); + +test('home redirects to calendar for authenticated users', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('home')); + + $response->assertRedirect(route('calendar')); +}); diff --git a/tests/Feature/MediaControllerTest.php b/tests/Feature/MediaControllerTest.php new file mode 100644 index 00000000..7b157ecb --- /dev/null +++ b/tests/Feature/MediaControllerTest.php @@ -0,0 +1,140 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); + + $this->socialAccount = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]); + $this->post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + $this->postPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $this->socialAccount->id, + ]); +}); + +// Store tests +test('store media requires authentication', function () { + $response = $this->post(route('medias.store'), [ + 'model' => 'postPlatform', + 'model_id' => $this->postPlatform->id, + 'media' => UploadedFile::fake()->image('test.jpg'), + ]); + + $response->assertRedirect(route('login')); +}); + +test('store media uploads file', function () { + $response = $this->actingAs($this->user)->post(route('medias.store'), [ + 'model' => 'App\Models\PostPlatform', + 'model_id' => $this->postPlatform->id, + 'media' => UploadedFile::fake()->image('test.jpg'), + ]); + + $response->assertOk(); + $response->assertJsonStructure(['id', 'url', 'type', 'original_filename']); +}); + +test('store media validates required fields', function () { + $response = $this->actingAs($this->user)->post(route('medias.store'), [ + 'model' => '', + 'model_id' => '', + ]); + + $response->assertSessionHasErrors(['model', 'model_id', 'media']); +}); + +// Destroy tests +test('destroy media requires authentication', function () { + $media = Media::factory()->create([ + 'mediable_id' => $this->postPlatform->id, + 'mediable_type' => 'postPlatform', + ]); + + $response = $this->delete(route('medias.destroy', [$this->postPlatform->id, $media])); + + $response->assertRedirect(route('login')); +}); + +test('destroy media deletes the media', function () { + $media = Media::factory()->create([ + 'mediable_id' => $this->postPlatform->id, + 'mediable_type' => 'postPlatform', + ]); + + $response = $this->actingAs($this->user)->delete(route('medias.destroy', [$this->postPlatform->id, $media])); + + $response->assertOk(); + $response->assertJson(['success' => true]); + expect(Media::find($media->id))->toBeNull(); +}); + +test('destroy media returns 403 for mismatched model', function () { + $otherPostPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $this->socialAccount->id, + ]); + + $media = Media::factory()->create([ + 'mediable_id' => $otherPostPlatform->id, + 'mediable_type' => 'postPlatform', + ]); + + $response = $this->actingAs($this->user)->delete(route('medias.destroy', [$this->postPlatform->id, $media])); + + $response->assertForbidden(); +}); + +// Duplicate tests +test('duplicate media requires authentication', function () { + $media = Media::factory()->create([ + 'mediable_id' => $this->postPlatform->id, + 'mediable_type' => 'postPlatform', + ]); + + $response = $this->post(route('medias.duplicate', $media), [ + 'targets' => [], + ]); + + $response->assertRedirect(route('login')); +}); + +test('duplicate media creates copies', function () { + $media = Media::factory()->create([ + 'mediable_id' => $this->postPlatform->id, + 'mediable_type' => 'postPlatform', + ]); + + $otherPostPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $this->socialAccount->id, + ]); + + $response = $this->actingAs($this->user)->post(route('medias.duplicate', $media), [ + 'targets' => [ + [ + 'model' => 'postPlatform', + 'model_id' => $otherPostPlatform->id, + ], + ], + ]); + + $response->assertOk(); + $response->assertJsonCount(1); + + expect(Media::where('mediable_id', $otherPostPlatform->id)->count())->toBe(1); +}); diff --git a/tests/Feature/OnboardingControllerTest.php b/tests/Feature/OnboardingControllerTest.php new file mode 100644 index 00000000..19653bcb --- /dev/null +++ b/tests/Feature/OnboardingControllerTest.php @@ -0,0 +1,141 @@ +user = User::factory()->create(['setup' => Setup::Role]); +}); + +// Step 1 tests +test('step1 requires authentication', function () { + $response = $this->get(route('onboarding.step1')); + + $response->assertRedirect(route('login')); +}); + +test('step1 shows persona selection', function () { + $response = $this->actingAs($this->user)->get(route('onboarding.step1')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('onboarding/Step1', false) + ->has('personas') + ); +}); + +// Store Step 1 tests +test('store step1 requires authentication', function () { + $response = $this->post(route('onboarding.step1.store'), [ + 'persona' => Persona::Founder->value, + ]); + + $response->assertRedirect(route('login')); +}); + +test('store step1 saves persona and redirects to step2', function () { + $response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [ + 'persona' => Persona::Founder->value, + ]); + + $response->assertRedirect(route('onboarding.step2')); + + $this->user->refresh(); + expect($this->user->persona)->toBe(Persona::Founder); + expect($this->user->setup)->toBe(Setup::Connections); +}); + +test('store step1 validates persona is required', function () { + $response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [ + 'persona' => '', + ]); + + $response->assertSessionHasErrors('persona'); +}); + +test('store step1 validates persona is valid enum', function () { + $response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [ + 'persona' => 'invalid', + ]); + + $response->assertSessionHasErrors('persona'); +}); + +// Step 2 tests +test('step2 requires authentication', function () { + $response = $this->get(route('onboarding.step2')); + + $response->assertRedirect(route('login')); +}); + +test('step2 shows social accounts connection', function () { + $this->user->update(['setup' => Setup::Connections]); + + $response = $this->actingAs($this->user)->get(route('onboarding.step2')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('onboarding/Step2', false) + ->has('platforms') + ->has('hasWorkspace') + ); +}); + +test('step2 shows connected accounts for workspace', function () { + $this->user->update(['setup' => Setup::Connections]); + $workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $workspace->id]); + + SocialAccount::factory()->create([ + 'workspace_id' => $workspace->id, + 'platform' => Platform::LinkedIn, + ]); + + $response = $this->actingAs($this->user)->get(route('onboarding.step2')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('hasWorkspace', true) + ); +}); + +// Store Step 2 tests +test('store step2 requires authentication', function () { + $response = $this->post(route('onboarding.step2.store')); + + $response->assertRedirect(route('login')); +}); + +test('store step2 completes setup in self-hosted mode', function () { + config(['trypost.self_hosted' => true]); + $this->user->update(['setup' => Setup::Connections]); + + $response = $this->actingAs($this->user)->post(route('onboarding.step2.store')); + + $response->assertRedirect(route('calendar')); + + $this->user->refresh(); + expect($this->user->setup)->toBe(Setup::Completed); +}); + +// Complete tests +test('complete requires authentication', function () { + $response = $this->get(route('onboarding.complete')); + + $response->assertRedirect(route('login')); +}); + +test('complete marks setup as completed', function () { + $this->user->update(['setup' => Setup::Subscription]); + + $response = $this->actingAs($this->user)->get(route('onboarding.complete')); + + $response->assertRedirect(route('calendar')); + + $this->user->refresh(); + expect($this->user->setup)->toBe(Setup::Completed); +}); diff --git a/tests/Feature/PostControllerTest.php b/tests/Feature/PostControllerTest.php new file mode 100644 index 00000000..eaf711c1 --- /dev/null +++ b/tests/Feature/PostControllerTest.php @@ -0,0 +1,289 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); + + $this->socialAccount = SocialAccount::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'platform' => Platform::LinkedIn, + ]); +}); + +// Index tests +test('posts index requires authentication', function () { + $response = $this->get(route('posts.index')); + + $response->assertRedirect(route('login')); +}); + +test('posts index shows posts for current workspace', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->get(route('posts.index')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('posts/Index', false) + ->has('posts.data', 1) + ); +}); + +test('posts index redirects to create workspace if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('posts.index')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Calendar tests +test('calendar requires authentication', function () { + $response = $this->get(route('calendar')); + + $response->assertRedirect(route('login')); +}); + +test('calendar shows posts for current week', function () { + $response = $this->actingAs($this->user)->get(route('calendar')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('posts/Calendar') + ->has('workspace') + ->has('posts') + ->has('currentWeekStart') + ->has('view') + ); +}); + +test('calendar supports month view', function () { + $response = $this->actingAs($this->user)->get(route('calendar', ['view' => 'month'])); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('view', 'month') + ); +}); + +// Create tests +test('create post requires authentication', function () { + $response = $this->get(route('posts.create')); + + $response->assertRedirect(route('login')); +}); + +test('create post redirects to accounts if no social accounts connected', function () { + $this->socialAccount->delete(); + + $response = $this->actingAs($this->user)->get(route('posts.create')); + + $response->assertRedirect(route('accounts')); +}); + +test('create post creates draft and redirects to edit', function () { + $response = $this->actingAs($this->user)->get(route('posts.create')); + + $response->assertRedirect(); + + $post = Post::where('workspace_id', $this->workspace->id)->first(); + expect($post)->not->toBeNull(); + expect($post->status)->toBe(PostStatus::Draft); + expect($post->postPlatforms)->toHaveCount(1); +}); + +// Edit tests +test('edit post requires authentication', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->get(route('posts.edit', $post)); + + $response->assertRedirect(route('login')); +}); + +test('edit post shows edit page', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + 'status' => PostStatus::Draft, + ]); + + PostPlatform::factory()->create([ + 'post_id' => $post->id, + 'social_account_id' => $this->socialAccount->id, + ]); + + $response = $this->actingAs($this->user)->get(route('posts.edit', $post)); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('posts/Edit') + ->has('post') + ->has('socialAccounts') + ); +}); + +test('edit post returns 404 for post from different workspace', function () { + $otherWorkspace = Workspace::factory()->create(); + $post = Post::factory()->create([ + 'workspace_id' => $otherWorkspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->get(route('posts.edit', $post)); + + $response->assertNotFound(); +}); + +test('edit post redirects to show for published posts', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + 'status' => PostStatus::Published, + ]); + + $response = $this->actingAs($this->user)->get(route('posts.edit', $post)); + + $response->assertRedirect(route('posts.show', $post)); +}); + +// Show tests +test('show post requires authentication', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->get(route('posts.show', $post)); + + $response->assertRedirect(route('login')); +}); + +test('show post displays post details', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + PostPlatform::factory()->create([ + 'post_id' => $post->id, + 'social_account_id' => $this->socialAccount->id, + ]); + + $response = $this->actingAs($this->user)->get(route('posts.show', $post)); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('posts/Show') + ->has('post') + ); +}); + +// Update tests +test('update post requires authentication', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->put(route('posts.update', $post), []); + + $response->assertRedirect(route('login')); +}); + +test('update post saves changes', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + 'status' => PostStatus::Draft, + ]); + + $postPlatform = PostPlatform::factory()->create([ + 'post_id' => $post->id, + 'social_account_id' => $this->socialAccount->id, + 'content' => 'Original content', + ]); + + $response = $this->actingAs($this->user)->put(route('posts.update', $post), [ + 'status' => 'draft', + 'platforms' => [ + [ + 'id' => $postPlatform->id, + 'content' => 'Updated content', + 'content_type' => ContentType::LinkedInPost->value, + ], + ], + ]); + + $response->assertRedirect(); + + $postPlatform->refresh(); + expect($postPlatform->content)->toBe('Updated content'); +}); + +test('update post cannot update published posts', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + 'status' => PostStatus::Published, + ]); + + $response = $this->actingAs($this->user)->put(route('posts.update', $post), [ + 'status' => 'draft', + ]); + + $response->assertRedirect(); +}); + +// Destroy tests +test('destroy post requires authentication', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->delete(route('posts.destroy', $post)); + + $response->assertRedirect(route('login')); +}); + +test('destroy post deletes the post', function () { + $post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->delete(route('posts.destroy', $post)); + + $response->assertRedirect(route('calendar')); + expect(Post::find($post->id))->toBeNull(); +}); + +test('destroy post returns 404 for post from different workspace', function () { + $otherWorkspace = Workspace::factory()->create(); + $post = Post::factory()->create([ + 'workspace_id' => $otherWorkspace->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->delete(route('posts.destroy', $post)); + + $response->assertNotFound(); +}); diff --git a/tests/Feature/SocialControllerTest.php b/tests/Feature/SocialControllerTest.php new file mode 100644 index 00000000..12076992 --- /dev/null +++ b/tests/Feature/SocialControllerTest.php @@ -0,0 +1,71 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Index tests +test('accounts index requires authentication', function () { + $response = $this->get(route('accounts')); + + $response->assertRedirect(route('login')); +}); + +test('accounts index shows platforms and connected accounts', function () { + SocialAccount::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'platform' => Platform::LinkedIn, + ]); + + $response = $this->actingAs($this->user)->get(route('accounts')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('accounts/Index', false) + ->has('workspace') + ->has('platforms') + ); +}); + +test('accounts index redirects if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('accounts')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Disconnect tests +test('disconnect requires authentication', function () { + $account = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->delete(route('accounts.disconnect', $account)); + + $response->assertRedirect(route('login')); +}); + +test('disconnect removes social account', function () { + $account = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->delete(route('accounts.disconnect', $account)); + + $response->assertRedirect(); + expect(SocialAccount::find($account->id))->toBeNull(); +}); + +test('disconnect returns 403 for other workspace account', function () { + $otherWorkspace = Workspace::factory()->create(); + $account = SocialAccount::factory()->create(['workspace_id' => $otherWorkspace->id]); + + $response = $this->actingAs($this->user)->delete(route('accounts.disconnect', $account)); + + $response->assertForbidden(); +}); diff --git a/tests/Feature/WorkspaceControllerTest.php b/tests/Feature/WorkspaceControllerTest.php new file mode 100644 index 00000000..bf6a4c05 --- /dev/null +++ b/tests/Feature/WorkspaceControllerTest.php @@ -0,0 +1,237 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Index tests +test('workspaces index requires authentication', function () { + $response = $this->get(route('workspaces.index')); + + $response->assertRedirect(route('login')); +}); + +test('workspaces index shows all workspaces for user', function () { + Workspace::factory()->count(2)->create(['user_id' => $this->user->id]); + + $response = $this->actingAs($this->user)->get(route('workspaces.index')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('workspaces/Index', false) + ->has('workspaces', 3) + ->has('currentWorkspaceId') + ); +}); + +// Create tests +test('create workspace requires authentication', function () { + $response = $this->get(route('workspaces.create')); + + $response->assertRedirect(route('login')); +}); + +test('create workspace shows form for user with no workspaces', function () { + // Delete existing workspace so user has none + $this->user->update(['current_workspace_id' => null]); + $this->workspace->delete(); + + $response = $this->actingAs($this->user)->get(route('workspaces.create')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('workspaces/Create', false) + ); +}); + +test('create workspace redirects to billing if user already has workspace and no subscription', function () { + $response = $this->actingAs($this->user)->get(route('workspaces.create')); + + $response->assertRedirect(route('billing.index')); +}); + +// Store tests +test('store workspace requires authentication', function () { + $response = $this->post(route('workspaces.store'), ['name' => 'Test Workspace']); + + $response->assertRedirect(route('login')); +}); + +test('store workspace creates first workspace', function () { + // Delete existing workspace so user has none + $this->user->update(['current_workspace_id' => null]); + $this->workspace->delete(); + + $response = $this->actingAs($this->user)->post(route('workspaces.store'), [ + 'name' => 'New Workspace', + ]); + + $response->assertRedirect(route('calendar')); + + $this->assertDatabaseHas('workspaces', [ + 'name' => 'New Workspace', + 'user_id' => $this->user->id, + ]); +}); + +test('store workspace redirects to billing for second workspace without subscription', function () { + $response = $this->actingAs($this->user)->post(route('workspaces.store'), [ + 'name' => 'Second Workspace', + ]); + + $response->assertRedirect(route('billing.index')); +}); + +test('store workspace validates name is required', function () { + $response = $this->actingAs($this->user)->post(route('workspaces.store'), [ + 'name' => '', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('store workspace sets new workspace as current', function () { + // Delete existing workspace so user has none + $this->user->update(['current_workspace_id' => null]); + $this->workspace->delete(); + + $this->actingAs($this->user)->post(route('workspaces.store'), [ + 'name' => 'New Workspace', + ]); + + $this->user->refresh(); + $newWorkspace = Workspace::where('name', 'New Workspace')->first(); + + expect($this->user->current_workspace_id)->toBe($newWorkspace->id); +}); + +// Switch tests +test('switch workspace requires authentication', function () { + $response = $this->post(route('workspaces.switch', $this->workspace)); + + $response->assertRedirect(route('login')); +}); + +test('switch workspace changes current workspace', function () { + $otherWorkspace = Workspace::factory()->create(['user_id' => $this->user->id]); + + $response = $this->actingAs($this->user)->post(route('workspaces.switch', $otherWorkspace)); + + $response->assertRedirect(route('calendar')); + + $this->user->refresh(); + expect($this->user->current_workspace_id)->toBe($otherWorkspace->id); +}); + +test('switch workspace returns 403 for workspace user does not belong to', function () { + $otherUser = User::factory()->create(['setup' => Setup::Completed]); + $otherWorkspace = Workspace::factory()->create(['user_id' => $otherUser->id]); + + $response = $this->actingAs($this->user)->post(route('workspaces.switch', $otherWorkspace)); + + $response->assertForbidden(); +}); + +// Settings tests +test('workspace settings requires authentication', function () { + $response = $this->get(route('workspace.settings')); + + $response->assertRedirect(route('login')); +}); + +test('workspace settings shows settings page', function () { + $response = $this->actingAs($this->user)->get(route('workspace.settings')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('settings/Workspace', false) + ->has('workspace') + ->has('timezones') + ); +}); + +test('workspace settings redirects to create if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('workspace.settings')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Update settings tests +test('update workspace settings requires authentication', function () { + $response = $this->put(route('workspace.settings.update'), [ + 'name' => 'Updated Name', + 'timezone' => 'America/New_York', + ]); + + $response->assertRedirect(route('login')); +}); + +test('update workspace settings updates workspace', function () { + $response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [ + 'name' => 'Updated Name', + 'timezone' => 'America/New_York', + ]); + + $response->assertRedirect(route('workspace.settings')); + + $this->workspace->refresh(); + expect($this->workspace->name)->toBe('Updated Name'); + expect($this->workspace->timezone)->toBe('America/New_York'); +}); + +test('update workspace settings validates required fields', function () { + $response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [ + 'name' => '', + 'timezone' => '', + ]); + + $response->assertSessionHasErrors(['name', 'timezone']); +}); + +test('update workspace settings validates timezone', function () { + $response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [ + 'name' => 'Valid Name', + 'timezone' => 'Invalid/Timezone', + ]); + + $response->assertSessionHasErrors('timezone'); +}); + +// Destroy tests +test('destroy workspace requires authentication', function () { + $response = $this->delete(route('workspaces.destroy', $this->workspace)); + + $response->assertRedirect(route('login')); +}); + +test('destroy workspace deletes the workspace', function () { + $workspaceId = $this->workspace->id; + + $response = $this->actingAs($this->user)->delete(route('workspaces.destroy', $this->workspace)); + + $response->assertRedirect(route('workspaces.index')); + expect(Workspace::find($workspaceId))->toBeNull(); +}); + +test('destroy workspace clears current workspace if deleting current', function () { + $this->actingAs($this->user)->delete(route('workspaces.destroy', $this->workspace)); + + $this->user->refresh(); + expect($this->user->current_workspace_id)->toBeNull(); +}); + +test('destroy workspace returns 403 for non-owner', function () { + $otherUser = User::factory()->create(['setup' => Setup::Completed]); + + $response = $this->actingAs($otherUser)->delete(route('workspaces.destroy', $this->workspace)); + + $response->assertForbidden(); +}); diff --git a/tests/Feature/WorkspaceHashtagControllerTest.php b/tests/Feature/WorkspaceHashtagControllerTest.php new file mode 100644 index 00000000..54aca131 --- /dev/null +++ b/tests/Feature/WorkspaceHashtagControllerTest.php @@ -0,0 +1,140 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Index tests +test('hashtags index requires authentication', function () { + $response = $this->get(route('hashtags.index')); + + $response->assertRedirect(route('login')); +}); + +test('hashtags index shows hashtags for workspace', function () { + WorkspaceHashtag::factory()->count(3)->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->get(route('hashtags.index')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('hashtags/Index', false) + ->has('workspace') + ->has('hashtags', 3) + ); +}); + +test('hashtags index redirects if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('hashtags.index')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Store tests +test('store hashtag requires authentication', function () { + $response = $this->post(route('hashtags.store'), [ + 'name' => 'Marketing', + 'hashtags' => '#marketing #digital #growth', + ]); + + $response->assertRedirect(route('login')); +}); + +test('store hashtag creates hashtag group', function () { + $response = $this->actingAs($this->user)->post(route('hashtags.store'), [ + 'name' => 'Marketing', + 'hashtags' => '#marketing #digital #growth', + ]); + + $response->assertRedirect(route('hashtags.index')); + + $this->assertDatabaseHas('workspace_hashtags', [ + 'workspace_id' => $this->workspace->id, + 'name' => 'Marketing', + 'hashtags' => '#marketing #digital #growth', + ]); +}); + +test('store hashtag validates required fields', function () { + $response = $this->actingAs($this->user)->post(route('hashtags.store'), [ + 'name' => '', + 'hashtags' => '', + ]); + + $response->assertSessionHasErrors(['name', 'hashtags']); +}); + +// Update tests +test('update hashtag requires authentication', function () { + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->put(route('hashtags.update', $hashtag), [ + 'name' => 'Updated Name', + 'hashtags' => '#updated #hashtags', + ]); + + $response->assertRedirect(route('login')); +}); + +test('update hashtag updates the hashtag group', function () { + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->put(route('hashtags.update', $hashtag), [ + 'name' => 'Updated Name', + 'hashtags' => '#updated #hashtags', + ]); + + $response->assertRedirect(route('hashtags.index')); + + $hashtag->refresh(); + expect($hashtag->name)->toBe('Updated Name'); + expect($hashtag->hashtags)->toBe('#updated #hashtags'); +}); + +test('update hashtag returns 404 for other workspace hashtag', function () { + $otherWorkspace = Workspace::factory()->create(); + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $otherWorkspace->id]); + + $response = $this->actingAs($this->user)->put(route('hashtags.update', $hashtag), [ + 'name' => 'Updated Name', + 'hashtags' => '#updated #hashtags', + ]); + + $response->assertNotFound(); +}); + +// Destroy tests +test('destroy hashtag requires authentication', function () { + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->delete(route('hashtags.destroy', $hashtag)); + + $response->assertRedirect(route('login')); +}); + +test('destroy hashtag deletes the hashtag group', function () { + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->delete(route('hashtags.destroy', $hashtag)); + + $response->assertRedirect(route('hashtags.index')); + expect(WorkspaceHashtag::find($hashtag->id))->toBeNull(); +}); + +test('destroy hashtag returns 404 for other workspace hashtag', function () { + $otherWorkspace = Workspace::factory()->create(); + $hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $otherWorkspace->id]); + + $response = $this->actingAs($this->user)->delete(route('hashtags.destroy', $hashtag)); + + $response->assertNotFound(); +}); diff --git a/tests/Feature/WorkspaceInviteControllerTest.php b/tests/Feature/WorkspaceInviteControllerTest.php new file mode 100644 index 00000000..396f2bfc --- /dev/null +++ b/tests/Feature/WorkspaceInviteControllerTest.php @@ -0,0 +1,245 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Index tests +test('members index requires authentication', function () { + $response = $this->get(route('members')); + + $response->assertRedirect(route('login')); +}); + +test('members index shows members and invites', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->get(route('members')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('settings/Members', false) + ->has('workspace') + ->has('invites') + ->has('members') + ->has('owner') + ->has('roles') + ); +}); + +test('members index redirects if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('members')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Store invite tests +test('store invite requires authentication', function () { + $response = $this->post(route('invites.store'), [ + 'email' => 'test@example.com', + 'role' => WorkspaceRole::Member->value, + ]); + + $response->assertRedirect(route('login')); +}); + +test('store invite creates invite and sends email', function () { + $response = $this->actingAs($this->user)->post(route('invites.store'), [ + 'email' => 'newmember@example.com', + 'role' => WorkspaceRole::Member->value, + ]); + + $response->assertRedirect(); + + $this->assertDatabaseHas('workspace_invites', [ + 'workspace_id' => $this->workspace->id, + 'email' => 'newmember@example.com', + ]); + + Mail::assertQueued(WorkspaceInviteMail::class); +}); + +test('store invite fails if invite already exists', function () { + WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + 'email' => 'existing@example.com', + ]); + + $response = $this->actingAs($this->user)->post(route('invites.store'), [ + 'email' => 'existing@example.com', + 'role' => WorkspaceRole::Member->value, + ]); + + $response->assertSessionHasErrors('email'); +}); + +test('store invite fails if user is already member', function () { + $member = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]); + + $response = $this->actingAs($this->user)->post(route('invites.store'), [ + 'email' => $member->email, + 'role' => WorkspaceRole::Member->value, + ]); + + $response->assertSessionHasErrors('email'); +}); + +// Destroy invite tests +test('destroy invite requires authentication', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->delete(route('invites.destroy', $invite)); + + $response->assertRedirect(route('login')); +}); + +test('destroy invite deletes invite', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->delete(route('invites.destroy', $invite)); + + $response->assertRedirect(); + expect(WorkspaceInvite::find($invite->id))->toBeNull(); +}); + +test('destroy invite returns 404 for other workspace invite', function () { + $otherWorkspace = Workspace::factory()->create(); + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $otherWorkspace->id, + ]); + + $response = $this->actingAs($this->user)->delete(route('invites.destroy', $invite)); + + $response->assertNotFound(); +}); + +// Show invite tests +test('show invite displays invite details', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->get(route('invites.show', $invite->token)); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('invites/Accept', false) + ->has('invite') + ); +}); + +test('show invite redirects for accepted invite', function () { + $invite = WorkspaceInvite::factory()->accepted()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->get(route('invites.show', $invite->token)); + + $response->assertRedirect(route('login')); +}); + +// Accept invite tests +test('accept invite redirects to login if not authenticated', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $response = $this->post(route('invites.accept', $invite->token)); + + $response->assertRedirect(route('login')); +}); + +test('accept invite adds user to workspace', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $newUser = User::factory()->create(['setup' => Setup::Completed]); + + $response = $this->actingAs($newUser)->post(route('invites.accept', $invite->token)); + + $response->assertRedirect(route('calendar')); + expect($this->workspace->hasMember($newUser))->toBeTrue(); +}); + +test('accept invite redirects for accepted invite', function () { + $invite = WorkspaceInvite::factory()->accepted()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $newUser = User::factory()->create(['setup' => Setup::Completed]); + + $response = $this->actingAs($newUser)->post(route('invites.accept', $invite->token)); + + $response->assertRedirect(route('login')); +}); + +test('accept invite handles already member', function () { + $invite = WorkspaceInvite::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'invited_by' => $this->user->id, + ]); + + $member = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]); + + $response = $this->actingAs($member)->post(route('invites.accept', $invite->token)); + + $response->assertRedirect(route('calendar')); + $response->assertSessionHas('flash.banner', 'You are already a member of this workspace.'); +}); + +// Remove member tests +test('remove member requires authentication', function () { + $member = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]); + + $response = $this->delete(route('members.remove', $member)); + + $response->assertRedirect(route('login')); +}); + +test('remove member removes user from workspace', function () { + $member = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]); + + $response = $this->actingAs($this->user)->delete(route('members.remove', $member)); + + $response->assertRedirect(); + expect($this->workspace->hasMember($member))->toBeFalse(); +}); + +test('remove member fails for owner', function () { + $response = $this->actingAs($this->user)->delete(route('members.remove', $this->user)); + + $response->assertSessionHasErrors('member'); +}); diff --git a/tests/Feature/WorkspaceLabelControllerTest.php b/tests/Feature/WorkspaceLabelControllerTest.php new file mode 100644 index 00000000..eb7151c8 --- /dev/null +++ b/tests/Feature/WorkspaceLabelControllerTest.php @@ -0,0 +1,149 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); +}); + +// Index tests +test('labels index requires authentication', function () { + $response = $this->get(route('labels.index')); + + $response->assertRedirect(route('login')); +}); + +test('labels index shows labels for workspace', function () { + WorkspaceLabel::factory()->count(3)->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->get(route('labels.index')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('labels/Index', false) + ->has('workspace') + ->has('labels', 3) + ); +}); + +test('labels index redirects if no workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + $response = $this->actingAs($this->user)->get(route('labels.index')); + + $response->assertRedirect(route('workspaces.create')); +}); + +// Store tests +test('store label requires authentication', function () { + $response = $this->post(route('labels.store'), [ + 'name' => 'Test Label', + 'color' => '#FF5733', + ]); + + $response->assertRedirect(route('login')); +}); + +test('store label creates label', function () { + $response = $this->actingAs($this->user)->post(route('labels.store'), [ + 'name' => 'New Label', + 'color' => '#FF5733', + ]); + + $response->assertRedirect(route('labels.index')); + + $this->assertDatabaseHas('workspace_labels', [ + 'workspace_id' => $this->workspace->id, + 'name' => 'New Label', + 'color' => '#FF5733', + ]); +}); + +test('store label validates required fields', function () { + $response = $this->actingAs($this->user)->post(route('labels.store'), [ + 'name' => '', + 'color' => '', + ]); + + $response->assertSessionHasErrors(['name', 'color']); +}); + +test('store label validates color format', function () { + $response = $this->actingAs($this->user)->post(route('labels.store'), [ + 'name' => 'Valid Name', + 'color' => 'invalid-color', + ]); + + $response->assertSessionHasErrors('color'); +}); + +// Update tests +test('update label requires authentication', function () { + $label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->put(route('labels.update', $label), [ + 'name' => 'Updated Label', + 'color' => '#00FF00', + ]); + + $response->assertRedirect(route('login')); +}); + +test('update label updates the label', function () { + $label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->put(route('labels.update', $label), [ + 'name' => 'Updated Label', + 'color' => '#00FF00', + ]); + + $response->assertRedirect(route('labels.index')); + + $label->refresh(); + expect($label->name)->toBe('Updated Label'); + expect($label->color)->toBe('#00FF00'); +}); + +test('update label returns 404 for other workspace label', function () { + $otherWorkspace = Workspace::factory()->create(); + $label = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]); + + $response = $this->actingAs($this->user)->put(route('labels.update', $label), [ + 'name' => 'Updated Label', + 'color' => '#00FF00', + ]); + + $response->assertNotFound(); +}); + +// Destroy tests +test('destroy label requires authentication', function () { + $label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->delete(route('labels.destroy', $label)); + + $response->assertRedirect(route('login')); +}); + +test('destroy label deletes the label', function () { + $label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]); + + $response = $this->actingAs($this->user)->delete(route('labels.destroy', $label)); + + $response->assertRedirect(route('labels.index')); + expect(WorkspaceLabel::find($label->id))->toBeNull(); +}); + +test('destroy label returns 404 for other workspace label', function () { + $otherWorkspace = Workspace::factory()->create(); + $label = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]); + + $response = $this->actingAs($this->user)->delete(route('labels.destroy', $label)); + + $response->assertNotFound(); +});