264 lines
12 KiB
PHP
264 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Projects;
|
|
|
|
use App\Actions\Projects\CreateProject;
|
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
|
use App\Actions\Projects\UpdateProjectTemplate;
|
|
use App\Actions\Projects\UpdateProjectTemplateLink;
|
|
use App\Models\BoardColumn;
|
|
use App\Models\Project;
|
|
use App\Models\ProjectTemplate;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketFieldDefinition;
|
|
use App\Models\TicketFieldValue;
|
|
use App\Models\TicketPriority;
|
|
use App\Models\TicketStatus;
|
|
use App\Models\TicketType;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ProjectTemplateOverrideTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_template_updates_preserve_status_ids_and_existing_tickets(): void
|
|
{
|
|
$sourceProject = Project::factory()->create();
|
|
$sourceStatus = TicketStatus::factory()->for($sourceProject)->create(['name' => 'Todo', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [DuplicateProjectConfiguration::COPY_TICKET_STATUSES]);
|
|
$project = Project::factory()->create();
|
|
$status = TicketStatus::factory()->for($project)->create(['name' => 'Todo', 'position' => 1]);
|
|
$project->forceFill(['project_template_id' => $template->id])->saveQuietly();
|
|
$ticket = $this->createTicket($project, $status);
|
|
|
|
$sourceStatus->update(['name' => 'Ready']);
|
|
app(UpdateProjectTemplate::class)->handle($template, [
|
|
'name' => $template->name,
|
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
]);
|
|
|
|
$this->assertSame('Ready', $status->fresh()->name);
|
|
$this->assertSame($status->id, $ticket->fresh()->ticket_status_id);
|
|
$this->assertDatabaseHas('tickets', ['id' => $ticket->id]);
|
|
}
|
|
|
|
public function test_projects_created_from_a_template_start_with_inherited_areas(): void
|
|
{
|
|
$sourceProject = Project::factory()->create();
|
|
$sourceStatus = TicketStatus::factory()->for($sourceProject)->create(['name' => 'Todo', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [DuplicateProjectConfiguration::COPY_TICKET_STATUSES]);
|
|
|
|
$project = app(CreateProject::class)->handle([
|
|
'name' => 'Inherited project',
|
|
'key' => 'INHR',
|
|
'creation_mode' => 'template',
|
|
'source_template_id' => $template->id,
|
|
], User::factory()->create());
|
|
|
|
$this->assertSame([], $project->template_overrides);
|
|
|
|
$sourceStatus->update(['name' => 'Ready']);
|
|
app(UpdateProjectTemplate::class)->handle($template, [
|
|
'name' => $template->name,
|
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
]);
|
|
|
|
$this->assertSame(['Ready'], $project->ticketStatuses()->pluck('name')->all());
|
|
}
|
|
|
|
public function test_local_override_remains_detached_even_when_it_temporarily_matches_the_template(): void
|
|
{
|
|
$sourceProject = Project::factory()->create();
|
|
$sourceStatus = TicketStatus::factory()->for($sourceProject)->create(['name' => 'Todo', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [DuplicateProjectConfiguration::COPY_TICKET_STATUSES]);
|
|
$project = Project::factory()->create();
|
|
$status = TicketStatus::factory()->for($project)->create(['name' => 'Todo', 'position' => 1]);
|
|
$project->forceFill(['project_template_id' => $template->id])->saveQuietly();
|
|
|
|
$status->update(['name' => 'Project only']);
|
|
$status->update(['name' => 'Todo']);
|
|
$this->assertContains(
|
|
DuplicateProjectConfiguration::COPY_TICKET_STATUSES,
|
|
$project->fresh()->template_overrides,
|
|
);
|
|
$sourceStatus->update(['name' => 'Ready']);
|
|
app(UpdateProjectTemplate::class)->handle($template, [
|
|
'name' => $template->name,
|
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
]);
|
|
|
|
$this->assertSame('Todo', $status->fresh()->name);
|
|
$this->assertContains(
|
|
DuplicateProjectConfiguration::COPY_TICKET_STATUSES,
|
|
$project->fresh()->template_overrides,
|
|
);
|
|
}
|
|
|
|
public function test_reset_requires_and_applies_a_replacement_for_used_local_statuses(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$sourceProject = Project::factory()->create();
|
|
TicketStatus::factory()->for($sourceProject)->create(['name' => 'Template status', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [DuplicateProjectConfiguration::COPY_TICKET_STATUSES]);
|
|
$project = Project::factory()->create([
|
|
'project_template_id' => $template->id,
|
|
'template_overrides' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
]);
|
|
$replacementStatus = TicketStatus::factory()->for($project)->create(['name' => 'Old template status', 'position' => 1]);
|
|
$localStatus = TicketStatus::factory()->for($project)->create(['name' => 'Local status', 'position' => 2]);
|
|
$ticket = $this->createTicket($project, $localStatus);
|
|
$this->syncProjectPermissions($project, $user, ['admin']);
|
|
|
|
$url = route('projects.settings.general.reset-template', $project);
|
|
$this->actingAs($user)->from($url)->post($url, [
|
|
'areas' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
])->assertSessionHasErrors("status_replacements.{$localStatus->id}");
|
|
|
|
$this->actingAs($user)->from($url)->post($url, [
|
|
'areas' => [DuplicateProjectConfiguration::COPY_TICKET_STATUSES],
|
|
'status_replacements' => [$localStatus->id => $replacementStatus->id],
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$this->assertDatabaseMissing('ticket_statuses', ['id' => $localStatus->id]);
|
|
$this->assertSame($replacementStatus->id, $ticket->fresh()->ticket_status_id);
|
|
$this->assertNotContains(
|
|
DuplicateProjectConfiguration::COPY_TICKET_STATUSES,
|
|
$project->fresh()->template_overrides,
|
|
);
|
|
}
|
|
|
|
public function test_reset_clears_only_the_selected_override_area(): void
|
|
{
|
|
$sourceProject = Project::factory()->create(['show_author' => true, 'show_story_points' => true]);
|
|
TicketType::factory()->for($sourceProject)->create(['name' => 'Template type', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
]);
|
|
$project = Project::factory()->create([
|
|
'project_template_id' => $template->id,
|
|
'show_author' => false,
|
|
'show_story_points' => false,
|
|
'template_overrides' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
],
|
|
]);
|
|
TicketType::factory()->for($project)->create(['name' => 'Local type', 'position' => 1]);
|
|
|
|
app(UpdateProjectTemplateLink::class)->reset(
|
|
$project->load('projectTemplate'),
|
|
[DuplicateProjectConfiguration::COPY_TICKET_TYPES],
|
|
);
|
|
|
|
$this->assertFalse($project->fresh()->show_author);
|
|
$this->assertSame(['Template type'], $project->ticketTypes()->pluck('name')->all());
|
|
$this->assertSame(
|
|
[DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS],
|
|
$project->fresh()->template_overrides,
|
|
);
|
|
}
|
|
|
|
public function test_new_template_areas_do_not_replace_existing_local_configuration(): void
|
|
{
|
|
$sourceProject = Project::factory()->create();
|
|
TicketType::factory()->for($sourceProject)->create(['name' => 'Template type', 'position' => 1]);
|
|
$template = $this->createTemplate($sourceProject, [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
]);
|
|
$project = Project::factory()->create();
|
|
TicketType::factory()->for($project)->create(['name' => 'Local type', 'position' => 1]);
|
|
$project->forceFill(['project_template_id' => $template->id])->saveQuietly();
|
|
|
|
app(UpdateProjectTemplate::class)->handle($template, [
|
|
'name' => $template->name,
|
|
'copy_areas' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
],
|
|
]);
|
|
|
|
$this->assertSame(['Local type'], $project->ticketTypes()->pluck('name')->all());
|
|
$this->assertContains(
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
$project->fresh()->template_overrides,
|
|
);
|
|
}
|
|
|
|
public function test_template_updates_preserve_custom_field_values(): void
|
|
{
|
|
$sourceProject = Project::factory()->create();
|
|
$sourceField = TicketFieldDefinition::factory()->for($sourceProject)->create([
|
|
'field_type' => 'number', 'label' => 'Estimate', 'position' => 1,
|
|
]);
|
|
$template = $this->createTemplate($sourceProject, [DuplicateProjectConfiguration::COPY_TICKET_FIELDS]);
|
|
$project = Project::factory()->create();
|
|
$field = TicketFieldDefinition::factory()->for($project)->create([
|
|
'field_type' => 'number', 'label' => 'Estimate', 'position' => 1,
|
|
]);
|
|
$project->forceFill(['project_template_id' => $template->id])->saveQuietly();
|
|
$status = TicketStatus::factory()->for($project)->create();
|
|
$ticket = $this->createTicket($project, $status);
|
|
$fieldValue = TicketFieldValue::factory()->create([
|
|
'ticket_id' => $ticket->id,
|
|
'ticket_field_definition_id' => $field->id,
|
|
'value_number' => 8,
|
|
]);
|
|
|
|
$sourceField->update(['label' => 'Story points']);
|
|
app(UpdateProjectTemplate::class)->handle($template, [
|
|
'name' => $template->name,
|
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_TICKET_FIELDS],
|
|
]);
|
|
|
|
$this->assertSame('Story points', $field->fresh()->label);
|
|
$this->assertSame($field->id, $fieldValue->fresh()->ticket_field_definition_id);
|
|
$this->assertEquals(8, $fieldValue->fresh()->value_number);
|
|
}
|
|
|
|
public function test_removing_a_board_column_never_deletes_its_statuses_or_tickets(): void
|
|
{
|
|
$project = Project::factory()->create();
|
|
$column = BoardColumn::factory()->for($project)->create();
|
|
$status = TicketStatus::factory()->for($project)->create([
|
|
'board_column_id' => $column->id,
|
|
]);
|
|
$ticket = $this->createTicket($project, $status);
|
|
|
|
$blockedAreas = app(DuplicateProjectConfiguration::class)->apply($project, [
|
|
DuplicateProjectConfiguration::COPY_BOARD_COLUMNS => [],
|
|
]);
|
|
|
|
$this->assertSame([DuplicateProjectConfiguration::COPY_BOARD_COLUMNS], $blockedAreas);
|
|
$this->assertDatabaseHas('board_columns', ['id' => $column->id]);
|
|
$this->assertDatabaseHas('ticket_statuses', ['id' => $status->id]);
|
|
$this->assertSame($status->id, $ticket->fresh()->ticket_status_id);
|
|
}
|
|
|
|
/** @param list<string> $areas */
|
|
private function createTemplate(Project $sourceProject, array $areas): ProjectTemplate
|
|
{
|
|
return ProjectTemplate::factory()->create([
|
|
'source_project_id' => $sourceProject->id,
|
|
'copy_areas' => $areas,
|
|
'configuration_snapshot' => app(DuplicateProjectConfiguration::class)->export($sourceProject, $areas),
|
|
]);
|
|
}
|
|
|
|
private function createTicket(Project $project, TicketStatus $status): Ticket
|
|
{
|
|
$type = TicketType::factory()->for($project)->create();
|
|
$priority = TicketPriority::factory()->for($project)->create();
|
|
|
|
return Ticket::factory()->create([
|
|
'project_id' => $project->id,
|
|
'ticket_status_id' => $status->id,
|
|
'ticket_type_id' => $type->id,
|
|
'ticket_priority_id' => $priority->id,
|
|
'ticket_key' => $project->key.'-1',
|
|
]);
|
|
}
|
|
}
|