mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-10 14:53:17 +01:00
Partial updates of time entries only validated end against start if both were part of the payload. Start and end are now also validated against the persisted value if only one of them is sent (fixes #1189). The importers now fail if the end of a time entry is before the start and the database consistency check reports such time entries.
211 lines
7.8 KiB
PHP
211 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Service\Import\Importers;
|
|
|
|
use App\Enums\TimeEntryType;
|
|
use App\Jobs\RecalculateSpentTimeForProject;
|
|
use App\Jobs\RecalculateSpentTimeForTask;
|
|
use App\Models\Organization;
|
|
use App\Models\TimeEntry;
|
|
use App\Service\Import\Importers\DefaultImporter;
|
|
use App\Service\Import\Importers\ImportException;
|
|
use App\Service\Import\Importers\SolidtimeImporter;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Str;
|
|
use PHPUnit\Framework\Attributes\CoversClass;
|
|
use ZipArchive;
|
|
|
|
#[CoversClass(SolidtimeImporter::class)]
|
|
#[CoversClass(ImportException::class)]
|
|
#[CoversClass(DefaultImporter::class)]
|
|
class SolidtimeImporterTest extends ImporterTestAbstract
|
|
{
|
|
public function test_import_throws_exception_if_data_is_not_zip(): void
|
|
{
|
|
// Arrange
|
|
$organization = Organization::factory()->create();
|
|
$timezone = 'Europe/Vienna';
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
|
|
// Act
|
|
try {
|
|
$importer->importData('not a zip', $timezone);
|
|
} catch (Exception $e) {
|
|
$this->assertInstanceOf(ImportException::class, $e);
|
|
$this->assertSame('Invalid ZIP, error code: 19', $e->getMessage());
|
|
|
|
return;
|
|
}
|
|
$this->fail();
|
|
}
|
|
|
|
public function test_import_throws_exception_if_zip_exceeds_uncompressed_size_limit(): void
|
|
{
|
|
// Arrange
|
|
config(['import.zip_max_uncompressed_size' => 10]);
|
|
$zipPath = $this->createTestZip('solidtime_import_test_1');
|
|
$timezone = 'Europe/Vienna';
|
|
$organization = Organization::factory()->create();
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
$data = file_get_contents($zipPath);
|
|
|
|
// Act
|
|
try {
|
|
$importer->importData($data, $timezone);
|
|
} catch (Exception $e) {
|
|
// Assert
|
|
$this->assertInstanceOf(ImportException::class, $e);
|
|
$this->assertSame('ZIP uncompressed size exceeds the maximum of 10 bytes', $e->getMessage());
|
|
$this->assertSame(0, $importer->getReport()->timeEntriesCreated);
|
|
|
|
return;
|
|
}
|
|
$this->fail();
|
|
}
|
|
|
|
public function test_import_of_test_file_succeeds(): void
|
|
{
|
|
// Arrange
|
|
$zipPath = $this->createTestZip('solidtime_import_test_1');
|
|
$timezone = 'Europe/Vienna';
|
|
$organization = Organization::factory()->create();
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
$data = file_get_contents($zipPath);
|
|
Queue::fake([
|
|
RecalculateSpentTimeForProject::class,
|
|
RecalculateSpentTimeForTask::class,
|
|
]);
|
|
|
|
// Act
|
|
DB::enableQueryLog();
|
|
DB::flushQueryLog();
|
|
$importer->importData($data, $timezone);
|
|
$report = $importer->getReport();
|
|
$queryLog = DB::getQueryLog();
|
|
|
|
// Assert
|
|
$this->assertCount(25, $queryLog);
|
|
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries(true);
|
|
$this->checkTimeEntries($testScenario);
|
|
$this->assertSame(2, $report->timeEntriesCreated);
|
|
$this->assertSame(2, $report->tagsCreated);
|
|
$this->assertSame(2, $report->tasksCreated);
|
|
$this->assertSame(1, $report->usersCreated);
|
|
$this->assertSame(3, $report->projectsCreated);
|
|
$this->assertSame(2, $report->clientsCreated);
|
|
Queue::assertPushed(RecalculateSpentTimeForProject::class, 1);
|
|
Queue::assertPushed(RecalculateSpentTimeForTask::class, 1);
|
|
}
|
|
|
|
public function test_import_of_test_file_with_type_column_imports_breaks(): void
|
|
{
|
|
// Arrange
|
|
$zipPath = $this->createTestZip('solidtime_import_test_2');
|
|
$timezone = 'Europe/Vienna';
|
|
$organization = Organization::factory()->create();
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
$data = file_get_contents($zipPath);
|
|
Queue::fake([
|
|
RecalculateSpentTimeForProject::class,
|
|
RecalculateSpentTimeForTask::class,
|
|
]);
|
|
|
|
// Act
|
|
$importer->importData($data, $timezone);
|
|
$report = $importer->getReport();
|
|
|
|
// Assert
|
|
$this->assertSame(3, $report->timeEntriesCreated);
|
|
$timeEntries = TimeEntry::all();
|
|
$this->assertCount(3, $timeEntries);
|
|
// Empty type value falls back to the default type (work)
|
|
$timeEntryWithoutType = $timeEntries->firstWhere('description', '');
|
|
$this->assertNotNull($timeEntryWithoutType);
|
|
$this->assertSame(TimeEntryType::Work, $timeEntryWithoutType->type);
|
|
$workEntry = $timeEntries->firstWhere('description', 'Working hard');
|
|
$this->assertNotNull($workEntry);
|
|
$this->assertSame(TimeEntryType::Work, $workEntry->type);
|
|
$breakEntry = $timeEntries->firstWhere('description', 'Lunch break');
|
|
$this->assertNotNull($breakEntry);
|
|
$this->assertSame(TimeEntryType::Break, $breakEntry->type);
|
|
$this->assertFalse($breakEntry->billable);
|
|
$this->assertNull($breakEntry->project_id);
|
|
$this->assertNull($breakEntry->task_id);
|
|
$this->assertSame([], $breakEntry->tags);
|
|
}
|
|
|
|
public function test_import_of_test_file_twice_succeeds(): void
|
|
{
|
|
// Arrange
|
|
$zipPath = $this->createTestZip('solidtime_import_test_1');
|
|
$timezone = 'Europe/Vienna';
|
|
$organization = Organization::factory()->create();
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
$data = file_get_contents($zipPath);
|
|
$importer->importData($data, $timezone);
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
Queue::fake([
|
|
RecalculateSpentTimeForProject::class,
|
|
RecalculateSpentTimeForTask::class,
|
|
]);
|
|
|
|
// Act
|
|
DB::enableQueryLog();
|
|
DB::flushQueryLog();
|
|
$importer->importData($data, $timezone);
|
|
$report = $importer->getReport();
|
|
$queryLog = DB::getQueryLog();
|
|
|
|
// Assert
|
|
$this->assertCount(13, $queryLog);
|
|
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries(true);
|
|
$this->checkTimeEntries($testScenario, true);
|
|
$this->assertSame(2, $report->timeEntriesCreated);
|
|
$this->assertSame(0, $report->tagsCreated);
|
|
$this->assertSame(0, $report->tasksCreated);
|
|
$this->assertSame(0, $report->usersCreated);
|
|
$this->assertSame(0, $report->projectsCreated);
|
|
$this->assertSame(0, $report->clientsCreated);
|
|
Queue::assertPushed(RecalculateSpentTimeForProject::class, 1);
|
|
Queue::assertPushed(RecalculateSpentTimeForTask::class, 1);
|
|
}
|
|
|
|
public function test_import_fails_if_end_of_time_entry_is_before_start(): void
|
|
{
|
|
// Arrange
|
|
$organization = Organization::factory()->create();
|
|
$timezone = 'Europe/Vienna';
|
|
$zipPath = $this->createTestZip('solidtime_import_test_1');
|
|
$zip = new ZipArchive;
|
|
$zip->open($zipPath);
|
|
$timeEntries = $zip->getFromName('time_entries.csv');
|
|
$timeEntries = Str::replaceFirst(',2024-03-04T09:23:52Z,2024-03-04T09:23:52Z,', ',2024-03-04T09:23:52Z,2024-03-04T08:23:52Z,', $timeEntries);
|
|
$zip->addFromString('time_entries.csv', $timeEntries);
|
|
$zip->close();
|
|
$importer = new SolidtimeImporter;
|
|
$importer->init($organization);
|
|
$data = file_get_contents($zipPath);
|
|
|
|
// Act
|
|
try {
|
|
$importer->importData($data, $timezone);
|
|
} catch (ImportException $e) {
|
|
// Assert
|
|
$this->assertSame('End date ("2024-03-04T08:23:52Z") is before start date ("2024-03-04T09:23:52Z")', $e->getMessage());
|
|
|
|
return;
|
|
}
|
|
$this->fail();
|
|
}
|
|
}
|