mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-07 21:33:18 +01:00
1375 lines
54 KiB
PHP
1375 lines
54 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Endpoint\Api\V1;
|
|
|
|
use App\Enums\GoalComparison;
|
|
use App\Enums\GoalPeriod;
|
|
use App\Enums\GoalStatus;
|
|
use App\Enums\GoalType;
|
|
use App\Enums\Role;
|
|
use App\Enums\TagMatchType;
|
|
use App\Enums\TimeEntryType;
|
|
use App\Enums\Weekday;
|
|
use App\Http\Controllers\Api\V1\GoalController;
|
|
use App\Models\Client;
|
|
use App\Models\Goal;
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Models\Project;
|
|
use App\Models\Tag;
|
|
use App\Models\Task;
|
|
use App\Models\TimeEntry;
|
|
use App\Service\BillingContract;
|
|
use App\Service\Dto\GoalFiltersDto;
|
|
use App\Service\GoalsContract;
|
|
use App\Service\TimeEntryFilter;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Testing\Fluent\AssertableJson;
|
|
use Laravel\Passport\Passport;
|
|
use PHPUnit\Framework\Attributes\UsesClass;
|
|
|
|
#[UsesClass(GoalController::class)]
|
|
class GoalEndpointTest extends ApiEndpointTestAbstract
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// Core rules only, independent of whether the Goals extension is enabled locally
|
|
$this->app->bind(GoalsContract::class);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function minimalGoalBody(): array
|
|
{
|
|
return [
|
|
'name' => 'Deep work',
|
|
'type' => GoalType::Personal->value,
|
|
'comparison' => GoalComparison::AtLeast->value,
|
|
'target_seconds' => 10 * 3600,
|
|
'period' => GoalPeriod::Week->value,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Organizations without a paid plan can only have a limited number of goals.
|
|
*/
|
|
private function limitGoalsTo(?int $limit): void
|
|
{
|
|
$this->app->bind(BillingContract::class, fn (): BillingContract => new class($limit) extends BillingContract
|
|
{
|
|
public function __construct(private readonly ?int $limit) {}
|
|
|
|
public function getGoalLimit(Organization $organization): ?int
|
|
{
|
|
return $this->limit;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function test_index_endpoint_fails_if_user_does_not_have_permission_to_view_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission();
|
|
Goal::factory()->forMember($data->member)->createMany(2);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_index_endpoint_succeeds_with_only_the_organization_type_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:organization-type']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
}
|
|
|
|
public function test_show_endpoint_fails_for_an_organization_goal_with_only_the_own_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$goal = Goal::factory()->organizationGoalForMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_show_endpoint_fails_for_a_personal_goal_of_another_member_with_only_the_organization_type_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:organization-type']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$goal = Goal::factory()->forMember($otherMember)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_index_endpoint_returns_only_own_goals_of_organization_ordered_by_created_at_desc(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$otherOrganization = Organization::factory()->create();
|
|
$ownGoals = Goal::factory()->forMember($data->member)->randomCreatedAt()->createMany(3);
|
|
Goal::factory()->forMember($otherMember)->createMany(2);
|
|
Goal::factory()->forOrganization($otherOrganization)->createMany(2);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$expected = Goal::query()->whereKey($ownGoals->pluck('id'))->orderBy('created_at', 'desc')->orderBy('id')->get();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->has('data')
|
|
->has('links')
|
|
->has('meta')
|
|
->count('data', 3)
|
|
->where('data.0.id', $expected->get(0)->getKey())
|
|
->where('data.1.id', $expected->get(1)->getKey())
|
|
->where('data.2.id', $expected->get(2)->getKey())
|
|
);
|
|
}
|
|
|
|
public function test_index_endpoint_returns_goal_with_filters_and_current_progress(): void
|
|
{
|
|
// Arrange
|
|
$this->travelTo(Carbon::create(2024, 1, 3, 12, 0, 0, 'UTC'));
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$project = Project::factory()->forOrganization($data->organization)->create();
|
|
$filters = new GoalFiltersDto;
|
|
$filters->setProjectIds([$project->getKey()]);
|
|
$filters->timeEntryType = TimeEntryType::Work;
|
|
$goal = Goal::factory()->forMember($data->member)->period(GoalPeriod::Day)->atLeast(3600)->filters($filters)->create();
|
|
TimeEntry::factory()->forMember($data->member)->forProject($project)
|
|
->startWithDuration(Carbon::now()->subHours(3), 1200)->create();
|
|
TimeEntry::factory()->forMember($data->member)
|
|
->startWithDuration(Carbon::now()->subHours(3), 1200)->create();
|
|
TimeEntry::factory()->forMember($data->member)->forProject($project)
|
|
->start(Carbon::now()->subMinutes(10))->active()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->count('data', 1)
|
|
->where('data.0.id', $goal->getKey())
|
|
->where('data.0.name', $goal->name)
|
|
->where('data.0.type', GoalType::Personal->value)
|
|
->where('data.0.comparison', GoalComparison::AtLeast->value)
|
|
->where('data.0.target_seconds', 3600)
|
|
->where('data.0.period', GoalPeriod::Day->value)
|
|
->where('data.0.member_id', $data->member->getKey())
|
|
->where('data.0.timezone', 'UTC')
|
|
->where('data.0.week_start', Weekday::Monday->value)
|
|
->where('data.0.is_archived', false)
|
|
->where('data.0.filters.member_ids', null)
|
|
->where('data.0.filters.project_ids', [$project->getKey()])
|
|
->where('data.0.filters.task_ids', null)
|
|
->where('data.0.filters.tag_ids', null)
|
|
->where('data.0.filters.tag_match_type', null)
|
|
->where('data.0.filters.client_ids', null)
|
|
->where('data.0.filters.billable', null)
|
|
->where('data.0.filters.time_entry_type', TimeEntryType::Work->value)
|
|
->where('data.0.progress.period_start', '2024-01-03T00:00:00Z')
|
|
->where('data.0.progress.period_end', '2024-01-04T00:00:00Z')
|
|
->where('data.0.progress.tracked_seconds', 1200 + 600)
|
|
->where('data.0.progress.status', GoalStatus::InProgress->value)
|
|
->etc()
|
|
);
|
|
}
|
|
|
|
public function test_index_endpoint_returns_the_progress_of_each_goal(): void
|
|
{
|
|
// Arrange
|
|
$this->travelTo(Carbon::create(2024, 1, 3, 12, 0, 0, 'UTC'));
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$project = Project::factory()->forOrganization($data->organization)->create();
|
|
$filters = new GoalFiltersDto;
|
|
$filters->setProjectIds([$project->getKey()]);
|
|
$dayGoal = Goal::factory()->forMember($data->member)->period(GoalPeriod::Day)->atLeast(3600)->filters($filters)->create([
|
|
'created_at' => Carbon::now()->subDay(),
|
|
]);
|
|
$weekGoal = Goal::factory()->forMember($data->member)->period(GoalPeriod::Week)->atLeast(3600)->create([
|
|
'created_at' => Carbon::now()->subDays(2),
|
|
]);
|
|
TimeEntry::factory()->forMember($data->member)->forProject($project)
|
|
->startWithDuration(Carbon::now()->subHours(3), 1200)->create();
|
|
TimeEntry::factory()->forMember($data->member)
|
|
->startWithDuration(Carbon::now()->subDay(), 1800)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->count('data', 2)
|
|
->where('data.0.id', $dayGoal->getKey())
|
|
->where('data.0.progress.period_start', '2024-01-03T00:00:00Z')
|
|
->where('data.0.progress.period_end', '2024-01-04T00:00:00Z')
|
|
->where('data.0.progress.tracked_seconds', 1200)
|
|
->where('data.1.id', $weekGoal->getKey())
|
|
->where('data.1.progress.period_start', '2024-01-01T00:00:00Z')
|
|
->where('data.1.progress.period_end', '2024-01-08T00:00:00Z')
|
|
->where('data.1.progress.tracked_seconds', 1200 + 1800)
|
|
->etc()
|
|
);
|
|
}
|
|
|
|
public function test_index_endpoint_does_not_show_organization_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$ownGoal = Goal::factory()->forMember($data->member)->create();
|
|
Goal::factory()->organizationGoalForMember($data->member)->create();
|
|
Goal::factory()->forOrganization($data->organization)->forEveryMember()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJsonCount(1, 'data');
|
|
$response->assertJsonPath('data.0.id', $ownGoal->getKey());
|
|
$response->assertJsonPath('data.0.type', GoalType::Personal->value);
|
|
}
|
|
|
|
public function test_index_endpoint_can_filter_by_type(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$personalResponse = $this->getJson(route('api.v1.goals.index', [
|
|
'organization' => $data->organization->getKey(),
|
|
'type' => GoalType::Personal->value,
|
|
]));
|
|
$organizationResponse = $this->getJson(route('api.v1.goals.index', [
|
|
'organization' => $data->organization->getKey(),
|
|
'type' => GoalType::Organization->value,
|
|
]));
|
|
|
|
// Assert
|
|
$personalResponse->assertOk();
|
|
$personalResponse->assertJsonCount(1, 'data');
|
|
$personalResponse->assertJsonPath('data.0.id', $goal->getKey());
|
|
$organizationResponse->assertOk();
|
|
$organizationResponse->assertJsonCount(0, 'data');
|
|
}
|
|
|
|
public function test_index_endpoint_fails_with_validation_errors_for_invalid_filters(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.index', [
|
|
'organization' => $data->organization->getKey(),
|
|
'type' => 'team',
|
|
'archived' => 'maybe',
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['type', 'archived']);
|
|
}
|
|
|
|
public function test_index_endpoint_hides_archived_goals_per_default_and_can_filter_by_archived(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
$archivedGoal = Goal::factory()->forMember($data->member)->archived()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$defaultResponse = $this->getJson(route('api.v1.goals.index', [$data->organization->getKey()]));
|
|
$archivedResponse = $this->getJson(route('api.v1.goals.index', [
|
|
'organization' => $data->organization->getKey(),
|
|
'archived' => 'true',
|
|
]));
|
|
$allResponse = $this->getJson(route('api.v1.goals.index', [
|
|
'organization' => $data->organization->getKey(),
|
|
'archived' => 'all',
|
|
]));
|
|
|
|
// Assert
|
|
$defaultResponse->assertOk();
|
|
$defaultResponse->assertJsonCount(1, 'data');
|
|
$defaultResponse->assertJsonPath('data.0.id', $goal->getKey());
|
|
$archivedResponse->assertOk();
|
|
$archivedResponse->assertJsonCount(1, 'data');
|
|
$archivedResponse->assertJsonPath('data.0.id', $archivedGoal->getKey());
|
|
$archivedResponse->assertJsonPath('data.0.is_archived', true);
|
|
$allResponse->assertOk();
|
|
$allResponse->assertJsonCount(2, 'data');
|
|
}
|
|
|
|
public function test_show_endpoint_fails_if_user_does_not_have_permission_to_view_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission();
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_show_endpoint_fails_if_goal_belongs_to_other_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$goal = Goal::factory()->forMember($otherMember)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_show_endpoint_fails_if_goal_belongs_to_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$goal = Goal::factory()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_show_endpoint_returns_own_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:view:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->getJson(route('api.v1.goals.show', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.id', $goal->getKey())
|
|
->where('data.name', $goal->name)
|
|
->has('data.progress.tracked_seconds')
|
|
->has('data.progress.status')
|
|
->etc()
|
|
);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_if_user_does_not_have_permission_to_create_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_creates_personal_goal_with_minimal_body_for_the_current_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$data->user->timezone = 'Europe/Vienna';
|
|
$data->user->week_start = Weekday::Sunday;
|
|
$data->user->save();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.name', 'Deep work')
|
|
->where('data.type', GoalType::Personal->value)
|
|
->where('data.comparison', GoalComparison::AtLeast->value)
|
|
->where('data.target_seconds', 36000)
|
|
->where('data.period', GoalPeriod::Week->value)
|
|
->where('data.member_id', $data->member->getKey())
|
|
->where('data.is_archived', false)
|
|
// The timezone and the week start of the current user are pinned on the goal
|
|
->where('data.timezone', 'Europe/Vienna')
|
|
->where('data.week_start', Weekday::Sunday->value)
|
|
->where('data.filters.project_ids', null)
|
|
->where('data.filters.time_entry_type', null)
|
|
->where('data.progress.tracked_seconds', 0)
|
|
->where('data.progress.status', GoalStatus::InProgress->value)
|
|
->etc()
|
|
);
|
|
$this->assertDatabaseHas(Goal::class, [
|
|
'id' => $response->json('data.id'),
|
|
'name' => 'Deep work',
|
|
'type' => GoalType::Personal->value,
|
|
'organization_id' => $data->organization->getKey(),
|
|
'member_id' => $data->member->getKey(),
|
|
'archived_at' => null,
|
|
]);
|
|
}
|
|
|
|
public function test_store_endpoint_creates_goal_with_an_explicit_timezone_and_week_start(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'timezone' => 'America/New_York',
|
|
'week_start' => Weekday::Saturday->value,
|
|
]));
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$response->assertJsonPath('data.timezone', 'America/New_York');
|
|
$response->assertJsonPath('data.week_start', Weekday::Saturday->value);
|
|
}
|
|
|
|
public function test_store_endpoint_creates_goal_with_all_filters(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$client = Client::factory()->forOrganization($data->organization)->create();
|
|
$project = Project::factory()->forOrganization($data->organization)->forClient($client)->create();
|
|
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), [
|
|
'name' => 'Less meetings',
|
|
'type' => GoalType::Personal->value,
|
|
'comparison' => GoalComparison::LessThan->value,
|
|
'target_seconds' => 5 * 3600,
|
|
'period' => GoalPeriod::Month->value,
|
|
'member_id' => $data->member->getKey(),
|
|
'filters' => [
|
|
'project_ids' => [$project->getKey(), TimeEntryFilter::NONE_VALUE],
|
|
'task_ids' => [$task->getKey()],
|
|
'tag_ids' => [$tag->getKey()],
|
|
'tag_match_type' => TagMatchType::NotContains->value,
|
|
'client_ids' => [$client->getKey()],
|
|
'billable' => false,
|
|
'time_entry_type' => TimeEntryType::Work->value,
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.comparison', GoalComparison::LessThan->value)
|
|
->where('data.period', GoalPeriod::Month->value)
|
|
->where('data.filters.project_ids', [$project->getKey(), TimeEntryFilter::NONE_VALUE])
|
|
->where('data.filters.task_ids', [$task->getKey()])
|
|
->where('data.filters.tag_ids', [$tag->getKey()])
|
|
->where('data.filters.tag_match_type', TagMatchType::NotContains->value)
|
|
->where('data.filters.client_ids', [$client->getKey()])
|
|
->where('data.filters.billable', false)
|
|
->where('data.filters.time_entry_type', TimeEntryType::Work->value)
|
|
->where('data.progress.status', GoalStatus::OnTrack->value)
|
|
->etc()
|
|
);
|
|
/** @var Goal $goal */
|
|
$goal = Goal::query()->findOrFail($response->json('data.id'));
|
|
$this->assertSame([$project->getKey(), TimeEntryFilter::NONE_VALUE], $goal->filters->projectIds?->toArray());
|
|
$this->assertSame(TagMatchType::NotContains, $goal->filters->tagMatchType);
|
|
$this->assertFalse($goal->filters->billable);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_errors_for_invalid_body(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), [
|
|
'name' => '',
|
|
'type' => 'team',
|
|
'comparison' => 'more_than',
|
|
'target_seconds' => 0,
|
|
'period' => 'year',
|
|
'timezone' => 'Mars/Phobos',
|
|
'week_start' => 'caturday',
|
|
'filters' => [
|
|
'project_ids' => ['not-a-uuid'],
|
|
'tag_match_type' => 'maybe',
|
|
'time_entry_type' => 'lunch',
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors([
|
|
'name',
|
|
'type',
|
|
'comparison',
|
|
'target_seconds',
|
|
'period',
|
|
'timezone',
|
|
'week_start',
|
|
'filters.project_ids.0',
|
|
'filters.tag_match_type',
|
|
'filters.time_entry_type',
|
|
]);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_if_filter_ids_belong_to_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$otherOrganization = Organization::factory()->create();
|
|
$project = Project::factory()->forOrganization($otherOrganization)->create();
|
|
$tag = Tag::factory()->forOrganization($otherOrganization)->create();
|
|
$client = Client::factory()->forOrganization($otherOrganization)->create();
|
|
$task = Task::factory()->forOrganization($otherOrganization)->forProject($project)->create();
|
|
$memberOfOtherOrganization = Member::factory()->forOrganization($otherOrganization)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'filters' => [
|
|
'member_ids' => [$memberOfOtherOrganization->getKey()],
|
|
'project_ids' => [$project->getKey()],
|
|
'task_ids' => [$task->getKey()],
|
|
'tag_ids' => [$tag->getKey()],
|
|
'client_ids' => [$client->getKey()],
|
|
],
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors([
|
|
'filters.member_ids.0',
|
|
'filters.project_ids.0',
|
|
'filters.task_ids.0',
|
|
'filters.tag_ids.0',
|
|
'filters.client_ids.0',
|
|
]);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_for_a_personal_goal_for_other_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'member_id' => $otherMember->getKey(),
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_for_organization_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$responseForEveryMember = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => null,
|
|
]));
|
|
$responseForSelf = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => $data->member->getKey(),
|
|
]));
|
|
|
|
// Assert
|
|
$responseForEveryMember->assertForbidden();
|
|
$responseForSelf->assertForbidden();
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_for_organization_goals_with_the_organization_type_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:organization-type']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$responseForEveryMember = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => null,
|
|
]));
|
|
$responseForOtherMember = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => $otherMember->getKey(),
|
|
]));
|
|
|
|
// Assert
|
|
$responseForEveryMember->assertForbidden();
|
|
$responseForOtherMember->assertForbidden();
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_for_a_personal_goal_with_a_member_ids_filter(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'filters' => [
|
|
'member_ids' => [$data->member->getKey()],
|
|
],
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['filters.member_ids']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_stores_empty_id_filters_as_null(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'filters' => [
|
|
'member_ids' => [],
|
|
'project_ids' => [],
|
|
'task_ids' => [],
|
|
'tag_ids' => [],
|
|
'client_ids' => [],
|
|
],
|
|
]));
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.filters.member_ids', null)
|
|
->where('data.filters.project_ids', null)
|
|
->where('data.filters.task_ids', null)
|
|
->where('data.filters.tag_ids', null)
|
|
->where('data.filters.client_ids', null)
|
|
->etc()
|
|
);
|
|
/** @var Goal $goal */
|
|
$goal = Goal::query()->findOrFail($response->json('data.id'));
|
|
$this->assertNull($goal->filters->memberIds);
|
|
$this->assertNull($goal->filters->projectIds);
|
|
$this->assertNull($goal->filters->taskIds);
|
|
$this->assertNull($goal->filters->tagIds);
|
|
$this->assertNull($goal->filters->clientIds);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_for_a_personal_goal_without_a_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'member_id' => null,
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_for_invalid_member_id(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'member_id' => 'not-a-uuid',
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_if_member_belongs_to_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$memberOfOtherOrganization = Member::factory()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'member_id' => $memberOfOtherOrganization->getKey(),
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_if_member_is_a_placeholder(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:organization-type']);
|
|
$placeholder = Member::factory()->forOrganization($data->organization)->role(Role::Placeholder)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => $placeholder->getKey(),
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_with_validation_error_if_member_ids_filter_contains_a_placeholder(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:organization-type']);
|
|
$member = Member::factory()->forOrganization($data->organization)->create();
|
|
$placeholder = Member::factory()->forOrganization($data->organization)->role(Role::Placeholder)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), array_merge($this->minimalGoalBody(), [
|
|
'type' => GoalType::Organization->value,
|
|
'member_id' => null,
|
|
'filters' => [
|
|
'member_ids' => [$member->getKey(), $placeholder->getKey()],
|
|
],
|
|
]));
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['filters.member_ids.1']);
|
|
$response->assertJsonMissingValidationErrors(['filters.member_ids', 'filters.member_ids.0']);
|
|
$this->assertDatabaseCount(Goal::class, 0);
|
|
}
|
|
|
|
public function test_store_endpoint_fails_if_the_organization_reached_the_goal_limit_of_its_plan(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$this->limitGoalsTo(1);
|
|
Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$response->assertStatus(400);
|
|
$response->assertExactJson([
|
|
'error' => true,
|
|
'key' => 'feature_is_not_available_in_free_plan',
|
|
'message' => 'Feature is not available in free plan',
|
|
]);
|
|
$this->assertDatabaseCount(Goal::class, 1);
|
|
}
|
|
|
|
public function test_store_endpoint_counts_archived_goals_towards_the_goal_limit(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$this->limitGoalsTo(1);
|
|
Goal::factory()->forMember($data->member)->archived()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$response->assertStatus(400);
|
|
$response->assertExactJson([
|
|
'error' => true,
|
|
'key' => 'feature_is_not_available_in_free_plan',
|
|
'message' => 'Feature is not available in free plan',
|
|
]);
|
|
$this->assertDatabaseCount(Goal::class, 1);
|
|
}
|
|
|
|
public function test_store_endpoint_creates_goal_if_the_organization_is_below_the_goal_limit(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$this->limitGoalsTo(2);
|
|
Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$this->assertDatabaseCount(Goal::class, 2);
|
|
}
|
|
|
|
public function test_store_endpoint_counts_the_goals_of_other_members_towards_the_goal_limit(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$this->limitGoalsTo(1);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
Goal::factory()->forMember($otherMember)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$response->assertStatus(400);
|
|
$response->assertJsonPath('key', 'feature_is_not_available_in_free_plan');
|
|
$this->assertDatabaseCount(Goal::class, 1);
|
|
}
|
|
|
|
public function test_store_endpoint_does_not_count_the_goals_of_other_organizations_towards_the_goal_limit(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:create:own']);
|
|
$this->limitGoalsTo(1);
|
|
Goal::factory()->forOrganization(Organization::factory()->create())->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->postJson(route('api.v1.goals.store', [$data->organization->getKey()]), $this->minimalGoalBody());
|
|
|
|
// Assert
|
|
$this->assertResponseCode($response, 201);
|
|
$this->assertDatabaseCount(Goal::class, 2);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_if_user_does_not_have_permission_to_update_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission();
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_update_endpoint_fails_if_goal_belongs_to_other_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$goal = Goal::factory()->forMember($otherMember)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseMissing(Goal::class, ['name' => 'New name']);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_if_goal_belongs_to_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_update_endpoint_updates_only_given_fields(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$filters = new GoalFiltersDto;
|
|
$filters->billable = true;
|
|
$goal = Goal::factory()->forMember($data->member)->atLeast(3600)->period(GoalPeriod::Day)->filters($filters)->create([
|
|
'name' => 'Old name',
|
|
]);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
'target_seconds' => 7200,
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.name', 'New name')
|
|
->where('data.target_seconds', 7200)
|
|
->where('data.comparison', GoalComparison::AtLeast->value)
|
|
->where('data.period', GoalPeriod::Day->value)
|
|
->where('data.filters.billable', true)
|
|
->etc()
|
|
);
|
|
$goal->refresh();
|
|
$this->assertSame('New name', $goal->name);
|
|
$this->assertSame(7200, $goal->target_seconds);
|
|
$this->assertTrue($goal->filters->billable);
|
|
}
|
|
|
|
public function test_update_endpoint_can_change_the_timezone_and_the_week_start(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->timezone('UTC')->weekStart(Weekday::Monday)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'timezone' => 'Europe/Vienna',
|
|
'week_start' => Weekday::Sunday->value,
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJsonPath('data.timezone', 'Europe/Vienna');
|
|
$response->assertJsonPath('data.week_start', Weekday::Sunday->value);
|
|
$goal->refresh();
|
|
$this->assertSame('Europe/Vienna', $goal->timezone);
|
|
$this->assertSame(Weekday::Sunday, $goal->week_start);
|
|
}
|
|
|
|
public function test_update_endpoint_archives_and_unarchives_a_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$archiveResponse = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'is_archived' => true,
|
|
]);
|
|
$archivedAt = $goal->refresh()->archived_at;
|
|
$unarchiveResponse = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'is_archived' => false,
|
|
]);
|
|
|
|
// Assert
|
|
$archiveResponse->assertOk();
|
|
$archiveResponse->assertJsonPath('data.is_archived', true);
|
|
$this->assertNotNull($archivedAt);
|
|
$unarchiveResponse->assertOk();
|
|
$unarchiveResponse->assertJsonPath('data.is_archived', false);
|
|
$this->assertNull($goal->refresh()->archived_at);
|
|
}
|
|
|
|
public function test_update_endpoint_replaces_filters_when_given(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$project = Project::factory()->forOrganization($data->organization)->create();
|
|
$filters = new GoalFiltersDto;
|
|
$filters->billable = true;
|
|
$filters->setProjectIds([$project->getKey()]);
|
|
$goal = Goal::factory()->forMember($data->member)->filters($filters)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'comparison' => GoalComparison::LessThan->value,
|
|
'period' => GoalPeriod::Month->value,
|
|
'filters' => [
|
|
'time_entry_type' => TimeEntryType::Break->value,
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertOk();
|
|
$response->assertJson(fn (AssertableJson $json) => $json
|
|
->where('data.comparison', GoalComparison::LessThan->value)
|
|
->where('data.period', GoalPeriod::Month->value)
|
|
->where('data.filters.project_ids', null)
|
|
->where('data.filters.billable', null)
|
|
->where('data.filters.time_entry_type', TimeEntryType::Break->value)
|
|
->etc()
|
|
);
|
|
$goal->refresh();
|
|
$this->assertNull($goal->filters->projectIds);
|
|
$this->assertNull($goal->filters->billable);
|
|
$this->assertSame(TimeEntryType::Break, $goal->filters->timeEntryType);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_for_stored_filter_ids_of_deleted_entities(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$project = Project::factory()->forOrganization($data->organization)->create();
|
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
|
$filters = new GoalFiltersDto;
|
|
$filters->setProjectIds([$project->getKey()]);
|
|
$filters->setTagIds([$tag->getKey()]);
|
|
$goal = Goal::factory()->forMember($data->member)->filters($filters)->create(['name' => 'Old name']);
|
|
$project->delete();
|
|
$tag->delete();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
'filters' => [
|
|
'project_ids' => [$project->getKey()],
|
|
'tag_ids' => [$tag->getKey()],
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['filters.project_ids.0', 'filters.tag_ids.0']);
|
|
$this->assertSame('Old name', $goal->refresh()->name);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_for_filter_ids_of_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$project = Project::factory()->forOrganization($data->organization)->create();
|
|
$filters = new GoalFiltersDto;
|
|
$filters->setProjectIds([$project->getKey()]);
|
|
$goal = Goal::factory()->forMember($data->member)->filters($filters)->create();
|
|
$otherOrganizationProject = Project::factory()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'filters' => [
|
|
'project_ids' => [$project->getKey(), $otherOrganizationProject->getKey()],
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['filters.project_ids.1']);
|
|
$response->assertJsonMissingValidationErrors(['filters.project_ids.0']);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_with_validation_error_if_the_type_is_sent(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'Renamed',
|
|
'type' => GoalType::Organization->value,
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['type']);
|
|
$goal->refresh();
|
|
$this->assertSame('Old name', $goal->name);
|
|
$this->assertSame(GoalType::Personal, $goal->type);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_with_validation_error_if_the_member_id_is_sent(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$goal = Goal::factory()->forMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'Renamed',
|
|
'member_id' => $otherMember->getKey(),
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['member_id']);
|
|
$goal->refresh();
|
|
$this->assertSame('Old name', $goal->name);
|
|
$this->assertSame($data->member->getKey(), $goal->member_id);
|
|
}
|
|
|
|
public function test_update_endpoint_accepts_the_type_and_the_member_id_if_they_are_unchanged(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'Renamed',
|
|
'type' => GoalType::Personal->value,
|
|
'member_id' => $data->member->getKey(),
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(200);
|
|
$goal->refresh();
|
|
$this->assertSame('Renamed', $goal->name);
|
|
$this->assertSame(GoalType::Personal, $goal->type);
|
|
$this->assertSame($data->member->getKey(), $goal->member_id);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_with_validation_error_for_a_member_ids_filter_on_a_personal_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
'filters' => [
|
|
'member_ids' => [$data->member->getKey()],
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['filters.member_ids']);
|
|
$this->assertSame('Old name', $goal->refresh()->name);
|
|
}
|
|
|
|
public function test_update_endpoint_accepts_an_empty_member_ids_filter_on_a_personal_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
'filters' => [
|
|
'member_ids' => [],
|
|
],
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(200);
|
|
$goal->refresh();
|
|
$this->assertSame('New name', $goal->name);
|
|
$this->assertNull($goal->filters->memberIds);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_for_an_organization_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->organizationGoalForMember($data->member)->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertSame('Old name', $goal->refresh()->name);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_for_an_organization_goal_with_the_organization_type_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:organization-type']);
|
|
$goal = Goal::factory()->forOrganization($data->organization)->forEveryMember()->create(['name' => 'Old name']);
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'name' => 'New name',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertSame('Old name', $goal->refresh()->name);
|
|
}
|
|
|
|
public function test_update_endpoint_fails_with_validation_errors_for_invalid_body(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:update:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->putJson(route('api.v1.goals.update', [$data->organization->getKey(), $goal->getKey()]), [
|
|
'target_seconds' => -5,
|
|
'period' => 'quarter',
|
|
'is_archived' => 'maybe',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertUnprocessable();
|
|
$response->assertJsonValidationErrors(['target_seconds', 'period', 'is_archived']);
|
|
}
|
|
|
|
public function test_destroy_endpoint_fails_if_user_does_not_have_permission_to_delete_goals(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission();
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
|
|
public function test_destroy_endpoint_fails_if_goal_belongs_to_other_member(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:delete:own']);
|
|
$otherMember = Member::factory()->forOrganization($data->organization)->create();
|
|
$goal = Goal::factory()->forMember($otherMember)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
|
|
public function test_destroy_endpoint_fails_for_an_organization_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:delete:own']);
|
|
$goal = Goal::factory()->organizationGoalForMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
|
|
public function test_destroy_endpoint_fails_for_an_organization_goal_with_the_organization_type_permission(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:delete:organization-type']);
|
|
$goal = Goal::factory()->forOrganization($data->organization)->forEveryMember()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
|
|
public function test_destroy_endpoint_fails_if_goal_belongs_to_other_organization(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:delete:own']);
|
|
$goal = Goal::factory()->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
|
|
public function test_destroy_endpoint_deletes_own_goal(): void
|
|
{
|
|
// Arrange
|
|
$data = $this->createUserWithPermission(['goals:delete:own']);
|
|
$goal = Goal::factory()->forMember($data->member)->create();
|
|
Passport::actingAs($data->user);
|
|
|
|
// Act
|
|
$response = $this->deleteJson(route('api.v1.goals.destroy', [$data->organization->getKey(), $goal->getKey()]));
|
|
|
|
// Assert
|
|
$response->assertNoContent();
|
|
$this->assertDatabaseMissing(Goal::class, ['id' => $goal->getKey()]);
|
|
}
|
|
}
|