mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-13 02:32:15 +01:00
Allow NONE filter value to shared reports and add shared-report tests
This commit is contained in:
@@ -16,6 +16,7 @@ use App\Models\Task;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\CurrencyService;
|
||||
use App\Service\Dto\ReportPropertiesDto;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\Unit\Endpoint\Api\V1\ApiEndpointTestAbstract;
|
||||
|
||||
@@ -423,4 +424,244 @@ class PublicReportEndpointTest extends ApiEndpointTestAbstract
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_show_returns_only_entries_without_project_when_none_project_filter_is_set(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$project = Project::factory()->forOrganization($organization)->create();
|
||||
|
||||
// Entry with project (should be excluded)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forProject($project)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create();
|
||||
// Entry without project (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->startWithDuration(now()->subDay(), 200)
|
||||
->create();
|
||||
|
||||
$reportDto = new ReportPropertiesDto;
|
||||
$reportDto->start = now()->subDays(2);
|
||||
$reportDto->end = now();
|
||||
$reportDto->group = TimeEntryAggregationType::Project;
|
||||
$reportDto->subGroup = TimeEntryAggregationType::Task;
|
||||
$reportDto->historyGroup = TimeEntryAggregationTypeInterval::Day;
|
||||
$reportDto->weekStart = Weekday::Monday;
|
||||
$reportDto->timezone = 'Europe/Vienna';
|
||||
$reportDto->setProjectIds([TimeEntryFilter::NONE_VALUE]);
|
||||
$report = Report::factory()->forOrganization($organization)->public()->create([
|
||||
'public_until' => null,
|
||||
'properties' => $reportDto,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.public.reports.show'), [
|
||||
'X-Api-Key' => $report->share_secret,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'seconds' => 200,
|
||||
'cost' => 0,
|
||||
'grouped_type' => TimeEntryAggregationType::Project->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_show_returns_entries_with_and_without_project_when_none_and_real_id_combined(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$projectA = Project::factory()->forOrganization($organization)->create();
|
||||
$projectB = Project::factory()->forOrganization($organization)->create();
|
||||
|
||||
// Entry with project A (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forProject($projectA)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create();
|
||||
// Entry with project B (should be excluded)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forProject($projectB)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create();
|
||||
// Entry without project (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->startWithDuration(now()->subDay(), 200)
|
||||
->create();
|
||||
|
||||
$reportDto = new ReportPropertiesDto;
|
||||
$reportDto->start = now()->subDays(2);
|
||||
$reportDto->end = now();
|
||||
$reportDto->group = TimeEntryAggregationType::Project;
|
||||
$reportDto->subGroup = TimeEntryAggregationType::Task;
|
||||
$reportDto->historyGroup = TimeEntryAggregationTypeInterval::Day;
|
||||
$reportDto->weekStart = Weekday::Monday;
|
||||
$reportDto->timezone = 'Europe/Vienna';
|
||||
$reportDto->setProjectIds([$projectA->getKey(), TimeEntryFilter::NONE_VALUE]);
|
||||
$report = Report::factory()->forOrganization($organization)->public()->create([
|
||||
'public_until' => null,
|
||||
'properties' => $reportDto,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.public.reports.show'), [
|
||||
'X-Api-Key' => $report->share_secret,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'seconds' => 300,
|
||||
'cost' => 0,
|
||||
'grouped_type' => TimeEntryAggregationType::Project->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_show_returns_only_entries_without_task_when_none_task_filter_is_set(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$project = Project::factory()->forOrganization($organization)->create();
|
||||
$task = Task::factory()->forOrganization($organization)->forProject($project)->create();
|
||||
|
||||
// Entry with task (should be excluded)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forTask($task)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create();
|
||||
// Entry without task (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forProject($project)
|
||||
->startWithDuration(now()->subDay(), 200)
|
||||
->create();
|
||||
|
||||
$reportDto = new ReportPropertiesDto;
|
||||
$reportDto->start = now()->subDays(2);
|
||||
$reportDto->end = now();
|
||||
$reportDto->group = TimeEntryAggregationType::Project;
|
||||
$reportDto->subGroup = TimeEntryAggregationType::Task;
|
||||
$reportDto->historyGroup = TimeEntryAggregationTypeInterval::Day;
|
||||
$reportDto->weekStart = Weekday::Monday;
|
||||
$reportDto->timezone = 'Europe/Vienna';
|
||||
$reportDto->setTaskIds([TimeEntryFilter::NONE_VALUE]);
|
||||
$report = Report::factory()->forOrganization($organization)->public()->create([
|
||||
'public_until' => null,
|
||||
'properties' => $reportDto,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.public.reports.show'), [
|
||||
'X-Api-Key' => $report->share_secret,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'seconds' => 200,
|
||||
'cost' => 0,
|
||||
'grouped_type' => TimeEntryAggregationType::Project->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_show_returns_only_entries_without_client_when_none_client_filter_is_set(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$client = Client::factory()->forOrganization($organization)->create();
|
||||
$projectWithClient = Project::factory()->forClient($client)->forOrganization($organization)->create();
|
||||
|
||||
// Entry with client (should be excluded)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->forProject($projectWithClient)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create();
|
||||
// Entry without client (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->startWithDuration(now()->subDay(), 200)
|
||||
->create();
|
||||
|
||||
$reportDto = new ReportPropertiesDto;
|
||||
$reportDto->start = now()->subDays(2);
|
||||
$reportDto->end = now();
|
||||
$reportDto->group = TimeEntryAggregationType::Project;
|
||||
$reportDto->subGroup = TimeEntryAggregationType::Task;
|
||||
$reportDto->historyGroup = TimeEntryAggregationTypeInterval::Day;
|
||||
$reportDto->weekStart = Weekday::Monday;
|
||||
$reportDto->timezone = 'Europe/Vienna';
|
||||
$reportDto->setClientIds([TimeEntryFilter::NONE_VALUE]);
|
||||
$report = Report::factory()->forOrganization($organization)->public()->create([
|
||||
'public_until' => null,
|
||||
'properties' => $reportDto,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.public.reports.show'), [
|
||||
'X-Api-Key' => $report->share_secret,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'seconds' => 200,
|
||||
'cost' => 0,
|
||||
'grouped_type' => TimeEntryAggregationType::Project->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_show_returns_only_entries_without_tags_when_none_tag_filter_is_set(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$tag = Tag::factory()->forOrganization($organization)->create();
|
||||
|
||||
// Entry with tag (should be excluded)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->startWithDuration(now()->subDay(), 100)
|
||||
->create([
|
||||
'tags' => [$tag->getKey()],
|
||||
]);
|
||||
// Entry without tags (should be included)
|
||||
TimeEntry::factory()->forOrganization($organization)
|
||||
->startWithDuration(now()->subDay(), 200)
|
||||
->create();
|
||||
|
||||
$reportDto = new ReportPropertiesDto;
|
||||
$reportDto->start = now()->subDays(2);
|
||||
$reportDto->end = now();
|
||||
$reportDto->group = TimeEntryAggregationType::Project;
|
||||
$reportDto->subGroup = TimeEntryAggregationType::Task;
|
||||
$reportDto->historyGroup = TimeEntryAggregationTypeInterval::Day;
|
||||
$reportDto->weekStart = Weekday::Monday;
|
||||
$reportDto->timezone = 'Europe/Vienna';
|
||||
$reportDto->setTagIds([TimeEntryFilter::NONE_VALUE]);
|
||||
$report = Report::factory()->forOrganization($organization)->public()->create([
|
||||
'public_until' => null,
|
||||
'properties' => $reportDto,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.public.reports.show'), [
|
||||
'X-Api-Key' => $report->share_secret,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'seconds' => 200,
|
||||
'cost' => 0,
|
||||
'grouped_type' => TimeEntryAggregationType::Project->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ use App\Enums\TimeEntryAggregationType;
|
||||
use App\Enums\TimeEntryRoundingType;
|
||||
use App\Enums\Weekday;
|
||||
use App\Http\Controllers\Api\V1\ReportController;
|
||||
use App\Models\Client;
|
||||
use App\Models\Project;
|
||||
use App\Models\Report;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Testing\Fluent\AssertableJson;
|
||||
use Laravel\Passport\Passport;
|
||||
@@ -490,6 +495,84 @@ class ReportEndpointTest extends ApiEndpointTestAbstract
|
||||
);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_creates_report_with_none_filter_values(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'reports:create',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->withoutExceptionHandling()->postJson(route('api.v1.reports.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Test Report with None Filters',
|
||||
'is_public' => false,
|
||||
'properties' => [
|
||||
'start' => Carbon::now()->subDays(30)->toIso8601ZuluString(),
|
||||
'end' => Carbon::now()->toIso8601ZuluString(),
|
||||
'group' => TimeEntryAggregationType::Project->value,
|
||||
'sub_group' => TimeEntryAggregationType::Task->value,
|
||||
'history_group' => TimeEntryAggregationType::Day->value,
|
||||
'project_ids' => [TimeEntryFilter::NONE_VALUE],
|
||||
'client_ids' => [TimeEntryFilter::NONE_VALUE],
|
||||
'tag_ids' => [TimeEntryFilter::NONE_VALUE],
|
||||
'task_ids' => [TimeEntryFilter::NONE_VALUE],
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(201);
|
||||
/** @var Report $report */
|
||||
$report = Report::query()->findOrFail($response->json('data.id'));
|
||||
$this->assertTrue($report->properties->projectIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->clientIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->tagIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->taskIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
}
|
||||
|
||||
public function test_store_endpoint_creates_report_with_none_combined_with_real_ids(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'reports:create',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$client = Client::factory()->forOrganization($data->organization)->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->withoutExceptionHandling()->postJson(route('api.v1.reports.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Test Report with Combined Filters',
|
||||
'is_public' => false,
|
||||
'properties' => [
|
||||
'start' => Carbon::now()->subDays(30)->toIso8601ZuluString(),
|
||||
'end' => Carbon::now()->toIso8601ZuluString(),
|
||||
'group' => TimeEntryAggregationType::Project->value,
|
||||
'sub_group' => TimeEntryAggregationType::Task->value,
|
||||
'history_group' => TimeEntryAggregationType::Day->value,
|
||||
'project_ids' => [$project->getKey(), TimeEntryFilter::NONE_VALUE],
|
||||
'client_ids' => [$client->getKey(), TimeEntryFilter::NONE_VALUE],
|
||||
'tag_ids' => [$tag->getKey(), TimeEntryFilter::NONE_VALUE],
|
||||
'task_ids' => [$task->getKey(), TimeEntryFilter::NONE_VALUE],
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(201);
|
||||
/** @var Report $report */
|
||||
$report = Report::query()->findOrFail($response->json('data.id'));
|
||||
$this->assertTrue($report->properties->projectIds->contains($project->getKey()));
|
||||
$this->assertTrue($report->properties->projectIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->clientIds->contains($client->getKey()));
|
||||
$this->assertTrue($report->properties->clientIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->tagIds->contains($tag->getKey()));
|
||||
$this->assertTrue($report->properties->tagIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
$this->assertTrue($report->properties->taskIds->contains($task->getKey()));
|
||||
$this->assertTrue($report->properties->taskIds->contains(TimeEntryFilter::NONE_VALUE));
|
||||
}
|
||||
|
||||
public function test_destroy_endpoint_fails_if_user_has_no_permission_to_delete_report(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Reference in New Issue
Block a user