create(); $project = Project::factory()->create(); TicketType::factory()->for($project)->create([ 'name' => 'Bug', 'position' => 3, ]); TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->get(route('projects.settings.metadata', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/settings/Metadata') ->where('ticketTypes.0.name', 'Task') ->where('ticketTypes.1.name', 'Parent') ->where('ticketTypes.2.name', 'Bug') ->where('ticketLinkTypes.0.label', 'Relates to'), ); } /** * Ensure users can create custom ticket types. */ public function test_users_with_edit_permission_can_create_custom_ticket_types(): void { $user = User::factory()->create(); $project = Project::factory()->create(); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->post(route('projects.settings.metadata.types.store', $project), [ 'name' => 'Bug', ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Bug type was added.'); $this->assertDatabaseHas('ticket_types', [ 'project_id' => $project->id, 'name' => 'Bug', 'is_builtin' => false, 'system_key' => null, ]); } /** * Ensure built-in ticket types can be renamed but not deleted. */ public function test_builtin_ticket_types_can_be_renamed_but_not_deleted(): void { $user = User::factory()->create(); $project = Project::factory()->create(); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $taskType = TicketType::query() ->where('project_id', $project->id) ->where('system_key', 'task') ->first(); if ($taskType === null) { $this->actingAs($user) ->get(route('projects.settings.metadata', $project)) ->assertOk(); $taskType = TicketType::query() ->where('project_id', $project->id) ->where('system_key', 'task') ->firstOrFail(); } $this->actingAs($user) ->patch(route('projects.settings.metadata.types.update', [$project, $taskType]), [ 'name' => 'Standard Task', 'position' => 1, ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Standard Task type was updated.'); $this->assertDatabaseHas('ticket_types', [ 'id' => $taskType->id, 'name' => 'Standard Task', 'system_key' => 'task', 'is_builtin' => true, ]); $this->actingAs($user) ->delete(route('projects.settings.metadata.types.destroy', [$project, $taskType])) ->assertForbidden(); } /** * Ensure priorities can be created, updated, and deleted. */ public function test_users_with_edit_permission_can_manage_ticket_priorities(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->post(route('projects.settings.metadata.priorities.store', $project), [ 'name' => 'Low', ]) ->assertRedirect(route('projects.settings.metadata', $project)); $this->actingAs($user) ->patch(route('projects.settings.metadata.priorities.update', [$project, $priority]), [ 'name' => 'Urgent', 'position' => 2, ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Urgent priority was updated.'); $this->actingAs($user) ->delete(route('projects.settings.metadata.priorities.destroy', [$project, $priority])) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Urgent priority was removed.'); $this->assertDatabaseMissing('ticket_priorities', [ 'id' => $priority->id, ]); } /** * Ensure custom field definitions can be created and updated with supported types. */ public function test_users_with_edit_permission_can_manage_custom_field_definitions(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $fieldDefinition = TicketFieldDefinition::factory()->for($project)->create([ 'label' => 'Story Points', 'field_type' => 'number', 'position' => 1, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->post(route('projects.settings.metadata.fields.store', $project), [ 'label' => 'Reviewer', 'field_type' => 'user', ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Reviewer field was added.'); $this->actingAs($user) ->patch(route('projects.settings.metadata.fields.update', [$project, $fieldDefinition]), [ 'label' => 'Estimated Points', 'field_type' => 'number', 'position' => 2, ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Estimated Points field was updated.'); $this->assertDatabaseHas('ticket_field_definitions', [ 'id' => $fieldDefinition->id, 'label' => 'Estimated Points', 'field_type' => 'number', ]); } /** * Ensure unsupported field types are rejected. */ public function test_unsupported_field_types_are_rejected(): void { $user = User::factory()->create(); $project = Project::factory()->create(); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->from(route('projects.settings.metadata', $project)) ->post(route('projects.settings.metadata.fields.store', $project), [ 'label' => 'Reviewer', 'field_type' => 'text', ]) ->assertSessionHasErrors('field_type') ->assertRedirect(route('projects.settings.metadata', $project)); } /** * Ensure users with edit permission can manage ticket link types. */ public function test_users_with_edit_permission_can_manage_ticket_link_types(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $ticketLinkType = TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->post(route('projects.settings.metadata.link-types.store', $project), [ 'name' => 'Blocks', ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Blocks link type was added.'); $this->actingAs($user) ->patch(route('projects.settings.metadata.link-types.update', [$project, $ticketLinkType]), [ 'name' => 'Depends on', 'position' => 2, ]) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Depends on link type was updated.'); $this->actingAs($user) ->delete(route('projects.settings.metadata.link-types.destroy', [$project, $ticketLinkType])) ->assertRedirect(route('projects.settings.metadata', $project)) ->assertSessionHas('status', 'Depends on link type was removed.'); $this->assertDatabaseMissing('ticket_link_types', [ 'id' => $ticketLinkType->id, ]); } }