From 65525791a64953377056f0900c595db0325b1ca5 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Wed, 14 Oct 2026 10:30:53 +0200 Subject: [PATCH] Throw import exception for missing references in solidtime import Time entries referencing a user, member, client, project or task that is not part of the import caused a TypeError, which surfaced as "Unknown error". --- .../Import/Importers/SolidtimeImporter.php | 15 +++++++++++++ .../clients.csv | 3 +++ .../members.csv | 2 ++ .../meta.json | 1 + .../organization_invitations.csv | 1 + .../organizations.csv | 2 ++ .../project_members.csv | 2 ++ .../projects.csv | 4 ++++ .../tags.csv | 3 +++ .../tasks.csv | 3 +++ .../time_entries.csv | 3 +++ .../Importers/SolidtimeImporterTest.php | 22 +++++++++++++++++++ 12 files changed, 61 insertions(+) create mode 100644 resources/testfiles/solidtime_import_test_missing_task/clients.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/members.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/meta.json create mode 100644 resources/testfiles/solidtime_import_test_missing_task/organization_invitations.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/organizations.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/project_members.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/projects.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/tags.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/tasks.csv create mode 100644 resources/testfiles/solidtime_import_test_missing_task/time_entries.csv diff --git a/app/Service/Import/Importers/SolidtimeImporter.php b/app/Service/Import/Importers/SolidtimeImporter.php index f6eade0e..5b89416f 100644 --- a/app/Service/Import/Importers/SolidtimeImporter.php +++ b/app/Service/Import/Importers/SolidtimeImporter.php @@ -212,17 +212,29 @@ class SolidtimeImporter extends DefaultImporter // Time entries foreach ($timeEntriesReader as $timeEntryRow) { $userId = $this->userImportHelper->getKeyByExternalIdentifier($timeEntryRow['user_id']); + if ($userId === null) { + throw new ImportException('Time entry references a user that does not exist in the import'); + } $memberId = $this->memberImportHelper->getKeyByExternalIdentifier($timeEntryRow['member_id']); + if ($memberId === null) { + throw new ImportException('Time entry references a member that does not exist in the import'); + } $member = $this->memberImportHelper->getModelById($memberId); $clientId = null; if ($timeEntryRow['client_id'] !== '') { $clientId = $this->clientImportHelper->getKeyByExternalIdentifier($timeEntryRow['client_id']); + if ($clientId === null) { + throw new ImportException('Time entry references a client that does not exist in the import'); + } } $project = null; $projectId = null; $projectMember = null; if ($timeEntryRow['project_id'] !== '') { $projectId = $this->projectImportHelper->getKeyByExternalIdentifier($timeEntryRow['project_id']); + if ($projectId === null) { + throw new ImportException('Time entry references a project that does not exist in the import'); + } $project = $this->projectImportHelper->getModelById($projectId); $projectMember = $this->projectMemberImportHelper->getModel([ 'project_id' => $projectId, @@ -232,6 +244,9 @@ class SolidtimeImporter extends DefaultImporter $taskId = null; if ($timeEntryRow['task_id'] !== '') { $taskId = $this->taskImportHelper->getKeyByExternalIdentifier($timeEntryRow['task_id']); + if ($taskId === null) { + throw new ImportException('Time entry references a task that does not exist in the import'); + } $this->taskImportHelper->getModelById($taskId); } $timeEntry = new TimeEntry; diff --git a/resources/testfiles/solidtime_import_test_missing_task/clients.csv b/resources/testfiles/solidtime_import_test_missing_task/clients.csv new file mode 100644 index 00000000..e84ef176 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/clients.csv @@ -0,0 +1,3 @@ +id,name,organization_id,archived_at,created_at,updated_at +b4187a44-41f4-46d7-8460-f15a25b3aad6,"Big Company",ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +e5a4d8f5-81ae-4606-8e84-6ab1ffa58b72,"Other Company (Archived)",ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/members.csv b/resources/testfiles/solidtime_import_test_missing_task/members.csv new file mode 100644 index 00000000..70140e1b --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/members.csv @@ -0,0 +1,2 @@ +id,user_id,name,email,organization_id,billable_rate,role,created_at,updated_at +06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,"Peter Tester",peter.test@email.test,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,admin,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/meta.json b/resources/testfiles/solidtime_import_test_missing_task/meta.json new file mode 100644 index 00000000..b40876f8 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/meta.json @@ -0,0 +1 @@ +{"id":"d6a324ee-58d5-4096-8069-c63bd55608f7","version":"1.0","organizations":["ee5a8cd6-312f-4ae6-b044-e2014f09ecc2"],"exported_at":"2024-08-26T18:21:59Z"} \ No newline at end of file diff --git a/resources/testfiles/solidtime_import_test_missing_task/organization_invitations.csv b/resources/testfiles/solidtime_import_test_missing_task/organization_invitations.csv new file mode 100644 index 00000000..01bbe37d --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/organization_invitations.csv @@ -0,0 +1 @@ +id,email,organization_id,role,created_at,updated_at diff --git a/resources/testfiles/solidtime_import_test_missing_task/organizations.csv b/resources/testfiles/solidtime_import_test_missing_task/organizations.csv new file mode 100644 index 00000000..09cfbed0 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/organizations.csv @@ -0,0 +1,2 @@ +id,name,billable_rate,currency,created_at,updated_at +ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,"ACME Corp",,EUR,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/project_members.csv b/resources/testfiles/solidtime_import_test_missing_task/project_members.csv new file mode 100644 index 00000000..3633c3d5 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/project_members.csv @@ -0,0 +1,2 @@ +id,billable_rate,project_id,user_id,member_id,created_at,updated_at +180a1a98-2f1c-4596-86e4-63a6be0d7b1d,10002,06e79ec4-33f8-4730-804c-d03c014991d1,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,06e6e605-86bd-417b-b75d-02f671e5d520,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/projects.csv b/resources/testfiles/solidtime_import_test_missing_task/projects.csv new file mode 100644 index 00000000..3c3b9e3d --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/projects.csv @@ -0,0 +1,4 @@ +id,name,color,billable_rate,is_public,client_id,organization_id,is_billable,archived_at,created_at,updated_at +06e79ec4-33f8-4730-804c-d03c014991d1,"Project for Big Company",#ec407a,10001,false,b4187a44-41f4-46d7-8460-f15a25b3aad6,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,true,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +622c74a9-7e64-44c2-9426-2a37ac738206,"Project without Client",#ef5350,,false,,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +aa831162-dbb2-4cfe-bfe0-5e3a252c66f0,"Project (Archived)",#6a407f,,true,e5a4d8f5-81ae-4606-8e84-6ab1ffa58b72,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,true,2024-08-25T10:00:00Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/tags.csv b/resources/testfiles/solidtime_import_test_missing_task/tags.csv new file mode 100644 index 00000000..6791d565 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/tags.csv @@ -0,0 +1,3 @@ +id,name,organization_id,created_at,updated_at +2c5c2da7-9ef8-4410-bb8f-6e0a90f9d2c7,Development,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +bf6c0ac5-2587-474b-8983-40bb3ea8002f,Backend,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/tasks.csv b/resources/testfiles/solidtime_import_test_missing_task/tasks.csv new file mode 100644 index 00000000..01babe7c --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/tasks.csv @@ -0,0 +1,3 @@ +id,name,project_id,organization_id,done_at,created_at,updated_at +b49688a0-94f3-4cb3-9ca1-5003de955fb0,"Task 1",06e79ec4-33f8-4730-804c-d03c014991d1,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +b49688a0-94f3-4cb3-9ca1-5003de955fb0,"Task 2",06e79ec4-33f8-4730-804c-d03c014991d1,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-24T10:00:00Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_missing_task/time_entries.csv b/resources/testfiles/solidtime_import_test_missing_task/time_entries.csv new file mode 100644 index 00000000..098ea127 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_missing_task/time_entries.csv @@ -0,0 +1,3 @@ +id,description,start,end,billable_rate,billable,member_id,user_id,organization_id,client_id,project_id,task_id,tags,is_imported,still_active_email_sent_at,created_at,updated_at +00aae3be-18fc-462d-bee4-350fb605b2f3,,2024-03-04T09:23:52Z,2024-03-04T09:23:52Z,,false,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,,,"[""2c5c2da7-9ef8-4410-bb8f-6e0a90f9d2c7"",""bf6c0ac5-2587-474b-8983-40bb3ea8002f""]",false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +1c7a905d-aa12-4d08-bc41-7e92577e7cdf,"Working hard",2024-03-04T09:23:00Z,2024-03-04T10:23:01Z,,true,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,b4187a44-41f4-46d7-8460-f15a25b3aad6,06e79ec4-33f8-4730-804c-d03c014991d1,9f0c1b2a-0000-4000-8000-000000000000,[],false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php b/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php index 3e93d675..b73ecaa4 100644 --- a/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php +++ b/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php @@ -140,6 +140,28 @@ class SolidtimeImporterTest extends ImporterTestAbstract $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_of_test_file_twice_succeeds(): void { // Arrange