Repository: phac-nml/irida-next
Pull request #992: Don't require project field when importing samples into project
Author: login: JeffreyThiessen
Labels: name: enhancement
description: New feature or request
name: ready for review
description: Pull request is ready for review
Created: 2025-03-19T17:30:41Z | Merged: 2025-03-21T20:30:28Z

## What does this PR do and why?
_Describe in detail what your merge request does and why._

`BatchFileImportService` no longer requires a project_puid header when importing into a project. The header is only required when importing into a group.

Restructured and added test files to verify functionality for both cases.

## Screenshots or screen recordings
_Screenshots are required for UI changes, and strongly recommended for all other pull requests._

## How to set up and validate locally
_Numbered steps to set up and validate the change are strongly suggested._

## PR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

- [x] I have evaluated the [PR acceptance checklist](https://phac-nml.github.io/irida-next/docs/development/development_processes/code_review#acceptance-checklist) for this PR.



Diff:
diff --git a/app/controllers/concerns/sample_spreadsheet_import_actions.rb b/app/controllers/concerns/sample_spreadsheet_import_actions.rb
index 93b5428219..6802e3af5b 100644
--- a/app/controllers/concerns/sample_spreadsheet_import_actions.rb
+++ b/app/controllers/concerns/sample_spreadsheet_import_actions.rb
@@ -30,10 +30,4 @@ def create # rubocop:disable Metrics/AbcSize
 
     render status: :ok
   end
-
-  private
-
-  def spreadsheet_import_params
-    params.expect(spreadsheet_import: %i[file sample_id_column project_puid_column sample_description_column])
-  end
 end
diff --git a/app/controllers/groups/samples/spreadsheet_imports_controller.rb b/app/controllers/groups/samples/spreadsheet_imports_controller.rb
index 46f3bdbd93..bf1de0bee0 100644
--- a/app/controllers/groups/samples/spreadsheet_imports_controller.rb
+++ b/app/controllers/groups/samples/spreadsheet_imports_controller.rb
@@ -23,6 +23,10 @@ def group
       def ensure_enabled
         not_found unless Flipper.enabled?(:batch_sample_spreadsheet_import)
       end
+
+      def spreadsheet_import_params
+        params.expect(spreadsheet_import: %i[file sample_name_column project_puid_column sample_description_column])
+      end
     end
   end
 end
diff --git a/app/controllers/projects/samples/spreadsheet_imports_controller.rb b/app/controllers/projects/samples/spreadsheet_imports_controller.rb
index 9ac462c7c3..5eb028f418 100644
--- a/app/controllers/projects/samples/spreadsheet_imports_controller.rb
+++ b/app/controllers/projects/samples/spreadsheet_imports_controller.rb
@@ -19,6 +19,10 @@ def namespace
       def ensure_enabled
         not_found unless Flipper.enabled?(:batch_sample_spreadsheet_import)
       end
+
+      def spreadsheet_import_params
+        params.expect(spreadsheet_import: %i[file sample_name_column sample_description_column])
+      end
     end
   end
 end
diff --git a/app/services/samples/batch_file_import_service.rb b/app/services/samples/batch_file_import_service.rb
index 0118d2aaeb..cb1f6c9e6d 100644
--- a/app/services/samples/batch_file_import_service.rb
+++ b/app/services/samples/batch_file_import_service.rb
@@ -7,9 +7,15 @@ module Samples
   class BatchFileImportService < BaseSpreadsheetImportService # rubocop:disable Metrics/ClassLength
     def initialize(namespace, user = nil, blob_id = nil, params = {})
       @sample_name_column = params[:sample_name_column]
-      @project_puid_column = params[:project_puid_column]
       @sample_description_column = params[:sample_description_column]
-      required_headers = [@sample_name_column, @project_puid_column]
+      required_headers = [@sample_name_column]
+      if namespace.group_namespace?
+        @project_puid_column = params[:project_puid_column]
+        required_headers.push @project_puid_column
+        @static_project = nil
+      else
+        @static_project = namespace.project
+      end
       @project_samples_count = {}
       @project_puid_map = {}
       super(namespace, user, blob_id, required_headers, 0, params)
@@ -26,7 +32,7 @@ def execute
 
     protected
 
-    def perform_file_import # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
+    def perform_file_import # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
       response = {}
       parse_settings = @headers.zip(@headers).to_h
 
@@ -34,7 +40,8 @@ def perform_file_import # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
         next unless index.positive?
 
         sample_name = data[@sample_name_column]
-        project_puid = data[@project_puid_column]
+
+        project_puid = @static_project&.puid || data[@project_puid_column]
         description = data[@sample_description_column]
         metadata = process_metadata_row(data)
 
@@ -44,11 +51,15 @@ def perform_file_import # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
           next
         end
 
-        project = Namespaces::ProjectNamespace.find_by(puid: project_puid)&.project
-        error = errors_with_project(project_puid, project)
-        unless error.nil?
-          response[sample_name] = error
-          next
+        if @static_project
+          project = @static_project
+        else
+          project = Namespaces::ProjectNamespace.find_by(puid: project_puid)&.project
+          error = errors_with_project(project_puid, project)
+          unless error.nil?
+            response[sample_name] = error
+            next
+          end
         end
 
         response[sample_name] = process_sample_row(sample_name, project, description, metadata)
@@ -103,7 +114,7 @@ def errors_with_project(project_puid, project)
     end
 
     def process_metadata_row(data)
-      metadata = data.except(@sample_name_column, @project_puid_column, @sample_description_column)
+      metadata = data.except(*@required_headers, @sample_description_column)
       metadata.compact!
 
       metadata
diff --git a/test/controllers/groups/samples/spreadsheet_import_controller_test.rb b/test/controllers/groups/samples/spreadsheet_import_controller_test.rb
index ec899f92af..83a6e4e543 100644
--- a/test/controllers/groups/samples/spreadsheet_import_controller_test.rb
+++ b/test/controllers/groups/samples/spreadsheet_import_controller_test.rb
@@ -8,7 +8,7 @@ class SpreadsheetImportControllerTest < ActionDispatch::IntegrationTest
       setup do
         sign_in users(:john_doe)
         @group = groups(:group_one)
-        @csv = fixture_file_upload('test/fixtures/files/batch_sample_import_valid.csv')
+        @csv = fixture_file_upload('test/fixtures/files/batch_sample_import/group/valid.csv')
 
         Flipper.enable(:batch_sample_spreadsheet_import)
       end
diff --git a/test/controllers/projects/samples/spreadsheet_import_controller_test.rb b/test/controllers/projects/samples/spreadsheet_import_controller_test.rb
index 3cd8f8f224..5c244392b0 100644
--- a/test/controllers/projects/samples/spreadsheet_import_controller_test.rb
+++ b/test/controllers/projects/samples/spreadsheet_import_controller_test.rb
@@ -9,7 +9,7 @@ class SpreadsheetImportControllerTest < ActionDispatch::IntegrationTest
         sign_in users(:john_doe)
         @namespace = groups(:group_one)
         @project = projects(:project1)
-        @csv = fixture_file_upload('test/fixtures/files/batch_sample_import_valid.csv')
+        @csv = fixture_file_upload('test/fixtures/files/batch_sample_import/project/valid.csv')
 
         Flipper.enable(:batch_sample_spreadsheet_import)
       end
@@ -20,8 +20,7 @@ class SpreadsheetImportControllerTest < ActionDispatch::IntegrationTest
                params: {
                  spreadsheet_import: {
                    file: @csv,
-                   sample_id_column: 'sample_name',
-                   project_puid_column: 'project_puid'
+                   sample_id_column: 'sample_name'
                  }
                }
         end
diff --git a/test/fixtures/files/batch_sample_import_invalid_blank_line.csv b/test/fixtures/files/batch_sample_import/group/invalid_blank_line.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_blank_line.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_blank_line.csv
diff --git a/test/fixtures/files/batch_sample_import_invalid_missing_puid.csv b/test/fixtures/files/batch_sample_import/group/invalid_missing_puid.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_missing_puid.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_missing_puid.csv
diff --git a/test/fixtures/files/batch_sample_import_invalid_project.csv b/test/fixtures/files/batch_sample_import/group/invalid_project.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_project.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_project.csv
diff --git a/test/fixtures/files/batch_sample_import_invalid_sample_dup_in_file.csv b/test/fixtures/files/batch_sample_import/group/invalid_sample_dup_in_file.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_sample_dup_in_file.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_sample_dup_in_file.csv
diff --git a/test/fixtures/files/batch_sample_import_invalid_sample_exists.csv b/test/fixtures/files/batch_sample_import/group/invalid_sample_exists.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_sample_exists.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_sample_exists.csv
diff --git a/test/fixtures/files/batch_sample_import_invalid_short_sample_name.csv b/test/fixtures/files/batch_sample_import/group/invalid_short_sample_name.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_invalid_short_sample_name.csv
rename to test/fixtures/files/batch_sample_import/group/invalid_short_sample_name.csv
diff --git a/test/fixtures/files/batch_sample_import_valid.csv b/test/fixtures/files/batch_sample_import/group/valid.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_valid.csv
rename to test/fixtures/files/batch_sample_import/group/valid.csv
diff --git a/test/fixtures/files/batch_sample_import_with_metadata_valid.csv b/test/fixtures/files/batch_sample_import/group/with_metadata_valid.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_with_metadata_valid.csv
rename to test/fixtures/files/batch_sample_import/group/with_metadata_valid.csv
diff --git a/test/fixtures/files/batch_sample_import_with_metadata_with_empty.csv b/test/fixtures/files/batch_sample_import/group/with_metadata_with_empty.csv
similarity index 100%
rename from test/fixtures/files/batch_sample_import_with_metadata_with_empty.csv
rename to test/fixtures/files/batch_sample_import/group/with_metadata_with_empty.csv
diff --git a/test/fixtures/files/batch_sample_import/project/invalid_blank_line.csv b/test/fixtures/files/batch_sample_import/project/invalid_blank_line.csv
new file mode 100644
index 0000000000..6c5b672c5a
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/invalid_blank_line.csv
@@ -0,0 +1,4 @@
+sample_name,description
+my new sample,my description
+,,
+my new sample 2,my description 2
diff --git a/test/fixtures/files/batch_sample_import/project/invalid_sample_dup_in_file.csv b/test/fixtures/files/batch_sample_import/project/invalid_sample_dup_in_file.csv
new file mode 100644
index 0000000000..bf50a52909
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/invalid_sample_dup_in_file.csv
@@ -0,0 +1,3 @@
+sample_name,description
+my new sample,my description
+my new sample,my description 2
diff --git a/test/fixtures/files/batch_sample_import/project/invalid_sample_exists.csv b/test/fixtures/files/batch_sample_import/project/invalid_sample_exists.csv
new file mode 100644
index 0000000000..e4319089ba
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/invalid_sample_exists.csv
@@ -0,0 +1,3 @@
+sample_name,description
+my new sample,my description
+Project 1 Sample 1,my description 2
diff --git a/test/fixtures/files/batch_sample_import/project/invalid_short_sample_name.csv b/test/fixtures/files/batch_sample_import/project/invalid_short_sample_name.csv
new file mode 100644
index 0000000000..039426c4a1
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/invalid_short_sample_name.csv
@@ -0,0 +1,3 @@
+sample_name,description
+my new sample,my description
+m,my description 2
diff --git a/test/fixtures/files/batch_sample_import/project/valid.csv b/test/fixtures/files/batch_sample_import/project/valid.csv
new file mode 100644
index 0000000000..1c36bbcc96
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/valid.csv
@@ -0,0 +1,3 @@
+sample_name,description
+my new sample,my description
+my new sample 2,my description 2
diff --git a/test/fixtures/files/batch_sample_import/project/with_metadata_valid.csv b/test/fixtures/files/batch_sample_import/project/with_metadata_valid.csv
new file mode 100644
index 0000000000..0b65c02c6a
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/with_metadata_valid.csv
@@ -0,0 +1,3 @@
+sample_name,description,metadata1,metadata2
+my new sample,my description,a,b
+my new sample 2,my description 2,c,d
diff --git a/test/fixtures/files/batch_sample_import/project/with_metadata_with_empty.csv b/test/fixtures/files/batch_sample_import/project/with_metadata_with_empty.csv
new file mode 100644
index 0000000000..7c7b863ee0
--- /dev/null
+++ b/test/fixtures/files/batch_sample_import/project/with_metadata_with_empty.csv
@@ -0,0 +1,3 @@
+sample_name,description,metadata1,metadata2
+my new sample,my description,a,b
+my new sample 2,my description 2,,d
diff --git a/test/services/samples/batch_file_import_service_test.rb b/test/services/samples/group_batch_sample_import_service_test.rb
similarity index 72%
rename from test/services/samples/batch_file_import_service_test.rb
rename to test/services/samples/group_batch_sample_import_service_test.rb
index 97c1139404..27b1046fe9 100644
--- a/test/services/samples/batch_file_import_service_test.rb
+++ b/test/services/samples/group_batch_sample_import_service_test.rb
@@ -3,7 +3,7 @@
 require 'test_helper'
 
 module Samples
-  class BatchFileImportServiceTest < ActiveSupport::TestCase
+  class GroupBatchSampleImportServiceTest < ActiveSupport::TestCase
     def setup # rubocop:disable Metrics/MethodLength
       @john_doe = users(:john_doe)
       @jane_doe = users(:jane_doe)
@@ -12,7 +12,7 @@ def setup # rubocop:disable Metrics/MethodLength
       @project = projects(:project1)
       @project2 = projects(:project2)
 
-      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/batch_sample_import_valid.csv'))
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/batch_sample_import/group/valid.csv'))
       @blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
         filename: file.original_filename,
@@ -25,20 +25,6 @@ def setup # rubocop:disable Metrics/MethodLength
       }
     end
 
-    test 'import samples with permission for project namespace' do
-      assert_equal 3, @project.samples.count
-
-      assert_authorized_to(:import_samples_and_metadata?, @project.namespace,
-                           with: Namespaces::ProjectNamespacePolicy,
-                           context: { user: @john_doe }) do
-        Samples::BatchFileImportService.new(@project.namespace, @john_doe, @blob.id, @default_params).execute
-      end
-
-      assert_equal 5, @project.samples.count
-      assert_equal 1, @project.samples.where(name: 'my new sample').count
-      assert_equal 1, @project.samples.where(name: 'my new sample 2').count
-    end
-
     test 'import samples with permission for group' do
       assert_equal 3, @project.samples.count
 
@@ -53,21 +39,6 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 1, @project.samples.where(name: 'my new sample 2').count
     end
 
-    test 'import samples without permission for project namespace' do
-      assert_equal 3, @project.samples.count
-
-      exception = assert_raises(ActionPolicy::Unauthorized) do
-        Samples::BatchFileImportService.new(@project.namespace, @jane_doe, @blob.id, @default_params).execute
-      end
-      assert_equal Namespaces::ProjectNamespacePolicy, exception.policy
-      assert_equal :import_samples_and_metadata?, exception.rule
-      assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
-      assert_equal I18n.t(:'action_policy.policy.namespaces/project_namespace.import_samples_and_metadata?',
-                          name: @project.name), exception.result.message
-
-      assert_equal 3, @project.samples.count
-    end
-
     test 'import samples without permission for group' do
       assert_equal 3, @project.samples.count
 
@@ -90,30 +61,10 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
-      assert_equal(@project.namespace.errors.full_messages_for(:base).first,
-                   I18n.t('services.spreadsheet_import.missing_header',
-                          header_title: 'sample_name,project_puid'))
-    end
-
-    test 'import samples into a project that does not belong to project namespace' do
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      response = Samples::BatchFileImportService.new(@project2.namespace, @john_doe, @blob.id,
-                                                     @default_params).execute
-
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample'][:message]
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample 2'][:message]
+      Samples::BatchFileImportService.new(@group, @john_doe, blob.id, @default_params).execute
+      assert_equal(I18n.t('services.spreadsheet_import.missing_header',
+                          header_title: 'sample_name,project_puid'),
+                   @group.errors.full_messages.to_sentence)
     end
 
     test 'import samples into a project that does not belong to group namespace' do
@@ -139,7 +90,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_project.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_project.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -147,7 +98,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 4, @project.samples.count
@@ -161,7 +112,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_missing_puid.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_missing_puid.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -169,7 +120,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 4, @project.samples.count
@@ -183,7 +134,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_blank_line.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_blank_line.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -191,7 +142,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 5, @project.samples.count
@@ -205,7 +156,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_short_sample_name.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_short_sample_name.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -213,7 +164,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 4, @project.samples.count
@@ -226,7 +177,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_sample_exists.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_sample_exists.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -234,7 +185,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 4, @project.samples.count
@@ -247,7 +198,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_invalid_sample_dup_in_file.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/invalid_sample_dup_in_file.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -255,7 +206,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id,
                                                      @default_params).execute
 
       assert_equal 4, @project.samples.count
@@ -269,7 +220,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_with_metadata_valid.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/with_metadata_valid.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -277,7 +228,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id, @default_params).execute
 
       assert_equal 5, @project.samples.count
 
@@ -299,7 +250,7 @@ def setup # rubocop:disable Metrics/MethodLength
       assert_equal 3, @project.samples.count
 
       file = Rack::Test::UploadedFile.new(
-        Rails.root.join('test/fixtures/files/batch_sample_import_with_metadata_with_empty.csv')
+        Rails.root.join('test/fixtures/files/batch_sample_import/group/with_metadata_with_empty.csv')
       )
       blob = ActiveStorage::Blob.create_and_upload!(
         io: file,
@@ -307,7 +258,7 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+      response = Samples::BatchFileImportService.new(@group, @john_doe, blob.id, @default_params).execute
 
       assert_equal 5, @project.samples.count
 
diff --git a/test/services/samples/project_batch_sample_import_service_test.rb b/test/services/samples/project_batch_sample_import_service_test.rb
new file mode 100644
index 0000000000..5e191bfb4a
--- /dev/null
+++ b/test/services/samples/project_batch_sample_import_service_test.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+module Samples
+  class ProjectBatchSampleImportServiceTest < ActiveSupport::TestCase
+    def setup # rubocop:disable Metrics/MethodLength
+      @john_doe = users(:john_doe)
+      @jane_doe = users(:jane_doe)
+      @group = groups(:group_one)
+      @group2 = groups(:group_two)
+      @project = projects(:project1)
+      @project2 = projects(:project2)
+
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/batch_sample_import/project/valid.csv'))
+      @blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+      @default_params = {
+        sample_name_column: 'sample_name',
+        sample_description_column: 'description'
+      }
+    end
+
+    test 'import samples with permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      assert_authorized_to(:import_samples_and_metadata?, @project.namespace,
+                           with: Namespaces::ProjectNamespacePolicy,
+                           context: { user: @john_doe }) do
+        Samples::BatchFileImportService.new(@project.namespace, @john_doe, @blob.id, @default_params).execute
+      end
+
+      assert_equal 5, @project.samples.count
+      assert_equal 1, @project.samples.where(name: 'my new sample').count
+      assert_equal 1, @project.samples.where(name: 'my new sample 2').count
+    end
+
+    test 'import samples without permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      exception = assert_raises(ActionPolicy::Unauthorized) do
+        Samples::BatchFileImportService.new(@project.namespace, @jane_doe, @blob.id, @default_params).execute
+      end
+      assert_equal Namespaces::ProjectNamespacePolicy, exception.policy
+      assert_equal :import_samples_and_metadata?, exception.rule
+      assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
+      assert_equal I18n.t(:'action_policy.policy.namespaces/project_namespace.import_samples_and_metadata?',
+                          name: @project.name), exception.result.message
+
+      assert_equal 3, @project.samples.count
+    end
+
+    test 'import samples with empty file' do
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/metadata/empty.csv'))
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+      assert_equal(I18n.t('services.spreadsheet_import.missing_header',
+                          header_title: 'sample_name'),
+                   @project.namespace.errors.full_messages.to_sentence)
+    end
+
+    test 'import with bad data blank line' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/invalid_blank_line.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+                                                     @default_params).execute
+
+      assert_equal 5, @project.samples.count
+
+      assert_equal I18n.t('services.spreadsheet_import.missing_field',
+                          index: 2),
+                   response['index 2'][:message]
+    end
+
+    test 'import with bad data short sample name' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/invalid_short_sample_name.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+                                                     @default_params).execute
+
+      assert_equal 4, @project.samples.count
+
+      assert_equal ['sample', 'name'], response['m'][0][:path] # rubocop:disable Style/WordArray
+      assert_equal 'is too short (minimum is 3 characters)', response['m'][0][:message]
+    end
+
+    test 'import with bad data sample already exists' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/invalid_sample_exists.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+                                                     @default_params).execute
+
+      assert_equal 4, @project.samples.count
+
+      assert_equal ['sample', 'name'], response['Project 1 Sample 1'][0][:path] # rubocop:disable Style/WordArray
+      assert_equal 'has already been taken', response['Project 1 Sample 1'][0][:message]
+    end
+
+    test 'import with bad data invalid duplicate sample name in file' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/invalid_sample_dup_in_file.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id,
+                                                     @default_params).execute
+
+      assert_equal 4, @project.samples.count
+
+      assert_equal I18n.t('services.samples.batch_import.duplicate_sample_name',
+                          index: 2),
+                   response['index 2'][:message]
+    end
+
+    test 'import samples with metadata' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/with_metadata_valid.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+
+      assert_equal 5, @project.samples.count
+
+      m1 = { 'metadata1' => 'a', 'metadata2' => 'b' }
+      m2 = { 'metadata1' => 'c', 'metadata2' => 'd' }
+
+      assert_equal m1, response['my new sample'].metadata
+      assert_equal m1, @project.samples.where(name: 'my new sample')[0].metadata
+      assert_equal m2, response['my new sample 2'].metadata
+      assert_equal m2, @project.samples.where(name: 'my new sample 2')[0].metadata
+
+      assert_equal 'user', @project.samples.where(name: 'my new sample')[0].metadata_provenance['metadata1']['source']
+      assert_equal 'user', @project.samples.where(name: 'my new sample')[0].metadata_provenance['metadata2']['source']
+      assert_equal 'user', @project.samples.where(name: 'my new sample 2')[0].metadata_provenance['metadata1']['source']
+      assert_equal 'user', @project.samples.where(name: 'my new sample 2')[0].metadata_provenance['metadata2']['source']
+    end
+
+    test 'import samples with metadata with empty value' do
+      assert_equal 3, @project.samples.count
+
+      file = Rack::Test::UploadedFile.new(
+        Rails.root.join('test/fixtures/files/batch_sample_import/project/with_metadata_with_empty.csv')
+      )
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      response = Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+
+      assert_equal 5, @project.samples.count
+
+      m1 = { 'metadata1' => 'a', 'metadata2' => 'b' }
+      m2 = { 'metadata2' => 'd' }
+
+      assert_equal m1, response['my new sample'].metadata
+      assert_equal m1, @project.samples.where(name: 'my new sample')[0].metadata
+      assert_equal m2, response['my new sample 2'].metadata
+      assert_equal m2, @project.samples.where(name: 'my new sample 2')[0].metadata
+
+      assert_equal 'user', @project.samples.where(name: 'my new sample')[0].metadata_provenance['metadata1']['source']
+      assert_equal 'user', @project.samples.where(name: 'my new sample')[0].metadata_provenance['metadata2']['source']
+      assert_equal 'user', @project.samples.where(name: 'my new sample 2')[0].metadata_provenance['metadata2']['source']
+    end
+  end
+end


Reviews:
user: login: deepsidhu85
body: This is looking good (still need to test it out). Just left a few small comments
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T19:21:00Z
user: login: JeffreyThiessen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T19:00:26Z
user: login: JeffreyThiessen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T19:00:50Z
user: login: JeffreyThiessen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T19:00:55Z
user: login: deepsidhu85
body: Looks good to me!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T20:29:43Z

Review comments:
diff_hunk: @@ -6,6 +6,7 @@ module SampleSpreadsheetImportActions
 
   included do
     before_action proc { namespace }
+    before_action proc { spreadsheet_import_params }
path: app/controllers/concerns/sample_spreadsheet_import_actions.rb
user: login: deepsidhu85
body: This line isn't required and can be removed
created_at: 2025-03-20T13:44:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,30 +61,10 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
-      assert_equal(@project.namespace.errors.full_messages_for(:base).first,
-                   I18n.t('services.spreadsheet_import.missing_header',
-                          header_title: 'sample_name,project_puid'))
-    end
-
-    test 'import samples into a project that does not belong to project namespace' do
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      response = Samples::BatchFileImportService.new(@project2.namespace, @john_doe, @blob.id,
-                                                     @default_params).execute
-
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample'][:message]
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample 2'][:message]
+      Samples::BatchFileImportService.new(@group, @john_doe, blob.id, @default_params).execute
+      assert_equal(I18n.t('services.spreadsheet_import.missing_header',
+                          header_title: 'sample_name,project_puid'),
+                   @group.errors.full_messages_for(:base).first)
path: test/services/samples/group_batch_sample_import_service_test.rb
user: login: deepsidhu85
body: Change this to `@group.errors.full_messages.to_sentence`
created_at: 2025-03-20T19:13:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+module Samples
+  class ProjectBatchSampleImportServiceTest < ActiveSupport::TestCase
+    def setup # rubocop:disable Metrics/MethodLength
+      @john_doe = users(:john_doe)
+      @jane_doe = users(:jane_doe)
+      @group = groups(:group_one)
+      @group2 = groups(:group_two)
+      @project = projects(:project1)
+      @project2 = projects(:project2)
+
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/batch_sample_import/project/valid.csv'))
+      @blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+      @default_params = {
+        sample_name_column: 'sample_name',
+        sample_description_column: 'description'
+      }
+    end
+
+    test 'import samples with permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      assert_authorized_to(:import_samples_and_metadata?, @project.namespace,
+                           with: Namespaces::ProjectNamespacePolicy,
+                           context: { user: @john_doe }) do
+        Samples::BatchFileImportService.new(@project.namespace, @john_doe, @blob.id, @default_params).execute
+      end
+
+      assert_equal 5, @project.samples.count
+      assert_equal 1, @project.samples.where(name: 'my new sample').count
+      assert_equal 1, @project.samples.where(name: 'my new sample 2').count
+    end
+
+    test 'import samples without permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      exception = assert_raises(ActionPolicy::Unauthorized) do
+        Samples::BatchFileImportService.new(@project.namespace, @jane_doe, @blob.id, @default_params).execute
+      end
+      assert_equal Namespaces::ProjectNamespacePolicy, exception.policy
+      assert_equal :import_samples_and_metadata?, exception.rule
+      assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
+      assert_equal I18n.t(:'action_policy.policy.namespaces/project_namespace.import_samples_and_metadata?',
+                          name: @project.name), exception.result.message
+
+      assert_equal 3, @project.samples.count
+    end
+
+    test 'import samples with empty file' do
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/metadata/empty.csv'))
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+      assert_equal(@project.namespace.errors.full_messages_for(:base).first,
path: test/services/samples/project_batch_sample_import_service_test.rb
user: login: deepsidhu85
body: Change this to `@project.namespace.errors.full_messages.to_sentence`
created_at: 2025-03-20T19:14:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,6 +6,7 @@ module SampleSpreadsheetImportActions
 
   included do
     before_action proc { namespace }
+    before_action proc { spreadsheet_import_params }
path: app/controllers/concerns/sample_spreadsheet_import_actions.rb
user: login: JeffreyThiessen
body: Fixed in https://github.com/phac-nml/irida-next/pull/992/commits/41400a4374f3b41fb3429abc3a1d11a8b96d66fb
created_at: 2025-03-21T19:00:26Z
author_association: MEMBER
diff_hunk: @@ -90,30 +61,10 @@ def setup # rubocop:disable Metrics/MethodLength
         content_type: file.content_type
       )
 
-      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
-      assert_equal(@project.namespace.errors.full_messages_for(:base).first,
-                   I18n.t('services.spreadsheet_import.missing_header',
-                          header_title: 'sample_name,project_puid'))
-    end
-
-    test 'import samples into a project that does not belong to project namespace' do
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      response = Samples::BatchFileImportService.new(@project2.namespace, @john_doe, @blob.id,
-                                                     @default_params).execute
-
-      assert_equal 3, @project.samples.count
-      assert_equal 20, @project2.samples.count
-
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample'][:message]
-      assert_equal I18n.t('services.samples.batch_import.project_puid_not_in_namespace',
-                          project_puid: @project.puid,
-                          namespace: @project2.namespace.full_path),
-                   response['my new sample 2'][:message]
+      Samples::BatchFileImportService.new(@group, @john_doe, blob.id, @default_params).execute
+      assert_equal(I18n.t('services.spreadsheet_import.missing_header',
+                          header_title: 'sample_name,project_puid'),
+                   @group.errors.full_messages_for(:base).first)
path: test/services/samples/group_batch_sample_import_service_test.rb
user: login: JeffreyThiessen
body: Fix tests https://github.com/phac-nml/irida-next/pull/992/commits/04f561b5fa2efa5a3f8da68aa5f26e381df149e7
created_at: 2025-03-21T19:00:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+module Samples
+  class ProjectBatchSampleImportServiceTest < ActiveSupport::TestCase
+    def setup # rubocop:disable Metrics/MethodLength
+      @john_doe = users(:john_doe)
+      @jane_doe = users(:jane_doe)
+      @group = groups(:group_one)
+      @group2 = groups(:group_two)
+      @project = projects(:project1)
+      @project2 = projects(:project2)
+
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/batch_sample_import/project/valid.csv'))
+      @blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+      @default_params = {
+        sample_name_column: 'sample_name',
+        sample_description_column: 'description'
+      }
+    end
+
+    test 'import samples with permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      assert_authorized_to(:import_samples_and_metadata?, @project.namespace,
+                           with: Namespaces::ProjectNamespacePolicy,
+                           context: { user: @john_doe }) do
+        Samples::BatchFileImportService.new(@project.namespace, @john_doe, @blob.id, @default_params).execute
+      end
+
+      assert_equal 5, @project.samples.count
+      assert_equal 1, @project.samples.where(name: 'my new sample').count
+      assert_equal 1, @project.samples.where(name: 'my new sample 2').count
+    end
+
+    test 'import samples without permission for project namespace' do
+      assert_equal 3, @project.samples.count
+
+      exception = assert_raises(ActionPolicy::Unauthorized) do
+        Samples::BatchFileImportService.new(@project.namespace, @jane_doe, @blob.id, @default_params).execute
+      end
+      assert_equal Namespaces::ProjectNamespacePolicy, exception.policy
+      assert_equal :import_samples_and_metadata?, exception.rule
+      assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
+      assert_equal I18n.t(:'action_policy.policy.namespaces/project_namespace.import_samples_and_metadata?',
+                          name: @project.name), exception.result.message
+
+      assert_equal 3, @project.samples.count
+    end
+
+    test 'import samples with empty file' do
+      file = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/metadata/empty.csv'))
+      blob = ActiveStorage::Blob.create_and_upload!(
+        io: file,
+        filename: file.original_filename,
+        content_type: file.content_type
+      )
+
+      Samples::BatchFileImportService.new(@project.namespace, @john_doe, blob.id, @default_params).execute
+      assert_equal(@project.namespace.errors.full_messages_for(:base).first,
path: test/services/samples/project_batch_sample_import_service_test.rb
user: login: JeffreyThiessen
body: https://github.com/phac-nml/irida-next/pull/992/commits/04f561b5fa2efa5a3f8da68aa5f26e381df149e7
created_at: 2025-03-21T19:00:55Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-19T22:16:45Z
body: ## Code Metrics Report
| Coverage | Test Execution Time |
|---------:|--------------------:|
| 94.4%    | 11m17s              |


### Code coverage of files in pull request scope (98.4%)

|                                                                                                                 Files                                                                                                                 | Coverage |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------:|
| [app/controllers/concerns/sample_spreadsheet_import_actions.rb](https://github.com/phac-nml/irida-next/blob/d9481cf69a643517504c2471512713c0bf57bff6/app%2Fcontrollers%2Fconcerns%2Fsample_spreadsheet_import_actions.rb)             | 91.6%    |
| [app/controllers/groups/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/d9481cf69a643517504c2471512713c0bf57bff6/app%2Fcontrollers%2Fgroups%2Fsamples%2Fspreadsheet_imports_controller.rb)     | 100.0%   |
| [app/controllers/projects/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/d9481cf69a643517504c2471512713c0bf57bff6/app%2Fcontrollers%2Fprojects%2Fsamples%2Fspreadsheet_imports_controller.rb) | 100.0%   |
| [app/services/samples/batch_file_import_service.rb](https://github.com/phac-nml/irida-next/blob/d9481cf69a643517504c2471512713c0bf57bff6/app%2Fservices%2Fsamples%2Fbatch_file_import_service.rb)                                     | 98.8%    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-20T21:15:35Z
body: ## Code Metrics Report
| Coverage | Test Execution Time |
|---------:|--------------------:|
| 94.4%    | 11m55s              |


### Code coverage of files in pull request scope (97.6%)

|                                                                                                                 Files                                                                                                                 | Coverage |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------:|
| [app/controllers/concerns/sample_spreadsheet_import_actions.rb](https://github.com/phac-nml/irida-next/blob/41400a4374f3b41fb3429abc3a1d11a8b96d66fb/app%2Fcontrollers%2Fconcerns%2Fsample_spreadsheet_import_actions.rb)             | 90.9%    |
| [app/controllers/groups/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/41400a4374f3b41fb3429abc3a1d11a8b96d66fb/app%2Fcontrollers%2Fgroups%2Fsamples%2Fspreadsheet_imports_controller.rb)     | 100.0%   |
| [app/controllers/projects/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/41400a4374f3b41fb3429abc3a1d11a8b96d66fb/app%2Fcontrollers%2Fprojects%2Fsamples%2Fspreadsheet_imports_controller.rb) | 100.0%   |
| [app/services/samples/batch_file_import_service.rb](https://github.com/phac-nml/irida-next/blob/41400a4374f3b41fb3429abc3a1d11a8b96d66fb/app%2Fservices%2Fsamples%2Fbatch_file_import_service.rb)                                     | 97.8%    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-21T20:28:19Z
body: ## Code Metrics Report
| Coverage | Test Execution Time |
|---------:|--------------------:|
| 94.5%    | 11m58s              |


### Code coverage of files in pull request scope (97.6%)

|                                                                                                                 Files                                                                                                                 | Coverage |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------:|
| [app/controllers/concerns/sample_spreadsheet_import_actions.rb](https://github.com/phac-nml/irida-next/blob/04f561b5fa2efa5a3f8da68aa5f26e381df149e7/app%2Fcontrollers%2Fconcerns%2Fsample_spreadsheet_import_actions.rb)             | 90.9%    |
| [app/controllers/groups/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/04f561b5fa2efa5a3f8da68aa5f26e381df149e7/app%2Fcontrollers%2Fgroups%2Fsamples%2Fspreadsheet_imports_controller.rb)     | 100.0%   |
| [app/controllers/projects/samples/spreadsheet_imports_controller.rb](https://github.com/phac-nml/irida-next/blob/04f561b5fa2efa5a3f8da68aa5f26e381df149e7/app%2Fcontrollers%2Fprojects%2Fsamples%2Fspreadsheet_imports_controller.rb) | 100.0%   |
| [app/services/samples/batch_file_import_service.rb](https://github.com/phac-nml/irida-next/blob/04f561b5fa2efa5a3f8da68aa5f26e381df149e7/app%2Fservices%2Fsamples%2Fbatch_file_import_service.rb)                                     | 97.8%    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: NONE