From 71bed7de3b9d0b79b1ada35253a6a24d56518bf9 Mon Sep 17 00:00:00 2001
From: Pedro Pombeiro <noreply@pedro.pombei.ro>
Date: Fri, 22 Dec 2023 10:34:15 +0000
Subject: [PATCH] GraphQL: Add time window arguments to RunnersExportUsageInput

EE: true
---
 doc/api/graphql/reference/index.md            |  2 +
 .../mutations/ci/runners/export_usage.rb      | 34 +++++++++----
 .../ci/runners/generate_usage_csv_service.rb  | 17 +++----
 .../ci/runners/send_usage_csv_service.rb      | 12 ++---
 .../ci/runners/export_usage_csv_worker.rb     |  4 +-
 .../mutations/ci/runners/export_usage_spec.rb | 51 ++++++++++++++++---
 .../generate_usage_csv_service_spec.rb        | 37 ++++----------
 .../ci/runners/send_usage_csv_service_spec.rb |  2 +-
 .../runners/export_usage_csv_worker_spec.rb   |  8 ++-
 9 files changed, 105 insertions(+), 62 deletions(-)

diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e8dcf831d9f78..ed858aa534295 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6747,7 +6747,9 @@ Input type: `RunnersExportUsageInput`
 | Name | Type | Description |
 | ---- | ---- | ----------- |
 | <a id="mutationrunnersexportusageclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationrunnersexportusagefromdate"></a>`fromDate` | [`ISO8601Date`](#iso8601date) | UTC start date of the period to report on. Defaults to the start of last full month. |
 | <a id="mutationrunnersexportusagemaxprojectcount"></a>`maxProjectCount` | [`Int`](#int) | Maximum number of projects to return. All other runner usage will be attributed to a '<Other projects>' entry. Defaults to 1000 projects. |
+| <a id="mutationrunnersexportusagetodate"></a>`toDate` | [`ISO8601Date`](#iso8601date) | UTC end date of the period to report on. " \ "Defaults to the end of the month specified by `fromDate`. |
 | <a id="mutationrunnersexportusagetype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Scope of the runners to include in the report. |
 
 #### Fields
diff --git a/ee/app/graphql/mutations/ci/runners/export_usage.rb b/ee/app/graphql/mutations/ci/runners/export_usage.rb
index 8fd316962afe4..13aa87f83571e 100644
--- a/ee/app/graphql/mutations/ci/runners/export_usage.rb
+++ b/ee/app/graphql/mutations/ci/runners/export_usage.rb
@@ -6,23 +6,31 @@ module Runners
       class ExportUsage < BaseMutation
         graphql_name 'RunnersExportUsage'
 
+        DEFAULT_PROJECT_COUNT = 1_000
+
         argument :type, ::Types::Ci::RunnerTypeEnum,
           required: false,
           description: 'Scope of the runners to include in the report.'
 
+        argument :from_date, ::GraphQL::Types::ISO8601Date,
+          required: false,
+          description: 'UTC start date of the period to report on. Defaults to the start of last full month.'
+        argument :to_date, ::GraphQL::Types::ISO8601Date,
+          required: false,
+          description: 'UTC end date of the period to report on. " \
+            "Defaults to the end of the month specified by `fromDate`.'
+
         argument :max_project_count, ::GraphQL::Types::Int,
           required: false,
+          default_value: DEFAULT_PROJECT_COUNT,
           description:
             "Maximum number of projects to return. All other runner usage will be attributed " \
-            "to a '<Other projects>' entry. " \
-            "Defaults to #{::Ci::Runners::GenerateUsageCsvService::DEFAULT_PROJECT_COUNT} projects."
+            "to a '<Other projects>' entry. Defaults to #{DEFAULT_PROJECT_COUNT} projects."
 
         def ready?(**args)
           raise_resource_not_available_error! unless Ability.allowed?(current_user, :read_runner_usage)
 
-          max_project_count = args.fetch(
-            :max_project_count, ::Ci::Runners::GenerateUsageCsvService::DEFAULT_PROJECT_COUNT
-          )
+          max_project_count = args.fetch(:max_project_count, DEFAULT_PROJECT_COUNT)
 
           unless max_project_count.between?(1, ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT)
             raise Gitlab::Graphql::Errors::ArgumentError,
@@ -32,10 +40,18 @@ def ready?(**args)
           super
         end
 
-        def resolve(type: nil, max_project_count: nil)
-          ::Ci::Runners::ExportUsageCsvWorker.perform_async( # rubocop: disable CodeReuse/Worker -- this worker sends out emails
-            current_user.id, { runner_type: ::Ci::Runner.runner_types[type], max_project_count: max_project_count }
-          )
+        def resolve(type: nil, from_date: nil, to_date: nil, max_project_count: nil)
+          from_date ||= Date.current.prev_month.beginning_of_month
+          to_date ||= from_date.end_of_month
+
+          args = {
+            runner_type: ::Ci::Runner.runner_types[type],
+            from_date: from_date,
+            to_date: to_date,
+            max_project_count: max_project_count
+          }
+
+          ::Ci::Runners::ExportUsageCsvWorker.perform_async(current_user.id, args) # rubocop: disable CodeReuse/Worker -- this worker sends out emails
 
           {
             errors: []
diff --git a/ee/app/services/ci/runners/generate_usage_csv_service.rb b/ee/app/services/ci/runners/generate_usage_csv_service.rb
index 3e33b66fbd750..43e35eb128eba 100644
--- a/ee/app/services/ci/runners/generate_usage_csv_service.rb
+++ b/ee/app/services/ci/runners/generate_usage_csv_service.rb
@@ -8,24 +8,23 @@ module Runners
     class GenerateUsageCsvService
       attr_reader :project_ids, :runner_type, :from_date, :to_date
 
-      DEFAULT_PROJECT_COUNT = 1_000
       MAX_PROJECT_COUNT = 1_000
       OTHER_PROJECTS_NAME = '<Other projects>'
 
       # @param [User] current_user The user performing the reporting
-      # @param [Symbol] runner_type The type of runners to report on. Defaults to nil, reporting on all runner types
-      # @param [Date] from_date The start date of the period to examine. Defaults to start of last full month
-      # @param [Date] to_date The end date of the period to examine. Defaults to end of month
+      # @param [Symbol] runner_type The type of runners to report on, or nil to report on all types
+      # @param [Date] from_date The start date of the period to examine
+      # @param [Date] to_date The end date of the period to examine
       # @param [Integer] max_project_count The maximum number of projects in the report. All others will be folded
-      #   into an 'Other projects' entry. Defaults to 1000
-      def initialize(current_user:, runner_type: nil, from_date: nil, to_date: nil, max_project_count: nil)
+      #   into an 'Other projects' entry
+      def initialize(current_user:, runner_type:, from_date:, to_date:, max_project_count:)
         runner_type = Ci::Runner.runner_types[runner_type] if runner_type.is_a?(Symbol)
 
         @current_user = current_user
         @runner_type = runner_type
-        @from_date = from_date || Date.current.prev_month.beginning_of_month
-        @to_date = to_date || @from_date.end_of_month
-        @max_project_count = [MAX_PROJECT_COUNT, max_project_count || DEFAULT_PROJECT_COUNT].min
+        @max_project_count = [MAX_PROJECT_COUNT, max_project_count].min
+        @from_date = from_date
+        @to_date = to_date
       end
 
       def execute
diff --git a/ee/app/services/ci/runners/send_usage_csv_service.rb b/ee/app/services/ci/runners/send_usage_csv_service.rb
index 6ae6ed50b397c..23dfa8a0a0b1d 100644
--- a/ee/app/services/ci/runners/send_usage_csv_service.rb
+++ b/ee/app/services/ci/runners/send_usage_csv_service.rb
@@ -7,12 +7,12 @@ module Runners
     #
     class SendUsageCsvService
       # @param [User] current_user The user performing the reporting
-      # @param [Symbol] runner_type The type of runners to report on. Defaults to nil, reporting on all runner types
-      # @param [Date] from_date The start date of the period to examine. Defaults to start of last full month
-      # @param [Date] to_date The end date of the period to examine. Defaults to end of month
+      # @param [Symbol] runner_type The type of runners to report on, or nil to report on all types
+      # @param [Date] from_date The start date of the period to examine
+      # @param [Date] to_date The end date of the period to examine
       # @param [Integer] max_project_count The maximum number of projects in the report. All others will be folded
-      #   into an 'Other projects' entry. Defaults to 1000
-      def initialize(current_user:, runner_type: nil, from_date: nil, to_date: nil, max_project_count: nil)
+      #   into an 'Other projects' entry
+      def initialize(current_user:, runner_type:, from_date:, to_date:, max_project_count:)
         @current_user = current_user
         @runner_type = runner_type
         @from_date = from_date
@@ -33,7 +33,7 @@ def execute
         return result if result.error?
 
         Notify.runner_usage_by_project_csv_email(
-          user: @current_user, from_date: generate_csv_service.from_date, to_date: generate_csv_service.to_date,
+          user: @current_user, from_date: @from_date, to_date: @to_date,
           csv_data: result.payload[:csv_data], export_status: result.payload[:status]
         ).deliver_now
 
diff --git a/ee/app/workers/ci/runners/export_usage_csv_worker.rb b/ee/app/workers/ci/runners/export_usage_csv_worker.rb
index 4a86028332545..22127cecbebda 100644
--- a/ee/app/workers/ci/runners/export_usage_csv_worker.rb
+++ b/ee/app/workers/ci/runners/export_usage_csv_worker.rb
@@ -18,8 +18,10 @@ def perform(current_user_id, params)
         params.symbolize_keys!
 
         user = User.find(current_user_id)
+        from_date = Date.parse(params[:from_date])
+        to_date = Date.parse(params[:to_date])
         result = Ci::Runners::SendUsageCsvService.new(
-          current_user: user, **params.slice(:runner_type, :max_project_count)
+          current_user: user, from_date: from_date, to_date: to_date, **params.slice(:runner_type, :max_project_count)
         ).execute
         log_extra_metadata_on_done(:status, result.status)
         log_extra_metadata_on_done(:message, result.message) if result.message
diff --git a/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb b/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb
index f0c9cfab27fec..76b92d4521f6c 100644
--- a/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb
@@ -22,9 +22,17 @@
   end
 
   let(:runner_type) { 'group_type' }
-  let(:max_project_count) { 7 }
+  let(:mutation_args) do
+    {
+      type: runner_type.upcase,
+      from_date: Date.new(2023, 11, 1),
+      to_date: Date.new(2023, 11, 30),
+      max_project_count: 7
+    }
+  end
+
   let(:mutation) do
-    graphql_mutation(:runners_export_usage, type: runner_type.upcase, max_project_count: max_project_count) do
+    graphql_mutation(:runners_export_usage, mutation_args) do
       <<~QL
         errors
       QL
@@ -45,11 +53,12 @@
   it 'sends email with report' do
     expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async)
       .with(current_user.id, {
-        runner_type: ::Ci::Runner.runner_types[runner_type], max_project_count: max_project_count
+        runner_type: ::Ci::Runner.runner_types[runner_type],
+        **mutation_args.slice(:from_date, :to_date, :max_project_count)
       }).and_call_original
     expect(Notify).to receive(:runner_usage_by_project_csv_email)
       .with(
-        user: current_user, from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 1).end_of_month,
+        user: current_user, from_date: mutation_args[:from_date], to_date: mutation_args[:to_date],
         csv_data: anything, export_status: anything
       ) do |args|
         expect(args.dig(:export_status, :rows_written)).to eq 1
@@ -64,9 +73,37 @@
     expect_graphql_errors_to_be_empty
   end
 
+  context 'with default args' do
+    let(:mutation_args) { {} }
+
+    it 'sends email with report' do
+      expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async)
+        .with(current_user.id, {
+          runner_type: nil, from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 30), max_project_count: 1_000
+        }).and_call_original
+
+      post_response
+      expect_graphql_errors_to_be_empty
+    end
+  end
+
+  context 'with only from_date' do
+    let(:mutation_args) { { from_date: Date.new(2023, 9, 1) } }
+
+    it 'sends email with report of the month of September' do
+      expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async)
+        .with(current_user.id, {
+          runner_type: nil, from_date: Date.new(2023, 9, 1), to_date: Date.new(2023, 9, 30), max_project_count: 1_000
+        }).and_call_original
+
+      post_response
+      expect_graphql_errors_to_be_empty
+    end
+  end
+
   context 'when max_project_count is out-of-range' do
     context 'and is below acceptable range' do
-      let(:max_project_count) { 0 }
+      let(:mutation_args) { { type: runner_type.upcase, max_project_count: 0 } }
 
       it 'returns an error' do
         post_response
@@ -75,7 +112,9 @@
     end
 
     context 'and is above acceptable range' do
-      let(:max_project_count) { ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT + 1 }
+      let(:mutation_args) do
+        { type: runner_type.upcase, max_project_count: ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT + 1 }
+      end
 
       it 'returns an error' do
         post_response
diff --git a/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb b/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb
index b50f18da2a9ad..9e49e0b662cdd 100644
--- a/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb
+++ b/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb
@@ -6,7 +6,7 @@
   feature_category: :fleet_visibility do
   include ClickHouseHelpers
 
-  let_it_be(:current_user) { create(:admin) }
+  let_it_be(:current_user) { build_stubbed(:admin) }
   let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_manager) }
   let_it_be(:group) { create(:group) }
   let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
@@ -26,9 +26,9 @@
   end
 
   let(:runner_type) { nil }
-  let(:from_date) { nil }
-  let(:to_date) { nil }
-  let(:max_project_count) { nil }
+  let(:from_date) { Date.new(2023, 12, 1) }
+  let(:to_date) { Date.new(2023, 12, 31) }
+  let(:max_project_count) { 50 }
   let(:response_status) { response.payload[:status] }
   let(:response_csv_lines) { response.payload[:csv_data].lines }
   let(:service) do
@@ -37,8 +37,6 @@
   end
 
   let(:expected_header) { "Project ID,Project path,Build count,Total duration (minutes),Total duration\n" }
-  let(:expected_from_date) { Date.new(2023, 12, 1) }
-  let(:expected_to_date) { Date.new(2023, 12, 31) }
 
   subject(:response) { service.execute }
 
@@ -165,20 +163,18 @@
     end
   end
 
-  context 'when from_date is beginning of current month' do
+  context 'when time window is current month' do
     let(:from_date) { Date.new(2024, 1, 1) }
-    let(:expected_from_date) { from_date }
-    let(:expected_to_date) { from_date.end_of_month }
+    let(:to_date) { Date.new(2024, 1, 31) }
 
     it 'exports usage data for runners which finished builds before date' do
       expect(response_status).to eq({ rows_expected: 16, rows_written: 16, truncated: false })
     end
   end
 
-  context 'when from_date is next month' do
+  context 'when time window is next month' do
     let(:from_date) { Date.new(2024, 2, 1) }
-    let(:expected_from_date) { from_date }
-    let(:expected_to_date) { from_date.end_of_month }
+    let(:to_date) { Date.new(2024, 2, 29) }
 
     it 'exports usage data for runners which finished builds before date' do
       expect(response_status).to eq({ rows_expected: 0, rows_written: 0, truncated: false })
@@ -186,9 +182,8 @@
   end
 
   context 'when to_date is an hour ago, almost at the end of the year' do
+    let(:from_date) { Date.new(2023, 11, 1) }
     let(:to_date) { Date.new(2023, 12, 31) }
-    let(:expected_from_date) { Date.new(2023, 11, 1) }
-    let(:expected_to_date) { to_date }
 
     before do
       travel_to DateTime.new(2023, 12, 31, 23, 59, 59)
@@ -199,20 +194,6 @@
     end
   end
 
-  context 'when to_date is end of last month' do
-    let(:to_date) { Date.new(2024, 1, 31) }
-    let(:expected_from_date) { Date.new(2024, 1, 1) }
-    let(:expected_to_date) { to_date }
-
-    before do
-      travel_to DateTime.new(2024, 2, 10)
-    end
-
-    it 'exports usage data for runners which finished builds after date' do
-      expect(response_status).to eq({ rows_expected: 16, rows_written: 16, truncated: false })
-    end
-  end
-
   def create_build(runner, project, created_at, duration = 14.minutes)
     started_at = created_at + 6.minutes
 
diff --git a/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb b/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb
index 305ceec08df8a..59f3837c7af5a 100644
--- a/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb
+++ b/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb
@@ -6,7 +6,7 @@
   feature_category: :fleet_visibility do
   include ClickHouseHelpers
 
-  let_it_be(:current_user) { create(:admin) }
+  let_it_be(:current_user) { build_stubbed(:admin) }
   let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_manager) }
 
   let(:from_date) { 1.month.ago }
diff --git a/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb b/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb
index 01c27a9fb137b..58d1d55671b2b 100644
--- a/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb
+++ b/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb
@@ -12,7 +12,9 @@
     subject(:perform) { worker.perform(current_user.id, params) }
 
     let(:current_user) { admin }
-    let(:params) { { runner_type: 1, max_project_count: 25 } }
+    let(:params) do
+      { runner_type: 1, from_date: '2023-11-01', to_date: '2023-11-30', max_project_count: 25 }
+    end
 
     before do
       stub_licensed_features(runner_performance_insights: true)
@@ -20,7 +22,9 @@
 
     it 'delegates to Ci::Runners::SendUsageCsvService' do
       expect_next_instance_of(Ci::Runners::SendUsageCsvService, {
-        current_user: current_user, runner_type: params[:runner_type], max_project_count: params[:max_project_count]
+        current_user: current_user, runner_type: params[:runner_type],
+        from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 30),
+        max_project_count: params[:max_project_count]
       }) do |service|
         expect(service).to receive(:execute).and_call_original
       end
-- 
GitLab