string('account_type')->default('apollo')->after('remember_token'); }); } // Create users $this->owner = User::factory()->create(['name' => 'Owner User']); $this->admin = User::factory()->create(['name' => 'Admin User']); $this->member = User::factory()->create(['name' => 'Member User']); $this->outsider = User::factory()->create(['name' => 'Outsider User']); // Create workspaces $this->workspace = Workspace::factory()->create([ 'name' => 'Test Workspace', 'slug' => 'test-workspace', 'domain' => 'hub.host.uk.com', 'type' => 'team', 'is_active' => true, ]); $this->secondWorkspace = Workspace::factory()->create([ 'name' => 'Second Workspace', 'slug' => 'second-workspace', 'domain' => 'hub.host.uk.com', 'type' => 'personal', 'is_active' => true, ]); // Attach users with roles $this->workspace->users()->attach($this->owner->id, [ 'role' => 'owner', 'is_default' => true, ]); $this->workspace->users()->attach($this->admin->id, [ 'role' => 'admin', 'is_default' => true, ]); $this->workspace->users()->attach($this->member->id, [ 'role' => 'member', 'is_default' => true, ]); // Give owner access to second workspace too $this->secondWorkspace->users()->attach($this->owner->id, [ 'role' => 'owner', 'is_default' => false, ]); // Give outsider their own workspace $outsiderWorkspace = Workspace::factory()->create([ 'name' => 'Outsider Workspace', 'domain' => 'hub.host.uk.com', ]); $outsiderWorkspace->users()->attach($this->outsider->id, [ 'role' => 'owner', 'is_default' => true, ]); } // ========================================================================= // INDEX - List workspaces // ========================================================================= public function test_user_can_list_their_workspaces(): void { $this->actingAs($this->owner); $workspaces = $this->owner->workspaces() ->orderBy('user_workspace.is_default', 'desc') ->orderBy('workspaces.name', 'asc') ->get(); $this->assertCount(2, $workspaces); $this->assertTrue( $workspaces->contains('id', $this->workspace->id) ); $this->assertTrue( $workspaces->contains('id', $this->secondWorkspace->id) ); } public function test_user_only_sees_own_workspaces(): void { $this->actingAs($this->member); $workspaces = $this->member->workspaces()->get(); // Member only has access to one workspace $this->assertCount(1, $workspaces); $this->assertEquals($this->workspace->id, $workspaces->first()->id); } public function test_workspace_list_can_filter_by_type(): void { $this->actingAs($this->owner); $teamWorkspaces = $this->owner->workspaces() ->where('type', 'team') ->get(); $this->assertCount(1, $teamWorkspaces); $this->assertEquals('Test Workspace', $teamWorkspaces->first()->name); $personalWorkspaces = $this->owner->workspaces() ->where('type', 'personal') ->get(); $this->assertCount(1, $personalWorkspaces); $this->assertEquals('Second Workspace', $personalWorkspaces->first()->name); } public function test_workspace_list_can_filter_by_active_status(): void { // Deactivate second workspace $this->secondWorkspace->update(['is_active' => false]); $this->actingAs($this->owner); $activeWorkspaces = $this->owner->workspaces() ->where('is_active', true) ->get(); $this->assertCount(1, $activeWorkspaces); $this->assertEquals($this->workspace->id, $activeWorkspaces->first()->id); $inactiveWorkspaces = $this->owner->workspaces() ->where('is_active', false) ->get(); $this->assertCount(1, $inactiveWorkspaces); $this->assertEquals($this->secondWorkspace->id, $inactiveWorkspaces->first()->id); } public function test_workspace_list_can_search_by_name(): void { $this->actingAs($this->owner); $results = $this->owner->workspaces() ->where('workspaces.name', 'like', '%Test%') ->get(); $this->assertCount(1, $results); $this->assertEquals('Test Workspace', $results->first()->name); } public function test_workspace_list_pagination(): void { $this->actingAs($this->owner); // Create more workspaces for the owner for ($i = 0; $i < 5; $i++) { $ws = Workspace::factory()->create(); $ws->users()->attach($this->owner->id, [ 'role' => 'member', 'is_default' => false, ]); } // Owner should now have 7 workspaces total (2 original + 5 new) $total = $this->owner->workspaces()->count(); $this->assertEquals(7, $total); // Paginate with per_page = 3 $page1 = $this->owner->workspaces() ->orderBy('workspaces.name', 'asc') ->paginate(3); $this->assertEquals(3, $page1->count()); $this->assertEquals(7, $page1->total()); $this->assertEquals(3, $page1->lastPage()); } public function test_workspace_list_per_page_capped_at_100(): void { // Verify the controller's per_page capping logic $requestedPerPage = 200; $perPage = min($requestedPerPage, 100); $this->assertEquals(100, $perPage); } public function test_workspace_list_defaults_ordered_by_default_then_name(): void { $this->actingAs($this->owner); $workspaces = $this->owner->workspaces() ->orderBy('user_workspace.is_default', 'desc') ->orderBy('workspaces.name', 'asc') ->get(); // Default workspace should come first $first = $workspaces->first(); $this->assertEquals($this->workspace->id, $first->id); $this->assertTrue((bool) $first->pivot->is_default); } // ========================================================================= // SHOW - Get a single workspace // ========================================================================= public function test_user_can_view_workspace_they_belong_to(): void { $this->actingAs($this->member); $hasAccess = $this->member->workspaces() ->where('workspaces.id', $this->workspace->id) ->exists(); $this->assertTrue($hasAccess); } public function test_user_cannot_view_workspace_they_dont_belong_to(): void { $this->actingAs($this->outsider); $hasAccess = $this->outsider->workspaces() ->where('workspaces.id', $this->workspace->id) ->exists(); $this->assertFalse($hasAccess); } public function test_workspace_show_loads_user_count(): void { $workspace = Workspace::withCount('users') ->find($this->workspace->id); // Owner, admin, and member $this->assertEquals(3, $workspace->users_count); } // ========================================================================= // STORE - Create a new workspace // ========================================================================= public function test_workspace_can_be_created_with_valid_data(): void { $workspace = Workspace::create([ 'name' => 'New Workspace', 'slug' => 'new-workspace-abc123', 'domain' => 'hub.host.uk.com', 'type' => 'custom', ]); $this->assertDatabaseHas('workspaces', [ 'id' => $workspace->id, 'name' => 'New Workspace', 'slug' => 'new-workspace-abc123', 'domain' => 'hub.host.uk.com', 'type' => 'custom', ]); } public function test_workspace_creator_is_attached_as_owner(): void { $user = $this->owner; $workspace = Workspace::create([ 'name' => 'Created Workspace', 'slug' => 'created-workspace-xyz', 'domain' => 'hub.host.uk.com', 'type' => 'custom', ]); // Attach user as owner (as the controller does) $workspace->users()->attach($user->id, [ 'role' => 'owner', 'is_default' => false, ]); $pivot = $workspace->users()->where('user_id', $user->id)->first()->pivot; $this->assertEquals('owner', $pivot->role); $this->assertFalse((bool) $pivot->is_default); } public function test_workspace_slug_must_be_unique(): void { $this->expectException(QueryException::class); Workspace::create([ 'name' => 'Duplicate Slug', 'slug' => 'test-workspace', // Same as $this->workspace 'domain' => 'hub.host.uk.com', ]); } public function test_workspace_defaults_type_when_not_provided(): void { // The controller sets default type to 'custom' when not provided $data = [ 'name' => 'No Type Workspace', 'slug' => 'no-type-ws', 'domain' => 'hub.host.uk.com', ]; $data['type'] = $data['type'] ?? 'custom'; $workspace = Workspace::create($data); $this->assertEquals('custom', $workspace->type); } public function test_workspace_store_validation_rules(): void { // Verify that the validation rules in the controller are correct $rules = [ 'name' => 'required|string|max:255', 'slug' => 'nullable|string|max:100|unique:workspaces,slug', 'icon' => 'nullable|string|max:50', 'color' => 'nullable|string|max:20', 'description' => 'nullable|string|max:500', 'type' => 'nullable|string|in:personal,team,agency,custom', ]; // Name is required $this->assertStringContainsString('required', $rules['name']); // Type must be one of the allowed values $this->assertStringContainsString('in:personal,team,agency,custom', $rules['type']); // Slug must be unique $this->assertStringContainsString('unique:workspaces,slug', $rules['slug']); } public function test_workspace_domain_defaults_to_hub(): void { // The controller always sets domain to 'hub.host.uk.com' $workspace = Workspace::create([ 'name' => 'Hub Workspace', 'slug' => 'hub-workspace-test', 'domain' => 'hub.host.uk.com', 'type' => 'custom', ]); $this->assertEquals('hub.host.uk.com', $workspace->domain); } // ========================================================================= // UPDATE - Update a workspace // ========================================================================= public function test_owner_can_update_workspace(): void { $this->actingAs($this->owner); $pivot = $this->owner->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; $this->assertNotNull($pivot); $this->assertTrue(in_array($pivot->role, ['owner', 'admin'], true)); // Perform the update $this->workspace->update([ 'name' => 'Updated Workspace Name', 'description' => 'Updated description', ]); $this->workspace->refresh(); $this->assertEquals('Updated Workspace Name', $this->workspace->name); $this->assertEquals('Updated description', $this->workspace->description); } public function test_admin_can_update_workspace(): void { $this->actingAs($this->admin); $pivot = $this->admin->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; $this->assertNotNull($pivot); $this->assertTrue(in_array($pivot->role, ['owner', 'admin'], true)); $this->workspace->update(['name' => 'Admin Updated']); $this->workspace->refresh(); $this->assertEquals('Admin Updated', $this->workspace->name); } public function test_member_cannot_update_workspace(): void { $this->actingAs($this->member); $pivot = $this->member->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; $this->assertNotNull($pivot); // Member role is not owner or admin $this->assertFalse(in_array($pivot->role, ['owner', 'admin'], true)); } public function test_outsider_cannot_update_workspace(): void { $this->actingAs($this->outsider); $pivot = $this->outsider->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; // Outsider has no pivot record for this workspace $this->assertNull($pivot); } public function test_workspace_update_can_change_active_status(): void { $this->workspace->update(['is_active' => false]); $this->workspace->refresh(); $this->assertFalse($this->workspace->is_active); $this->workspace->update(['is_active' => true]); $this->workspace->refresh(); $this->assertTrue($this->workspace->is_active); } public function test_workspace_slug_uniqueness_on_update(): void { $this->expectException(QueryException::class); // Try to change workspace slug to the second workspace's slug $this->workspace->update([ 'slug' => $this->secondWorkspace->slug, ]); } // ========================================================================= // DESTROY - Delete a workspace // ========================================================================= public function test_owner_can_delete_workspace_when_they_have_another(): void { $this->actingAs($this->owner); // Owner has 2 workspaces, so deletion should be allowed $workspaceCount = $this->owner->workspaces()->count(); $this->assertGreaterThan(1, $workspaceCount); $pivot = $this->owner->workspaces() ->where('workspaces.id', $this->secondWorkspace->id) ->first() ?->pivot; $this->assertEquals('owner', $pivot->role); $workspaceId = $this->secondWorkspace->id; $this->secondWorkspace->delete(); // Workspace model does not use SoftDeletes trait, so delete is permanent $this->assertDatabaseMissing('workspaces', ['id' => $workspaceId]); } public function test_cannot_delete_only_workspace(): void { $this->actingAs($this->member); // Member has only 1 workspace $workspaceCount = $this->member->workspaces()->count(); $this->assertEquals(1, $workspaceCount); // Controller would return 422 error in this case $this->assertTrue($workspaceCount <= 1); } public function test_only_owner_can_delete_workspace(): void { $this->actingAs($this->admin); $pivot = $this->admin->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; // Admin is not owner, so cannot delete $this->assertNotEquals('owner', $pivot->role); } public function test_member_cannot_delete_workspace(): void { $this->actingAs($this->member); $pivot = $this->member->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; $this->assertNotEquals('owner', $pivot->role); } public function test_outsider_cannot_delete_workspace(): void { $this->actingAs($this->outsider); $pivot = $this->outsider->workspaces() ->where('workspaces.id', $this->workspace->id) ->first() ?->pivot; $this->assertNull($pivot); } public function test_workspace_deletion_cascades_user_attachments(): void { $this->actingAs($this->owner); $workspaceId = $this->secondWorkspace->id; // Verify pivot records exist before deletion $this->assertDatabaseHas('user_workspace', [ 'workspace_id' => $workspaceId, 'user_id' => $this->owner->id, ]); $this->secondWorkspace->forceDelete(); // Pivot records should be removed via cascade $this->assertDatabaseMissing('user_workspace', [ 'workspace_id' => $workspaceId, ]); } // ========================================================================= // SWITCH - Switch default workspace // ========================================================================= public function test_user_can_switch_to_a_workspace_they_belong_to(): void { $this->actingAs($this->owner); // Verify owner has access to second workspace $hasAccess = $this->owner->workspaces() ->where('workspaces.id', $this->secondWorkspace->id) ->exists(); $this->assertTrue($hasAccess); // Simulate the switch transaction (same as controller) DB::transaction(function () { // Clear all existing defaults for hub workspaces DB::table('user_workspace') ->where('user_id', $this->owner->id) ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') ->where('domain', 'hub.host.uk.com'); }) ->update(['is_default' => false]); // Set the new default DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('workspace_id', $this->secondWorkspace->id) ->update(['is_default' => true]); }); // Verify the switch happened $defaultWorkspace = $this->owner->workspaces() ->wherePivot('is_default', true) ->first(); $this->assertNotNull($defaultWorkspace); $this->assertEquals($this->secondWorkspace->id, $defaultWorkspace->id); } public function test_switch_clears_all_previous_defaults(): void { $this->actingAs($this->owner); // Initially, workspace is the default $this->assertDatabaseHas('user_workspace', [ 'user_id' => $this->owner->id, 'workspace_id' => $this->workspace->id, 'is_default' => true, ]); // Perform switch to second workspace DB::transaction(function () { DB::table('user_workspace') ->where('user_id', $this->owner->id) ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') ->where('domain', 'hub.host.uk.com'); }) ->update(['is_default' => false]); DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('workspace_id', $this->secondWorkspace->id) ->update(['is_default' => true]); }); // The old default should now be false $this->assertDatabaseHas('user_workspace', [ 'user_id' => $this->owner->id, 'workspace_id' => $this->workspace->id, 'is_default' => false, ]); // The new workspace should be the default $this->assertDatabaseHas('user_workspace', [ 'user_id' => $this->owner->id, 'workspace_id' => $this->secondWorkspace->id, 'is_default' => true, ]); } public function test_user_cannot_switch_to_workspace_they_dont_belong_to(): void { $this->actingAs($this->outsider); $hasAccess = $this->outsider->workspaces() ->where('workspaces.id', $this->workspace->id) ->exists(); $this->assertFalse($hasAccess); } public function test_switch_only_affects_hub_domain_workspaces(): void { $this->actingAs($this->owner); // Create a non-hub workspace $externalWorkspace = Workspace::factory()->create([ 'domain' => 'other-domain.example.com', ]); $externalWorkspace->users()->attach($this->owner->id, [ 'role' => 'owner', 'is_default' => true, ]); // Perform switch to second hub workspace DB::transaction(function () { DB::table('user_workspace') ->where('user_id', $this->owner->id) ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') ->where('domain', 'hub.host.uk.com'); }) ->update(['is_default' => false]); DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('workspace_id', $this->secondWorkspace->id) ->update(['is_default' => true]); }); // External workspace default should NOT have changed $externalPivot = DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('workspace_id', $externalWorkspace->id) ->first(); $this->assertTrue((bool) $externalPivot->is_default); } public function test_switch_is_atomic_within_transaction(): void { $this->actingAs($this->owner); // Perform switch DB::transaction(function () { DB::table('user_workspace') ->where('user_id', $this->owner->id) ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') ->where('domain', 'hub.host.uk.com'); }) ->update(['is_default' => false]); DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('workspace_id', $this->secondWorkspace->id) ->update(['is_default' => true]); }); // After switch, only one hub workspace should be default $hubDefaults = DB::table('user_workspace') ->where('user_id', $this->owner->id) ->where('is_default', true) ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') ->where('domain', 'hub.host.uk.com'); }) ->count(); $this->assertEquals(1, $hubDefaults); } // ========================================================================= // CURRENT - Get current workspace // ========================================================================= public function test_default_host_workspace_resolves_for_authenticated_user(): void { $this->actingAs($this->owner); $default = $this->owner->defaultHostWorkspace(); $this->assertNotNull($default); $this->assertEquals($this->workspace->id, $default->id); } public function test_default_host_workspace_falls_back_to_first_workspace(): void { // Remove all defaults DB::table('user_workspace') ->where('user_id', $this->member->id) ->update(['is_default' => false]); $this->actingAs($this->member); $default = $this->member->defaultHostWorkspace(); // Should fall back to the first workspace $this->assertNotNull($default); $this->assertEquals($this->workspace->id, $default->id); } public function test_user_with_no_workspaces_returns_null(): void { $loneUser = User::factory()->create(['name' => 'Lone User']); $this->actingAs($loneUser); $default = $loneUser->defaultHostWorkspace(); $this->assertNull($default); } // ========================================================================= // ACCESS CONTROL - Role-based permission checks // ========================================================================= public function test_workspace_role_is_correctly_set_on_pivot(): void { $ownerPivot = $this->workspace->users() ->where('user_id', $this->owner->id) ->first() ->pivot; $this->assertEquals('owner', $ownerPivot->role); $adminPivot = $this->workspace->users() ->where('user_id', $this->admin->id) ->first() ->pivot; $this->assertEquals('admin', $adminPivot->role); $memberPivot = $this->workspace->users() ->where('user_id', $this->member->id) ->first() ->pivot; $this->assertEquals('member', $memberPivot->role); } public function test_workspace_owner_method_returns_correct_user(): void { $owner = $this->workspace->owner(); $this->assertNotNull($owner); $this->assertEquals($this->owner->id, $owner->id); } public function test_workspace_users_count(): void { $count = $this->workspace->users()->count(); // Owner, admin, member $this->assertEquals(3, $count); } // ========================================================================= // EDGE CASES // ========================================================================= public function test_workspace_with_special_characters_in_name(): void { $workspace = Workspace::create([ 'name' => "O'Brien's & Co. (Test)", 'slug' => 'obriens-co-test', 'domain' => 'hub.host.uk.com', 'type' => 'custom', ]); $this->assertEquals("O'Brien's & Co. (Test)", $workspace->name); } public function test_workspace_active_scope(): void { $this->secondWorkspace->update(['is_active' => false]); $activeCount = Workspace::active()->count(); // secondWorkspace is inactive now, so active count should be less than total $allCount = Workspace::count(); $this->assertLessThan($allCount, $activeCount); } public function test_workspace_is_active_cast_to_boolean(): void { $workspace = Workspace::factory()->create(['is_active' => true]); $this->assertIsBool($workspace->is_active); $this->assertTrue($workspace->is_active); $workspace->update(['is_active' => false]); $workspace->refresh(); $this->assertIsBool($workspace->is_active); $this->assertFalse($workspace->is_active); } public function test_workspace_settings_cast_to_array(): void { $workspace = Workspace::factory()->create([ 'settings' => ['theme' => 'dark', 'language' => 'en'], ]); $this->assertIsArray($workspace->settings); $this->assertEquals('dark', $workspace->settings['theme']); $this->assertEquals('en', $workspace->settings['language']); } public function test_get_setting_returns_value_or_default(): void { $workspace = Workspace::factory()->create([ 'settings' => ['theme' => 'dark'], ]); $this->assertEquals('dark', $workspace->getSetting('theme')); $this->assertNull($workspace->getSetting('nonexistent')); $this->assertEquals('fallback', $workspace->getSetting('nonexistent', 'fallback')); } public function test_workspace_to_service_array(): void { $array = $this->workspace->toServiceArray(); $this->assertArrayHasKey('name', $array); $this->assertArrayHasKey('slug', $array); $this->assertArrayHasKey('domain', $array); $this->assertArrayHasKey('icon', $array); $this->assertArrayHasKey('color', $array); $this->assertArrayHasKey('description', $array); $this->assertEquals('Test Workspace', $array['name']); $this->assertEquals('test-workspace', $array['slug']); } }