295 lines
11 KiB
PHP
295 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api;
|
|
|
|
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
|
use App\Models\BoardColumn;
|
|
use App\Models\Project;
|
|
use App\Models\ProjectTemplate;
|
|
use App\Models\TicketStatus;
|
|
use App\Models\TicketType;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ProjectApiTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_users_can_list_only_visible_projects(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$visibleProject = Project::factory()->create([
|
|
'name' => 'Visible project',
|
|
]);
|
|
Project::factory()->create([
|
|
'name' => 'Hidden project',
|
|
]);
|
|
|
|
$this->syncProjectPermissions($visibleProject, $user, ['view']);
|
|
|
|
$this->withApplicationKey($user)
|
|
->getJson(route('api.projects.index'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.name', 'Visible project')
|
|
->assertJsonCount(1, 'data');
|
|
}
|
|
|
|
public function test_users_with_project_create_permission_can_create_projects(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->syncGlobalPermissions($user, ['create']);
|
|
|
|
$this->withApplicationKey($user)
|
|
->postJson(route('api.projects.store'), [
|
|
'creation_mode' => 'scratch',
|
|
'key' => 'api',
|
|
'name' => 'API Project',
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('data.key', 'API')
|
|
->assertJsonPath('data.name', 'API Project')
|
|
->assertJsonPath('meta.message', 'API Project was created.');
|
|
|
|
$project = Project::query()->where('key', 'API')->firstOrFail();
|
|
$project->refresh();
|
|
|
|
$this->assertSame(2, $project->ticketTypes()->count());
|
|
$this->assertSame(1, $project->ticketStatuses()->count());
|
|
$this->assertNotNull($project->project_template_id);
|
|
}
|
|
|
|
public function test_users_can_create_projects_from_templates_via_api(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->syncGlobalPermissions($user, ['create']);
|
|
|
|
$template = ProjectTemplate::factory()->create([
|
|
'name' => 'API Template',
|
|
'slug' => 'api-template',
|
|
'copy_areas' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
],
|
|
'configuration_snapshot' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS => [
|
|
'show_author' => false,
|
|
'show_story_points' => false,
|
|
],
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES => [
|
|
[
|
|
'name' => 'Spike',
|
|
'position' => 1,
|
|
'is_parent' => false,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$this->withApplicationKey($user)
|
|
->postJson(route('api.projects.store'), [
|
|
'key' => 'tpl1',
|
|
'name' => 'Template API Project',
|
|
'creation_mode' => 'template',
|
|
'source_template_id' => $template->id,
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('data.key', 'TPL1')
|
|
->assertJsonPath('data.name', 'Template API Project');
|
|
|
|
$project = Project::query()->where('key', 'TPL1')->firstOrFail();
|
|
|
|
$project->refresh();
|
|
|
|
$this->assertSame($template->id, $project->project_template_id);
|
|
$this->assertFalse($project->show_author);
|
|
$this->assertFalse($project->show_story_points);
|
|
$this->assertSame(['Spike'], $project->ticketTypes()->pluck('name')->all());
|
|
}
|
|
|
|
public function test_project_api_exposes_template_context_and_overrides(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$project = Project::factory()->create([
|
|
'key' => 'TMP1',
|
|
'name' => 'Template Driven Project',
|
|
'show_author' => false,
|
|
'show_story_points' => false,
|
|
]);
|
|
$template = ProjectTemplate::factory()->create([
|
|
'name' => 'Workflow Template',
|
|
'slug' => 'workflow-template',
|
|
'copy_areas' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
],
|
|
'configuration_snapshot' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS => [
|
|
'show_author' => true,
|
|
'show_story_points' => true,
|
|
],
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES => [
|
|
[
|
|
'name' => 'Bug',
|
|
'position' => 1,
|
|
'is_parent' => false,
|
|
],
|
|
[
|
|
'name' => 'Feature',
|
|
'position' => 2,
|
|
'is_parent' => false,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$project->update([
|
|
'project_template_id' => $template->id,
|
|
]);
|
|
$project->ticketTypes()->delete();
|
|
TicketType::factory()->create([
|
|
'project_id' => $project->id,
|
|
'name' => 'Bug',
|
|
'position' => 1,
|
|
'is_parent' => false,
|
|
]);
|
|
TicketType::factory()->create([
|
|
'project_id' => $project->id,
|
|
'name' => 'Feature',
|
|
'position' => 2,
|
|
'is_parent' => false,
|
|
]);
|
|
|
|
$this->syncProjectPermissions($project, $user, ['view']);
|
|
|
|
$this->withApplicationKey($user)
|
|
->getJson(route('api.projects.show', $project))
|
|
->assertOk()
|
|
->assertJsonPath('data.project_template.id', $template->id)
|
|
->assertJsonPath('data.project_template.slug', 'workflow-template')
|
|
->assertJsonPath('data.project_template.configuration_snapshot.project_settings.show_author', true)
|
|
->assertJsonPath('data.template_state.inherits_from_template', true)
|
|
->assertJsonPath('data.template_state.template_id', $template->id)
|
|
->assertJsonPath('data.template_state.template_configuration.project_settings.show_author', true)
|
|
->assertJsonPath('data.template_state.effective_configuration.ticket_types.0.name', 'Bug')
|
|
->assertJsonPath('data.template_state.inherited_configuration.ticket_types.1.name', 'Feature')
|
|
->assertJsonPath('data.template_state.overridden_configuration.project_settings.show_author', false)
|
|
->assertJsonPath('data.template_state.overridden_areas.0.value', DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS);
|
|
}
|
|
|
|
public function test_users_can_duplicate_projects_via_api(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->syncGlobalPermissions($user, ['create']);
|
|
|
|
$sourceProject = Project::factory()->create([
|
|
'key' => 'SRC1',
|
|
'show_author' => false,
|
|
]);
|
|
$this->syncProjectPermissions($sourceProject, $user, ['view']);
|
|
|
|
$sourceProject->ticketTypes()->delete();
|
|
$sourceProject->ticketStatuses()->delete();
|
|
$sourceProject->boardColumns()->delete();
|
|
|
|
$column = BoardColumn::factory()->create([
|
|
'project_id' => $sourceProject->id,
|
|
'name' => 'In Progress',
|
|
'position' => 1,
|
|
]);
|
|
$backlogStatus = TicketStatus::factory()->create([
|
|
'project_id' => $sourceProject->id,
|
|
'name' => 'Queued',
|
|
'position' => 1,
|
|
]);
|
|
$activeStatus = TicketStatus::factory()->create([
|
|
'project_id' => $sourceProject->id,
|
|
'board_column_id' => $column->id,
|
|
'name' => 'Active',
|
|
'position' => 1,
|
|
]);
|
|
$backlogStatus->allowedTargetStatuses()->sync([$activeStatus->id]);
|
|
TicketType::factory()->create([
|
|
'project_id' => $sourceProject->id,
|
|
'name' => 'Spike',
|
|
'position' => 1,
|
|
]);
|
|
|
|
$this->withApplicationKey($user)
|
|
->postJson(route('api.projects.store'), [
|
|
'key' => 'dup2',
|
|
'name' => 'Duplicated API Project',
|
|
'creation_mode' => 'duplicate',
|
|
'source_project_id' => $sourceProject->id,
|
|
'copy_areas' => [
|
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
|
DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW,
|
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
|
],
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('data.key', 'DUP2')
|
|
->assertJsonPath('data.name', 'Duplicated API Project');
|
|
|
|
$project = Project::query()->where('key', 'DUP2')->firstOrFail();
|
|
|
|
$this->assertFalse($project->show_author);
|
|
$this->assertSame(['In Progress'], $project->boardColumns()->pluck('name')->all());
|
|
$this->assertSame(['Queued', 'Active'], $project->ticketStatuses()->pluck('name')->all());
|
|
$this->assertSame(['Spike'], $project->ticketTypes()->pluck('name')->all());
|
|
$this->assertSame(['Active'], $project->ticketStatuses()->where('name', 'Queued')->firstOrFail()->allowedTargetStatuses()->pluck('name')->all());
|
|
}
|
|
|
|
public function test_project_duplication_via_api_requires_visible_source_project(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->syncGlobalPermissions($user, ['create']);
|
|
$sourceProject = Project::factory()->create();
|
|
|
|
$this->withApplicationKey($user)
|
|
->postJson(route('api.projects.store'), [
|
|
'key' => 'nope',
|
|
'name' => 'No Access',
|
|
'creation_mode' => 'duplicate',
|
|
'source_project_id' => $sourceProject->id,
|
|
])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['source_project_id']);
|
|
}
|
|
|
|
public function test_project_show_and_delete_enforce_permissions(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$admin = User::factory()->create();
|
|
$project = Project::factory()->create([
|
|
'key' => 'CORE',
|
|
]);
|
|
|
|
app(InitializeProjectTicketMetadata::class)->handle($project);
|
|
|
|
$this->withApplicationKey($user)
|
|
->getJson(route('api.projects.show', $project))
|
|
->assertForbidden();
|
|
|
|
$this->syncProjectPermissions($project, $user, ['view']);
|
|
|
|
$this->withApplicationKey($user)
|
|
->getJson(route('api.projects.show', $project))
|
|
->assertOk()
|
|
->assertJsonPath('data.key', 'CORE');
|
|
|
|
$this->syncGlobalPermissions($admin, ['admin']);
|
|
|
|
$this->withApplicationKey($admin)
|
|
->deleteJson(route('api.projects.destroy', $project), [
|
|
'confirmation_key' => 'CORE',
|
|
])
|
|
->assertNoContent();
|
|
|
|
$this->assertDatabaseMissing('projects', [
|
|
'id' => $project->id,
|
|
]);
|
|
}
|
|
}
|