feat: overhaul project workspace navigation
linter / quality (pull_request) Successful in 1m28s
tests / ci (8.4) (pull_request) Successful in 1m47s

This commit is contained in:
2026-07-18 17:52:25 +02:00
parent a46695c2eb
commit ff796aa10a
45 changed files with 1790 additions and 995 deletions
@@ -57,6 +57,7 @@ class ProjectBacklogTest extends TestCase
->component('projects/Backlog')
->where('backlogTicketStatuses.0.name', 'Backlog')
->where('canEditTickets', false)
->where('canManageProject', false)
->where('project.id', $project->id)
->where('tickets.0.ticket_key', $ticket->ticket_key)
->where('canCreateTickets', false),
@@ -37,7 +37,7 @@ class ProjectBoardControllerTest extends TestCase
'name' => 'Doing',
'position' => 2,
]);
$this->createColumnStatus($todoColumn);
$todoStatus = $this->createColumnStatus($todoColumn);
$doingStatus = $this->createColumnStatus($doingColumn);
$backlogStatus = $this->backlogStatus($project);
@@ -72,6 +72,7 @@ class ProjectBoardControllerTest extends TestCase
]);
$this->syncProjectPermissions($project, $user, ['view']);
$this->allowMove($doingStatus, $todoStatus);
$this->actingAs($user)
->get(route('projects.board.index', $project))
@@ -88,8 +89,11 @@ class ProjectBoardControllerTest extends TestCase
->where('boardColumns.1.tickets.0.assignee.id', $assignee->id)
->where('boardColumns.1.tickets.0.assignee.name', $assignee->name)
->where('boardColumns.1.tickets.0.assignee.avatar', $assignee->avatar)
->where('boardColumns.1.tickets.0.ticket_status.allowed_target_statuses.0.id', $todoStatus->id)
->where('boardColumns.1.tickets.0.ticket_status.allowed_target_statuses.0.board_column.id', $todoColumn->id)
->where('canCreateTickets', false)
->where('canEditTickets', false),
->where('canEditTickets', false)
->where('canManageProject', false),
);
}
@@ -2,8 +2,8 @@
namespace Tests\Feature\Projects;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Actions\Projects\DuplicateProjectConfiguration;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Models\BoardColumn;
use App\Models\Project;
use App\Models\ProjectTemplate;
@@ -45,16 +45,20 @@ class ProjectBoardSettingsControllerTest extends TestCase
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->get(route('projects.settings.board', $project))
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'board',
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Board')
->component('projects/Settings')
->where('activeSection', 'board')
->where('project.id', $project->id)
->where('backlogTicketStatuses.0.name', 'Backlog')
->where('boardColumns.0.name', 'Todo')
->where('ticketStatuses.0.name', 'Backlog')
->where('templateState.templateName', 'Starter Workflow')
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW),
->where('sectionData.backlogTicketStatuses.0.name', 'Backlog')
->where('sectionData.boardColumns.0.name', 'Todo')
->where('sectionData.ticketStatuses.0.name', 'Backlog')
->where('sectionData.templateState.templateName', 'Starter Workflow')
->where('sectionData.templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW),
);
}
@@ -87,7 +91,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
->post(route('projects.settings.board.columns.store', $project), [
'name' => 'In Review',
])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'In Review was added to the board workflow.');
$this->assertDatabaseHas('board_columns', [
@@ -131,7 +135,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
'name' => 'Ready for QA',
'position' => 1,
])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'Ready for QA was updated.');
$firstColumn->refresh();
@@ -168,7 +172,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.board.columns.destroy', [$project, $secondColumn]))
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'In Progress was removed from the board workflow.');
$firstColumn->refresh();
@@ -209,7 +213,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
'board_column_id' => $boardColumn->id,
'name' => 'Ready for QA',
])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'Ready for QA was added to Todo.');
$newStatus = TicketStatus::query()
@@ -225,7 +229,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
->patch(route('projects.settings.board.statuses.update', [$project, $newStatus]), [
'name' => 'QA Ready',
])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'QA Ready was updated.');
$newStatus->refresh();
@@ -240,7 +244,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.board.statuses.destroy', [$project, $newStatus]))
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'QA Ready was removed from Todo.');
$this->assertDatabaseMissing('ticket_statuses', [
@@ -280,7 +284,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
->patch(route('projects.settings.board.statuses.transitions.update', [$project, $sourceStatus]), [
'allowed_ticket_status_ids' => [$reviewStatus->id, $doneStatus->id],
])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'Progress move targets were updated.');
$this->assertEqualsCanonicalizing(
@@ -290,7 +294,7 @@ class ProjectBoardSettingsControllerTest extends TestCase
$this->actingAs($user)
->patch(route('projects.settings.board.statuses.transitions.update', [$project, $sourceStatus]), [])
->assertRedirect(route('projects.settings.board', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'board']))
->assertSessionHas('status', 'Progress move targets were updated.');
$this->assertCount(0, $sourceStatus->allowedTargetStatuses()->get());
@@ -174,7 +174,7 @@ class ProjectControllerTest extends TestCase
$project = Project::query()->firstOrFail();
$response->assertRedirect(route('projects.show', $project));
$response->assertRedirect(route('projects.board.index', $project));
$this->assertDatabaseHas('projects', [
'name' => 'Kanban Foundation',
@@ -281,7 +281,7 @@ class ProjectControllerTest extends TestCase
$project = Project::query()->where('key', 'TMP1')->firstOrFail();
$response->assertRedirect(route('projects.show', $project));
$response->assertRedirect(route('projects.board.index', $project));
$project->refresh();
@@ -412,7 +412,7 @@ class ProjectControllerTest extends TestCase
$project = Project::query()->where('key', 'DUPE')->firstOrFail();
$response->assertRedirect(route('projects.show', $project));
$response->assertRedirect(route('projects.board.index', $project));
$this->assertSame('Duplicated Project', $project->name);
$this->assertFalse($project->show_author);
@@ -469,9 +469,9 @@ class ProjectControllerTest extends TestCase
}
/**
* Ensure authenticated users can view the project hub page.
* Ensure authenticated users enter a project through its board.
*/
public function test_authenticated_users_can_view_a_project_hub_page(): void
public function test_authenticated_users_are_redirected_to_the_project_board(): void
{
$user = User::factory()->create();
$project = Project::factory()->create([
@@ -482,13 +482,7 @@ class ProjectControllerTest extends TestCase
$this->actingAs($user)
->get(route('projects.show', $project))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/Show')
->where('canManageAccess', false)
->where('project.id', $project->id)
->where('project.key', 'CORE'),
);
->assertRedirect(route('projects.board.index', $project));
}
/**
@@ -507,11 +501,7 @@ class ProjectControllerTest extends TestCase
$this->actingAs($user)
->get(route('projects.show', $project))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/Show')
->where('project.id', $project->id),
);
->assertRedirect(route('projects.board.index', $project));
}
/**
@@ -544,14 +534,10 @@ class ProjectControllerTest extends TestCase
$this->actingAs($user)
->get(route('projects.access', $project))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/Access')
->where('project.id', $project->id)
->has('users', 1)
->has('teams', 1)
->where('permissionOptions.0.value', 'project:read'),
);
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'access',
]));
}
/**
@@ -566,12 +552,10 @@ class ProjectControllerTest extends TestCase
$this->actingAs($admin)
->get(route('projects.settings.general', $project))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/General')
->where('project.id', $project->id)
->where('project.key', 'CORE'),
);
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'general',
]));
}
/**
@@ -588,11 +572,10 @@ class ProjectControllerTest extends TestCase
$this->actingAs($user)
->get(route('projects.settings.general', $project))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/General')
->where('project.id', $project->id),
);
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'general',
]));
}
public function test_project_admins_can_switch_projects_to_another_template(): void
@@ -652,7 +635,10 @@ class ProjectControllerTest extends TestCase
'confirmation_key' => 'CORE',
'source_template_id' => $newTemplate->id,
])
->assertRedirect(route('projects.settings.general', $project))
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'general',
]))
->assertSessionHas('status', 'Core Platform now uses the Operations Template template.');
$project->refresh();
@@ -707,7 +693,10 @@ class ProjectControllerTest extends TestCase
->post(route('projects.settings.general.reset-template', $project), [
'confirmation_key' => 'CORE',
])
->assertRedirect(route('projects.settings.general', $project))
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'general',
]))
->assertSessionHas('status', 'Core Platform was reset to the Workflow Template template baseline.');
$project->refresh();
@@ -878,7 +867,10 @@ class ProjectControllerTest extends TestCase
->patch(route('projects.permissions.users.update', [$project, $member]), [
'permissions' => ['project:read', 'project:write'],
])
->assertRedirect(route('projects.access', $project))
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'access',
]))
->assertSessionHas('status', 'Regular User permissions were updated.');
$this->assertDatabaseHas('permission_assignments', [
@@ -917,7 +909,10 @@ class ProjectControllerTest extends TestCase
->patch(route('projects.permissions.teams.update', [$project, $team]), [
'permissions' => ['project:read', 'project:write'],
])
->assertRedirect(route('projects.access', $project))
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => 'access',
]))
->assertSessionHas('status', 'Delivery Team permissions were updated.');
$this->assertDatabaseHas('permission_assignments', [
@@ -55,37 +55,41 @@ class ProjectNotificationSettingsControllerTest extends TestCase
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->get(route('projects.settings.notifications', $project))
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'notifications',
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Notifications')
->component('projects/Settings')
->where('activeSection', 'notifications')
->where('project.id', $project->id)
->where('notificationActions.0.name', 'Discord alerts')
->where('notificationActions.0.channel', NotificationChannel::Discord->value)
->where('notificationActions.0.channel_configuration.allowed_mentions.0', 'users')
->where('notificationActions.0.channel_configuration.allowed_mentions.1', 'roles')
->where('notificationActions.0.event_type', NotificationEventType::TicketStatusChanged->value)
->where('notificationActions.0.enabled', true)
->where('notificationActions.0.has_delivery_url', true)
->where('notificationActions.0.filters.0.attribute', 'status.to.label')
->where('notificationActions.0.filters.0.operator', 'equals')
->where('notificationActions.0.filters.0.value', 'Doing')
->where('notificationActions.0.updated_at', $notificationAction->updated_at?->toISOString())
->where('channelOptions.0.value', NotificationChannel::Discord->value)
->where('eventTypeOptions.0.value', NotificationEventType::TicketStatusChanged->value)
->where('eventTypeOptions.1.value', NotificationEventType::TicketCreate->value)
->where('eventTypeOptions.2.value', NotificationEventType::TicketAssigneeChanged->value)
->where('discordAllowedMentionOptions.0.value', 'everyone')
->where('discordAllowedMentionOptions.2.value', 'users')
->where('filterAttributeOptions.0.value', 'project.id')
->where('filterOperatorOptions.0.value', 'equals')
->where('templateVariables.0.value', '$url')
->where('templateVariables.6.value', '$actor_name')
->where('templateVariables.7.value', '$assignee_from')
->where('templateVariables.8.value', '$assignee_to')
->where('templateState.templateName', 'Alert Template')
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS)
->missing('notificationActions.0.delivery_url'),
->where('sectionData.notificationActions.0.name', 'Discord alerts')
->where('sectionData.notificationActions.0.channel', NotificationChannel::Discord->value)
->where('sectionData.notificationActions.0.channel_configuration.allowed_mentions.0', 'users')
->where('sectionData.notificationActions.0.channel_configuration.allowed_mentions.1', 'roles')
->where('sectionData.notificationActions.0.event_type', NotificationEventType::TicketStatusChanged->value)
->where('sectionData.notificationActions.0.enabled', true)
->where('sectionData.notificationActions.0.has_delivery_url', true)
->where('sectionData.notificationActions.0.filters.0.attribute', 'status.to.label')
->where('sectionData.notificationActions.0.filters.0.operator', 'equals')
->where('sectionData.notificationActions.0.filters.0.value', 'Doing')
->where('sectionData.notificationActions.0.updated_at', $notificationAction->updated_at?->toISOString())
->where('sectionData.channelOptions.0.value', NotificationChannel::Discord->value)
->where('sectionData.eventTypeOptions.0.value', NotificationEventType::TicketStatusChanged->value)
->where('sectionData.eventTypeOptions.1.value', NotificationEventType::TicketCreate->value)
->where('sectionData.eventTypeOptions.2.value', NotificationEventType::TicketAssigneeChanged->value)
->where('sectionData.discordAllowedMentionOptions.0.value', 'everyone')
->where('sectionData.discordAllowedMentionOptions.2.value', 'users')
->where('sectionData.filterAttributeOptions.0.value', 'project.id')
->where('sectionData.filterOperatorOptions.0.value', 'equals')
->where('sectionData.templateVariables.0.value', '$url')
->where('sectionData.templateVariables.6.value', '$actor_name')
->where('sectionData.templateVariables.7.value', '$assignee_from')
->where('sectionData.templateVariables.8.value', '$assignee_to')
->where('sectionData.templateState.templateName', 'Alert Template')
->where('sectionData.templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS)
->missing('sectionData.notificationActions.0.delivery_url'),
);
}
@@ -133,7 +137,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
], JSON_THROW_ON_ERROR),
'message_template' => '$ticket_key moved from $status_from to $status_to by $actor_name: $url',
])
->assertRedirect(route('projects.settings.notifications', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']))
->assertSessionHas('status', 'Discord alerts notification was added.');
$notificationAction = NotificationAction::query()->firstOrFail();
@@ -195,7 +199,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
], JSON_THROW_ON_ERROR),
'message_template' => '$ticket_key moved to $status_to: $url',
])
->assertRedirect(route('projects.settings.notifications', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']))
->assertSessionHas('status', 'Ops escalation alerts notification was updated.');
$notificationAction->refresh();
@@ -250,7 +254,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
'delivery_url' => '',
'message_template' => '',
])
->assertRedirect(route('projects.settings.notifications', $project));
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']));
$this->assertFalse($notificationAction->fresh()->enabled);
@@ -263,7 +267,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
'delivery_url' => '',
'message_template' => '',
])
->assertRedirect(route('projects.settings.notifications', $project));
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']));
$this->assertTrue($notificationAction->fresh()->enabled);
}
@@ -293,7 +297,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
'delivery_url' => 'https://hooks.example.test/kanbangit/replaced-secret',
'message_template' => '',
])
->assertRedirect(route('projects.settings.notifications', $project));
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']));
$this->assertSame(
'https://hooks.example.test/kanbangit/replaced-secret',
@@ -318,7 +322,7 @@ class ProjectNotificationSettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.notifications.destroy', [$project, $notificationAction]))
->assertRedirect(route('projects.settings.notifications', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'notifications']))
->assertSessionHas('status', 'Webhook alerts notification was deleted.');
$this->assertModelMissing($notificationAction);
@@ -422,12 +426,15 @@ class ProjectNotificationSettingsControllerTest extends TestCase
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->get(route('projects.settings.notifications', $project))
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'notifications',
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Notifications')
->where('notificationActions.0.has_delivery_url', true)
->missing('notificationActions.0.delivery_url'),
->component('projects/Settings')
->where('sectionData.notificationActions.0.has_delivery_url', true)
->missing('sectionData.notificationActions.0.delivery_url'),
);
}
}
@@ -50,16 +50,20 @@ class ProjectRepositorySettingsControllerTest extends TestCase
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->get(route('projects.settings.repositories', $project))
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'repositories',
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Repositories')
->component('projects/Settings')
->where('activeSection', 'repositories')
->where('project.id', $project->id)
->where('repositoryConnections.0.repository_full_name', 'core-team/kanbangit')
->where('repositoryConnections.0.has_personal_access', false)
->where('availableRepositorySources', [])
->where('templateState.templateName', 'Connected Template')
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS),
->where('sectionData.repositoryConnections.0.repository_full_name', 'core-team/kanbangit')
->where('sectionData.repositoryConnections.0.has_personal_access', false)
->where('sectionData.availableRepositorySources', [])
->where('sectionData.templateState.templateName', 'Connected Template')
->where('sectionData.templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS),
);
}
@@ -111,7 +115,7 @@ class ProjectRepositorySettingsControllerTest extends TestCase
'provider_repository_id' => '501',
'user_git_connection_id' => $userGitConnection->id,
])
->assertRedirect(route('projects.settings.repositories', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'repositories']))
->assertSessionHas('status', 'core-team/kanbangit was linked to '.$project->name.'.');
$this->assertDatabaseHas('repository_connections', [
@@ -192,7 +196,7 @@ class ProjectRepositorySettingsControllerTest extends TestCase
'provider_repository_id' => '902',
'user_git_connection_id' => $userGitConnection->id,
])
->assertRedirect(route('projects.settings.repositories', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'repositories']))
->assertSessionHas('status', 'infra/platform connection was updated.');
$this->assertDatabaseHas('repository_connections', [
@@ -220,7 +224,7 @@ class ProjectRepositorySettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.repositories.destroy', [$project, $repositoryConnection]))
->assertRedirect(route('projects.settings.repositories', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'repositories']))
->assertSessionHas('status', 'core-team/kanbangit was removed from the project.');
$this->assertDatabaseMissing('repository_connections', [
@@ -0,0 +1,148 @@
<?php
namespace Tests\Feature\Projects;
use App\Actions\Git\GitProviderKey;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Models\BoardColumn;
use App\Models\Project;
use App\Models\Team;
use App\Models\TicketStatus;
use App\Models\User;
use App\Models\UserGitConnection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class ProjectSettingsControllerTest extends TestCase
{
use RefreshDatabase;
public function test_project_admins_can_open_each_settings_section(): void
{
$user = User::factory()->create();
$project = Project::factory()->create(['key' => 'CORE']);
$team = Team::factory()->create();
app(InitializeProjectTicketMetadata::class)->handle($project);
$boardColumn = BoardColumn::factory()->for($project)->create([
'name' => 'In progress',
]);
TicketStatus::factory()->for($project)->for($boardColumn)->create([
'name' => 'In progress',
]);
$this->syncProjectPermissions($project, $user, ['admin']);
$expectedSectionProps = [
'general' => 'sourceTemplates',
'board' => 'boardColumns',
'metadata' => 'ticketTypes',
'repositories' => 'repositoryConnections',
'notifications' => 'notificationActions',
'access' => 'teams',
];
foreach ($expectedSectionProps as $section => $expectedProp) {
$this->actingAs($user)
->get(route('projects.settings.index', [
'project' => $project,
'section' => $section,
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/Settings')
->where('activeSection', $section)
->where('project.id', $project->id)
->has("sectionData.{$expectedProp}"),
);
}
}
public function test_missing_and_invalid_sections_fall_back_to_general(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
$this->syncProjectPermissions($project, $user, ['admin']);
foreach ([null, 'unknown'] as $section) {
$parameters = ['project' => $project];
if ($section !== null) {
$parameters['section'] = $section;
}
$this->actingAs($user)
->get(route('projects.settings.index', $parameters))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/Settings')
->where('activeSection', 'general')
->has('sectionData.sourceTemplates'),
);
}
}
public function test_users_without_project_admin_permission_cannot_open_settings(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
$this->syncProjectPermissions($project, $user, ['view']);
$this->actingAs($user)
->get(route('projects.settings.index', $project))
->assertForbidden();
}
public function test_repository_providers_are_not_queried_outside_the_repository_section(): void
{
Http::preventStrayRequests();
$user = User::factory()->create();
$project = Project::factory()->create();
UserGitConnection::factory()->for($user)->create([
'base_url' => 'https://git.example.test',
'provider' => GitProviderKey::Gitea,
]);
$this->syncProjectPermissions($project, $user, ['admin']);
$this->actingAs($user)
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'general',
]))
->assertOk();
Http::assertNothingSent();
}
public function test_legacy_settings_urls_redirect_to_the_matching_section(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
$this->syncProjectPermissions($project, $user, ['admin']);
$legacyRoutes = [
'projects.settings.general' => 'general',
'projects.settings.board' => 'board',
'projects.settings.metadata' => 'metadata',
'projects.settings.repositories' => 'repositories',
'projects.settings.notifications' => 'notifications',
'projects.access' => 'access',
];
foreach ($legacyRoutes as $legacyRoute => $section) {
$this->actingAs($user)
->get(route($legacyRoute, $project))
->assertRedirect(route('projects.settings.index', [
'project' => $project,
'section' => $section,
]));
}
}
}
@@ -403,6 +403,7 @@ class ProjectTicketControllerTest extends TestCase
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/TicketCreate')
->where('canManageProject', false)
->where('defaultTicketStatusId', $boardStatus->id)
->where('parentTicketOptions.0.ticket_key', $parentTicket->ticket_key)
->where('project.id', $project->id)
@@ -2,8 +2,8 @@
namespace Tests\Feature\Projects;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Actions\Projects\DuplicateProjectConfiguration;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Models\Project;
use App\Models\ProjectTemplate;
use App\Models\Ticket;
@@ -60,19 +60,23 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->get(route('projects.settings.metadata', $project))
->get(route('projects.settings.index', [
'project' => $project,
'section' => 'metadata',
]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Metadata')
->component('projects/Settings')
->where('activeSection', 'metadata')
->where('project.show_author', true)
->where('project.show_story_points', false)
->where('ticketTypes.0.name', 'Task')
->where('ticketTypes.1.name', 'Release')
->where('ticketTypes.1.is_parent', true)
->where('ticketTypes.2.name', 'Bug')
->where('ticketLinkTypes.0.label', 'Relates to')
->where('templateState.templateName', 'Issue Tracker Template')
->where('templateState.overriddenAreas.0.value', DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS),
->where('sectionData.ticketTypes.0.name', 'Task')
->where('sectionData.ticketTypes.1.name', 'Release')
->where('sectionData.ticketTypes.1.is_parent', true)
->where('sectionData.ticketTypes.2.name', 'Bug')
->where('sectionData.ticketLinkTypes.0.label', 'Relates to')
->where('sectionData.templateState.templateName', 'Issue Tracker Template')
->where('sectionData.templateState.overriddenAreas.0.value', DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS),
);
}
@@ -91,7 +95,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'is_parent' => '1',
'name' => 'Bug',
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Bug type was added.');
$this->assertDatabaseHas('ticket_types', [
@@ -124,7 +128,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'name' => 'Standard Release',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Standard Release type was updated.');
$this->assertDatabaseHas('ticket_types', [
@@ -152,7 +156,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'show_author' => false,
'show_story_points' => false,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Settings have been updated.');
$this->assertDatabaseHas('projects', [
@@ -196,7 +200,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.metadata.types.destroy', [$project, $ticketType]))
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'The last ticket type cannot be deleted.');
$this->assertDatabaseHas('ticket_types', [
@@ -248,7 +252,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
->delete(route('projects.settings.metadata.types.destroy', [$project, $taskType]), [
'replacement_ticket_type_id' => $releaseType->id,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Task type was removed.');
$this->assertDatabaseMissing('ticket_types', [
@@ -327,7 +331,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
->delete(route('projects.settings.metadata.types.destroy', [$project, $releaseType]), [
'replacement_ticket_type_id' => $epicType->id,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Release type was removed.');
$this->assertDatabaseHas('tickets', [
@@ -354,19 +358,19 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
->post(route('projects.settings.metadata.priorities.store', $project), [
'name' => 'Low',
])
->assertRedirect(route('projects.settings.metadata', $project));
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']));
$this->actingAs($user)
->patch(route('projects.settings.metadata.priorities.update', [$project, $priority]), [
'name' => 'Urgent',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Urgent priority was updated.');
$this->actingAs($user)
->delete(route('projects.settings.metadata.priorities.destroy', [$project, $priority]))
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Urgent priority was removed.');
$this->assertDatabaseMissing('ticket_priorities', [
@@ -394,7 +398,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'label' => 'Reviewer',
'field_type' => 'user',
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Reviewer field was added.');
$this->actingAs($user)
@@ -403,7 +407,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'field_type' => 'number',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Risk score field was updated.');
$this->assertDatabaseHas('ticket_field_definitions', [
@@ -453,7 +457,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'name' => 'Blocks',
'counterpart_name' => 'Blocked by',
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Blocks link type was added.');
$this->assertDatabaseHas('ticket_link_types', [
@@ -468,7 +472,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
'counterpart_name' => 'Required by',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Depends on link type was updated.');
$this->assertDatabaseHas('ticket_link_types', [
@@ -479,7 +483,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->actingAs($user)
->delete(route('projects.settings.metadata.link-types.destroy', [$project, $ticketLinkType]))
->assertRedirect(route('projects.settings.metadata', $project))
->assertRedirect(route('projects.settings.index', ['project' => $project, 'section' => 'metadata']))
->assertSessionHas('status', 'Depends on link type was removed.');
$this->assertDatabaseMissing('ticket_link_types', [