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_drops_billable_flags_and_rates_if_organization_can_not_use_billable_rates(): void { // Arrange $this->actAsOrganizationWithoutBillableRates(); $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 $importer->importData($data, $timezone); // Assert $this->assertSame(3, Project::query()->whereBelongsTo($organization, 'organization')->whereNull('billable_rate')->where('is_billable', '=', false)->count()); $this->assertSame(1, ProjectMember::query()->whereBelongsToOrganization($organization)->whereNull('billable_rate')->count()); $timeEntries = TimeEntry::query()->whereBelongsTo($organization, 'organization')->get(); $this->assertCount(2, $timeEntries); foreach ($timeEntries as $timeEntry) { $this->assertFalse($timeEntry->billable); $this->assertNull($timeEntry->billable_rate); } } 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_throws_exception_if_time_entry_references_task_that_does_not_exist(): void { // Arrange $zipPath = $this->createTestZip('solidtime_import_test_missing_task'); $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) { $this->assertInstanceOf(ImportException::class, $e); $this->assertSame('Time entry references a task that does not exist in the import', $e->getMessage()); return; } $this->fail(); } public function test_import_skips_running_time_entries(): void { // Arrange $zipPath = $this->createTestZip('solidtime_import_test_3'); $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); $this->assertSame(3, TimeEntry::query()->count()); $this->assertSame(0, TimeEntry::query()->whereNull('end')->count()); $this->assertNull(TimeEntry::query()->where('description', 'Still running')->first()); } 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(); } }