jira-auto-tool 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcecb56b5f19c1cd8b4b54e50ccd801bff0e6179d0adc4c379855263dc698498
4
- data.tar.gz: 021b5d028d7469d1466b324a69ace618d9973bde7c5c6f1b32df8adb1582d73a
3
+ metadata.gz: f848715468127f8c2d342bdd77fd68f78b2e9684c67d506c0761e6982ce325d2
4
+ data.tar.gz: 3c578b782fce257b587e09c0261fe60c8fcb59293b6419e097b8f9fdd52e1bd4
5
5
  SHA512:
6
- metadata.gz: 80c9790171b5014a76ffcae1eae4391e7afd30fd667a48e27bd952e78e1af711e19f5c34cb030782d553de21707610d8c552a17d404f4afd755678b45b56caec
7
- data.tar.gz: 7e6ea437118ada049d32f04619224de79bf6f42c3882a02e2c741c510b8a33aa9888675773ce9a9abafd5ec0adfdb32b03af46c91868b6e3ee7882aa71812d8f
6
+ metadata.gz: 98c0d515b13ecf5f7f145eec5729435c7378d7325dae29bd250822461badcd47081a58ace1450b51c5b68eeb59d3552ff3905ae968c3e41fdc082c7af3141f37
7
+ data.tar.gz: 94871345d52b7809ad2267bf12f0d62114d0d10b1032fc93856a2fd78fef6c5c6308951fa6804e073ea3a0df046d81f3c648a6caadcacaac091ec1a98babe2b9
data/README.md CHANGED
@@ -7,15 +7,23 @@
7
7
  The purpose of this tool it support managing the sprints of multiple teams so it is easier to adjust to changes.
8
8
  See the [feature files](./features) for some behavior examples.
9
9
 
10
- ## Warning
10
+ ## Table of Contents
11
11
 
12
- 1. You should familiarize yourself with this tool in a Jira sandbox project **before applying it to your context**.
13
- That can be done easily by [creating a free Atlassian account](https://www.atlassian.com/software)
14
- like it has been done to document [this tool features](./features) using executable specifications.
12
+ <!-- Generated using jekyll-toc -->
15
13
 
16
- 1. Remember that you are **not allowed** to use confidential/sensitive information when familiarizing with this tool
17
- in such a cloud sandbox. Though, if the sandbox belongs to the target context
18
- (e.g., sandbox project on the client Jira instance) you can experiment with the parameters you intend to use later.
14
+ * TOC
15
+ {:toc}
16
+
17
+ ## Principles
18
+
19
+ Following a convention over configuration approach:
20
+ * All Scrum boards from the Jira instance are scanned to identify the ones matching the search criteria
21
+ (the list is [cached for a day for performance reasons](./features/cache_boards.feature) to deal with Jira
22
+ instances having thousands of boards);
23
+ * For each board, only the unclosed sprints are considered;
24
+ * Sprint manipulations only apply to those sprints whose names match the following format: `sprint_prefix_25.4.3`
25
+ * [Creating new sprints](./features/create_sprints_using_existing_ones_as_reference.feature)
26
+ will use the existing ones as a reference for the prefix and the length of the sprint.
19
27
 
20
28
  ## Installation
21
29
 
@@ -41,11 +49,8 @@ in such a cloud sandbox. Though, if the sandbox belongs to the target context
41
49
  ```
42
50
  2. Adjust the file to your context.
43
51
 
44
- **WARNING** - It is highly recommended that the JIRA_API_TOKEN value is set as an environment variable
45
- and **NOT** in the generated file.
46
-
47
- While we strive to use convention over configuration as a principle, the following environment variables have to be set
48
- in order to use this tool:
52
+ The following environment variables have to be set to use this tool. **Except** for te `JIRA_API_TOKEN` that should
53
+ be done via the configuration file.
49
54
 
50
55
  Some explanations:
51
56
 
@@ -67,8 +72,9 @@ Optional environment variables:
67
72
  See [sprint filtering](./features/sprint_filtering.feature).
68
73
  - `JIRA_CONTEXT_PATH` - Context path for Jira instance (if needed typically "/jira").
69
74
  - `JIRA_HTTP_DEBUG` - Enable HTTP debug logging (set to "true" or "false").
70
- - `JAT_RATE_LIMIT` - Rate limit for Jira API calls (e.g., "1").
71
- - `JAT_RATE_INTERVAL` - Interval for rate limiting in seconds (e.g., "1").
75
+ - `JAT_RATE_LIMIT_PER_INTERVAL` - Rate limit for Jira API calls (e.g., "1")
76
+ See [Control Jira HTTP request rate.](./features/control_http_request_rate_limit.feature).
77
+ - `JAT_RATE_INTERVAL_IN_SECONDS` - Interval for rate limiting in seconds (e.g., "1").
72
78
 
73
79
  ## Usage
74
80
 
@@ -79,6 +85,16 @@ See [sprint filtering](./features/sprint_filtering.feature).
79
85
  * Leverage the [specification by examples](./features) for a detailled understand of the features.
80
86
  * Note that usually the long option names have a short version equivalent to reduce typing.
81
87
 
88
+ ### Warning
89
+
90
+ 1. You should familiarize yourself with this tool in a Jira sandbox project **before applying it to your context**.
91
+ That can be done easily by [creating a free Atlassian account](https://www.atlassian.com/software)
92
+ like it has been done to document [this tool features](./features) using executable specifications.
93
+
94
+ 1. Remember that you are **not allowed** to use confidential/sensitive information when familiarizing with this tool
95
+ in such a cloud sandbox. Though, if the sandbox belongs to the target context
96
+ (e.g., sandbox project on the client Jira instance) you can experiment with the parameters you intend to use later.
97
+
82
98
  Below are a few examples.
83
99
 
84
100
  ### Add Sprints
@@ -10,12 +10,15 @@ DISABLE_COVERAGE: true
10
10
  EXPECTED_START_DATE_FIELD_NAME: Expected Start
11
11
  IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
12
12
  JAT_RATE_INTERVAL_IN_SECONDS:
13
- JAT_RATE_LIMIT_IN_SECONDS:
13
+ JAT_RATE_LIMIT_IMPLEMENTATION:
14
+ JAT_RATE_LIMIT_PER_INTERVAL:
14
15
  JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
16
+ # JIRA_BOARD_NAME: "<<<Name of one board if the project>>>"
15
17
  JIRA_BOARD_NAME: "<%= project_key %> - Delivery"
16
18
  JIRA_BOARD_NAME_REGEX: "<%= project_key %>|ART 16|unconventional board name"
17
19
  #JIRA_CONTEXT_PATH: /jira
18
20
  JIRA_CONTEXT_PATH:
21
+ JIRA_HTTP_DEBUG:
19
22
  JIRA_PROJECT_KEY: <%= project_key %>
20
23
  JIRA_SITE_URL: http://cbroult.atlassian.net:443/
21
24
  JIRA_SPRINT_FIELD_NAME: "<%= sprint_field_name %>"
@@ -17,7 +17,7 @@ Feature: Environment Configuration Management
17
17
  """
18
18
  ---
19
19
  <%
20
- project_key = "PROJ"
20
+ project_key = "JATCIDEVLX"
21
21
  sprint_field_name = "Sprint"
22
22
  jira_username = "cbroult@yahoo.com"
23
23
  %>
@@ -27,11 +27,14 @@ Feature: Environment Configuration Management
27
27
  EXPECTED_START_DATE_FIELD_NAME: Expected Start
28
28
  IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
29
29
  JAT_RATE_INTERVAL_IN_SECONDS:
30
- JAT_RATE_LIMIT_IN_SECONDS:
30
+ JAT_RATE_LIMIT_IMPLEMENTATION:
31
+ JAT_RATE_LIMIT_PER_INTERVAL:
31
32
  JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
32
- JIRA_BOARD_NAME: "<%= project_key %> - Your board name"
33
+ # JIRA_BOARD_NAME: "<<<Name of one board if the project>>>"
34
+ JIRA_BOARD_NAME: "<%= project_key %> - Delivery"
33
35
  JIRA_BOARD_NAME_REGEX: "<%= project_key %>|ART 16|unconventional board name"
34
- JIRA_CONTEXT_PATH: /jira
36
+ #JIRA_CONTEXT_PATH: /jira
37
+ JIRA_CONTEXT_PATH:
35
38
  JIRA_HTTP_DEBUG:
36
39
  JIRA_PROJECT_KEY: <%= project_key %>
37
40
  JIRA_SITE_URL: http://cbroult.atlassian.net:443/
@@ -74,7 +77,8 @@ Feature: Environment Configuration Management
74
77
  EXPECTED_START_DATE_FIELD_NAME: Expected Start
75
78
  IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
76
79
  JAT_RATE_INTERVAL_IN_SECONDS:
77
- JAT_RATE_LIMIT_IN_SECONDS:
80
+ JAT_RATE_LIMIT_IMPLEMENTATION:
81
+ JAT_RATE_LIMIT_PER_INTERVAL:
78
82
  JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
79
83
  JIRA_API_TOKEN: "current API TOKEN"
80
84
  JIRA_BOARD_NAME: "Team Board"
@@ -97,7 +101,8 @@ Feature: Environment Configuration Management
97
101
  | EXPECTED_START_DATE_FIELD_NAME | Expected Start |
98
102
  | IMPLEMENTATION_TEAM_FIELD_NAME | Implementation Team |
99
103
  | JAT_RATE_INTERVAL_IN_SECONDS | |
100
- | JAT_RATE_LIMIT_IN_SECONDS | |
104
+ | JAT_RATE_LIMIT_IMPLEMENTATION | |
105
+ | JAT_RATE_LIMIT_PER_INTERVAL | |
101
106
  | JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL | project = PROJ AND Sprint IS EMPTY |
102
107
  | JIRA_API_TOKEN | current API TOKEN |
103
108
  | JIRA_BOARD_NAME | Team Board |
@@ -5,13 +5,25 @@ Feature: Control the HTTP request rate limit
5
5
 
6
6
  Scenario Outline: Limiting the request rate
7
7
  Given the following environment variables are set:
8
- | name | value |
9
- | JAT_RATE_LIMIT_IN_SECONDS | <rate_limit> |
10
- | JAT_RATE_INTERVAL_IN_SECONDS | <rate_interval> |
8
+ | name | value |
9
+ | JAT_RATE_INTERVAL_IN_SECONDS | <rate_interval_in_seconds> |
10
+ | JAT_RATE_LIMIT_IMPLEMENTATION | <jat_rate_limit_implementation> |
11
+ | JAT_RATE_LIMIT_PER_INTERVAL | <rate_limit_per_interval> |
11
12
  Then successfully running `jira-auto-tool --board-list --sprint-prefix` takes between <minimal_time> and <maximal_time> seconds
12
13
 
13
14
  Examples:
14
- | rate_limit | rate_interval | minimal_time | maximal_time |
15
- | 0 | 0 | 0 | 5 |
16
- | 1 | 2 | 1 | 20 |
17
- | 1 | 10 | 18 | 120 |
15
+ | jat_rate_limit_implementation | rate_limit_per_interval | rate_interval_in_seconds | minimal_time | maximal_time |
16
+ | | 0 | 0 | 0 | 5 |
17
+ | in_process | 1 | 2 | 1 | 20 |
18
+ | redis | 1 | 2 | 1 | 20 |
19
+ | redis | 1 | 10 | 18 | 120 |
20
+
21
+ Scenario: Unexpected rate limiting implementation generates an error
22
+ Given the following environment variables are set:
23
+ | name | value |
24
+ | JAT_RATE_LIMIT_IMPLEMENTATION | UNKNOWN IMPLEMENTATION |
25
+ When I run `jira-auto-tool --board-list`
26
+ Then it should fail with:
27
+ """
28
+ RuntimeError: "UNKNOWN IMPLEMENTATION": unexpected rate limiting implementation specified!
29
+ """
@@ -34,7 +34,7 @@ Feature: Add sprints using existing ones as reference
34
34
  | Food_Restaurant_25.3.4 | 2025-06-13 11:00:00 UTC | future |
35
35
 
36
36
  Scenario: Add several planning interval sprints
37
- When I successfully run `jira-auto-tool --sprint-add=25.2.2,3 --sprint-add=25.3.1,4 --sprint-add=25.4.1,5`
37
+ When I successfully run `jira-auto-tool --sa=25.2.2,3 --sa=25.3.1,4 --sa=25.4.1,5`
38
38
  Then afterwards the board only has the following sprints:
39
39
  | name | start_date | state |
40
40
  | Food_Supply_25.1.3 | 2025-02-01 11:00:00 UTC | closed |
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-limiter"
4
+
5
+ require_relative "../rate_limited_jira_client"
6
+
7
+ module Jira
8
+ module Auto
9
+ class Tool
10
+ class RateLimitedJiraClient
11
+ class InProcessBased < RateLimitedJiraClient
12
+ def rate_limit(&block)
13
+ rate_queue.shift
14
+
15
+ block.call
16
+ end
17
+
18
+ def rate_queue
19
+ @rate_queue ||=
20
+ Limiter::RateQueue.new(rate_limit_per_interval, interval: rate_interval_in_seconds)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rate_limited_jira_client"
4
+ require "ratelimit"
5
+ require "redis"
6
+
7
+ require "jira/auto/tool"
8
+
9
+ module Jira
10
+ module Auto
11
+ class Tool
12
+ class RateLimitedJiraClient
13
+ class RedisBased < RateLimitedJiraClient
14
+ def rate_limit(&block)
15
+ rate_limiter.exec_within_threshold(rate_limiter_key, interval: rate_interval_in_seconds,
16
+ threshold: rate_limit_per_interval) do
17
+ response = block.call
18
+
19
+ rate_limiter.add(rate_limiter_key)
20
+
21
+ response
22
+ end
23
+ end
24
+
25
+ def rate_limiter_key
26
+ "jira_auto_tool_api_requests"
27
+ end
28
+
29
+ def rate_limiter
30
+ self.class.rate_limiter(rate_limiter_key, rate_interval_in_seconds)
31
+ end
32
+
33
+ def self.rate_limiter(rate_limiter_key, rate_interval)
34
+ @rate_limiter ||= Ratelimit.new(rate_limiter_key, bucket_interval: rate_interval)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,48 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ratelimit"
4
- require "redis"
5
-
6
- require "jira/auto/tool"
7
-
8
3
  module Jira
9
4
  module Auto
10
5
  class Tool
11
6
  class RateLimitedJiraClient < JIRA::Client
12
- NO_RATE_LIMIT_IN_SECONDS = 0
7
+ require_relative "rate_limited_jira_client/in_process_based"
8
+ require_relative "rate_limited_jira_client/redis_based"
9
+
10
+ def self.implementation_class_for(tool)
11
+ requested_implementation = tool.jat_rate_limit_implementation_when_defined_else nil
12
+
13
+ case requested_implementation
14
+ when "in_process", "", nil
15
+ InProcessBased
16
+ when "redis"
17
+ RedisBased
18
+ else
19
+ raise %(#{requested_implementation.inspect}: unexpected rate limiting implementation specified!")
20
+ end
21
+ end
22
+
23
+ NO_RATE_LIMIT_PER_INTERVAL = 0
13
24
  NO_RATE_INTERVAL_IN_SECONDS = 0
14
25
 
15
- attr_reader :rate_interval, :rate_limit
26
+ attr_reader :rate_interval_in_seconds, :rate_limit_per_interval
16
27
 
17
- def initialize(options, rate_interval: 1, rate_limit: 1)
28
+ def initialize(options, rate_interval_in_seconds: 1, rate_limit_per_interval: 1)
18
29
  super(options)
19
- @rate_interval = rate_interval
20
- @rate_limit = rate_limit
30
+ @rate_interval_in_seconds = rate_interval_in_seconds
31
+ @rate_limit_per_interval = rate_limit_per_interval
21
32
  end
22
33
 
23
34
  alias original_request request
24
- def request(*args)
25
- return original_request(*args) if rate_limit == NO_RATE_LIMIT_IN_SECONDS
26
-
27
- rate_limiter.exec_within_threshold(rate_limiter_key, interval: rate_interval, threshold: rate_limit) do
28
- response = original_request(*args)
29
35
 
30
- rate_limiter.add(rate_limiter_key)
31
-
32
- response
36
+ def request(*args)
37
+ if rate_limit_per_interval == NO_RATE_LIMIT_PER_INTERVAL
38
+ original_request(*args)
39
+ else
40
+ rate_limit { original_request(*args) }
33
41
  end
34
42
  end
35
43
 
36
- def rate_limiter_key
37
- "jira_auto_tool_api_requests"
38
- end
39
-
40
- def rate_limiter
41
- self.class.rate_limiter(rate_limiter_key, rate_interval)
42
- end
43
-
44
- def self.rate_limiter(rate_limiter_key, rate_interval)
45
- @rate_limiter ||= Ratelimit.new(rate_limiter_key, bucket_interval: rate_interval)
44
+ def rate_limit(&)
45
+ raise "rate_limit must be implemented by a subclass"
46
46
  end
47
47
  end
48
48
  end
@@ -3,7 +3,7 @@
3
3
  module Jira
4
4
  module Auto
5
5
  class Tool
6
- VERSION = "1.1.5"
6
+ VERSION = "1.2.0"
7
7
  end
8
8
  end
9
9
  end
@@ -83,15 +83,17 @@ module Jira
83
83
  end
84
84
 
85
85
  def jira_client
86
- RateLimitedJiraClient.new(jira_client_options,
87
- rate_interval:
88
- jat_rate_interval_in_seconds_when_defined_else(
89
- RateLimitedJiraClient::NO_RATE_INTERVAL_IN_SECONDS
90
- ).to_i,
91
- rate_limit:
92
- jat_rate_limit_in_seconds_when_defined_else(
93
- RateLimitedJiraClient::NO_RATE_LIMIT_IN_SECONDS
94
- ).to_i)
86
+ RateLimitedJiraClient
87
+ .implementation_class_for(self)
88
+ .new(jira_client_options,
89
+ rate_interval_in_seconds:
90
+ jat_rate_interval_in_seconds_when_defined_else(
91
+ RateLimitedJiraClient::RedisBased::NO_RATE_INTERVAL_IN_SECONDS
92
+ ).to_i,
93
+ rate_limit_per_interval:
94
+ jat_rate_limit_per_interval_when_defined_else(
95
+ RateLimitedJiraClient::RedisBased::NO_RATE_LIMIT_PER_INTERVAL
96
+ ).to_i)
95
97
  end
96
98
 
97
99
  def jira_client_options
@@ -105,6 +107,7 @@ module Jira
105
107
  }
106
108
  end
107
109
 
110
+ # TODO: fix this overly complex logic
108
111
  def jira_http_debug?
109
112
  value = if config.key?(:jira_http_debug)
110
113
  config[:jira_http_debug]
@@ -136,12 +139,13 @@ module Jira
136
139
  jira_base_url + url
137
140
  end
138
141
 
139
- %i[
142
+ ENVIRONMENT_BASED_VALUE_SYMBOLS = %i[
140
143
  art_sprint_regex
141
144
  expected_start_date_field_name
142
145
  implementation_team_field_name
143
- jat_rate_limit_in_seconds
144
146
  jat_rate_interval_in_seconds
147
+ jat_rate_limit_implementation
148
+ jat_rate_limit_per_interval
145
149
  jat_tickets_for_team_sprint_ticket_dispatcher_jql
146
150
  jira_api_token
147
151
  jira_board_name
@@ -150,9 +154,11 @@ module Jira
150
154
  jira_http_debug
151
155
  jira_project_key
152
156
  jira_site_url
153
- jira_username
154
157
  jira_sprint_field_name
155
- ].each do |method_name|
158
+ jira_username
159
+ ].freeze
160
+
161
+ ENVIRONMENT_BASED_VALUE_SYMBOLS.each do |method_name|
156
162
  define_overridable_environment_based_value(method_name)
157
163
  end
158
164
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool/rate_limited_jira_client/in_process_based"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class RateLimitedJiraClient
9
+ class InProcessBased
10
+ RSpec.describe InProcessBased do
11
+ def build_client
12
+ described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:)
13
+ end
14
+
15
+ let(:client) { build_client }
16
+
17
+ let(:rate_interval_in_seconds) { 2 }
18
+ let(:rate_limit_per_interval) { 1 }
19
+
20
+ describe "#rate_limit" do
21
+ let(:rate_queue) { instance_double(Limiter::RateQueue) }
22
+
23
+ it "properly initializes the rate queue" do
24
+ allow(Limiter::RateQueue)
25
+ .to receive(:new).with(rate_limit_per_interval, interval: rate_interval_in_seconds)
26
+ .and_return(rate_queue)
27
+
28
+ allow(rate_queue).to receive(:shift)
29
+
30
+ expect(client.rate_limit { :do_nothing }).to eq(:do_nothing)
31
+ end
32
+
33
+ context "when rate limiting multiple requests" do
34
+ let(:rate_limit_4_calls_to_original_request_code) do
35
+ 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
36
+ end
37
+
38
+ before do
39
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
40
+ allow(client).to receive_messages(rate_queue: rate_queue)
41
+ allow(rate_queue).to receive(:shift)
42
+ end
43
+
44
+ it "shifts the queue and performs the request call" do
45
+ rate_limit_4_calls_to_original_request_code
46
+
47
+ expect(rate_queue).to have_received(:shift).exactly(4).times
48
+ expect(client).to have_received(:original_request).exactly(4).times
49
+ end
50
+ end
51
+
52
+ describe "#rate_queue" do
53
+ let(:another_client) { build_client }
54
+
55
+ it "creating a second client will return another queue" do
56
+ expect(another_client.rate_queue).not_to equal(client.rate_queue)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool/rate_limited_jira_client/redis_based"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class RateLimitedJiraClient
9
+ class RedisBased
10
+ RSpec.describe RedisBased do
11
+ describe "#rate_limit" do
12
+ let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
13
+
14
+ let(:rate_interval_in_seconds) { 2 }
15
+ let(:rate_limit_per_interval) { 1 }
16
+ let(:rate_limiter) { instance_double(Ratelimit) }
17
+
18
+ let(:rate_limit_4_calls_to_original_request_code) do
19
+ 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
20
+ end
21
+
22
+ before do
23
+ allow(described_class).to receive_messages(rate_limiter: rate_limiter)
24
+ allow(client).to receive(:original_request)
25
+ end
26
+
27
+ it "uses :exec_within_threshold to control rate limiting" do
28
+ allow(rate_limiter).to receive(:exec_within_threshold)
29
+
30
+ rate_limit_4_calls_to_original_request_code
31
+
32
+ expect(rate_limiter)
33
+ .to have_received(:exec_within_threshold)
34
+ .with("jira_auto_tool_api_requests", { interval: rate_interval_in_seconds,
35
+ threshold: rate_limit_per_interval })
36
+ .exactly(4).times
37
+ end
38
+
39
+ it "adds keeps track of the rate limiter key calls" do
40
+ allow(rate_limiter).to receive(:exec_within_threshold).and_yield
41
+ allow(rate_limiter).to receive(:add)
42
+
43
+ rate_limit_4_calls_to_original_request_code
44
+
45
+ expect(rate_limiter)
46
+ .to have_received(:add)
47
+ .with("jira_auto_tool_api_requests")
48
+ .exactly(4).times
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,78 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jira/auto/tool/rate_limited_jira_client"
3
+ require "rspec"
4
4
 
5
5
  module Jira
6
6
  module Auto
7
7
  class Tool
8
- class RateLimitedJiraClient
9
- RSpec.describe RateLimitedJiraClient do
10
- describe "#request" do
11
- let(:client) { described_class.new({}, rate_interval:, rate_limit:) }
12
- let(:rate_interval) { 2 }
13
- let(:rate_limit) { 1 }
14
- let(:oauth_client) { instance_double(JIRA::OauthClient, request: nil, consumer: nil) }
15
- let(:rate_limiter) { instance_double(Ratelimit) }
8
+ RSpec.describe RateLimitedJiraClient do
9
+ describe ".implementation_class_for" do
10
+ let(:result) { described_class.implementation_class_for(tool) }
11
+ let(:tool) { instance_double(Tool) }
16
12
 
13
+ context "when the rate limiting implementation is unspecified" do
17
14
  before do
18
- allow(described_class).to receive_messages(rate_limiter: rate_limiter)
19
-
20
- allow(JIRA::OauthClient).to receive_messages(new: oauth_client)
15
+ allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: nil)
16
+ end
21
17
 
22
- allow(client).to receive_messages(original_request: :response)
18
+ it { expect(result).to eq(RateLimitedJiraClient::InProcessBased) }
19
+ end
23
20
 
24
- allow(rate_limiter).to receive_messages(add: nil)
25
- allow(rate_limiter).to receive(:exec_within_threshold).and_yield
21
+ context "when using in process based rate limiting" do
22
+ before do
23
+ allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: "in_process")
26
24
  end
27
25
 
28
- it "returns the response" do
29
- expect(client.request(:get, "/path/to/resource")).to eq(:response)
26
+ it { expect(result).to eq(RateLimitedJiraClient::InProcessBased) }
27
+ end
28
+
29
+ context "when using in Redis based rate limiting" do
30
+ before do
31
+ allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: "redis")
30
32
  end
31
33
 
32
- it "calls the original request method" do
33
- client.request(:get, "/path/to/resource")
34
+ it { expect(result).to eq(RateLimitedJiraClient::RedisBased) }
35
+ end
36
+
37
+ context "when the request implementation is unexpected" do
38
+ before do
39
+ allow(tool)
40
+ .to receive_messages(jat_rate_limit_implementation_when_defined_else: "unexpected_implementation")
41
+ end
34
42
 
35
- expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
43
+ it do
44
+ expect { result }
45
+ .to raise_error(RuntimeError,
46
+ %("unexpected_implementation": unexpected rate limiting implementation specified!"))
36
47
  end
48
+ end
49
+ end
37
50
 
38
- context "when it leverages the rate limiter" do
39
- it "uses :exec_within_threshold to control rate limiting" do
40
- allow(rate_limiter).to receive_messages(exec_within_threshold: nil)
51
+ RSpec.shared_examples "a rate limited client" do
52
+ before do
53
+ allow(client).to receive_messages(original_request: :response)
54
+ end
41
55
 
42
- 4.times { client.request(:get, "/path/to/resource") }
56
+ it "returns the response" do
57
+ expect(client.request(:get, "/path/to/resource")).to eq(:response)
58
+ end
43
59
 
44
- expect(rate_limiter)
45
- .to have_received(:exec_within_threshold)
46
- .with("jira_auto_tool_api_requests", { interval: rate_interval, threshold: rate_limit })
47
- .exactly(4).times
48
- end
60
+ it "calls the original request method" do
61
+ client.request(:get, "/path/to/resource")
49
62
 
50
- it "adds keeps track of the rate limiter key calls" do
51
- allow(rate_limiter).to receive_messages(add: nil)
63
+ expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
64
+ end
65
+ end
52
66
 
53
- 4.times { client.request(:get, "/path/to/resource") }
67
+ describe "#request" do
68
+ let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
54
69
 
55
- expect(rate_limiter)
56
- .to have_received(:add)
57
- .with("jira_auto_tool_api_requests")
58
- .exactly(4).times
59
- end
60
- end
70
+ context "when the rate limiter is not needed" do
71
+ let(:rate_interval_in_seconds) { 0 }
72
+ let(:rate_limit_per_interval) { 0 }
61
73
 
62
- context "when it does not leverage the rate limiter" do
63
- let(:rate_limit) { 0 }
74
+ it_behaves_like "a rate limited client"
64
75
 
65
- it "does not use :exec_within_threshold to control rate limiting" do
66
- client.request(:get, "/path/to/resource")
76
+ it "does not use the rate limiter" do
77
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
78
+ expect(client).not_to receive(:rate_limit)
67
79
 
68
- expect(rate_limiter).not_to have_received(:exec_within_threshold)
69
- end
80
+ client.request(:get, "/path/to/resource")
81
+ end
82
+ end
83
+
84
+ context "when the rate limiter is needed" do
85
+ let(:rate_interval_in_seconds) { 2 }
86
+ let(:rate_limit_per_interval) { 1 }
70
87
 
71
- it "calls the original request method" do
72
- client.request(:get, "/path/to/resource")
88
+ it_behaves_like "a rate limited client" do
89
+ before { allow(client).to receive(:rate_limit).and_yield }
90
+ end
91
+
92
+ it "uses the rate limiter" do
93
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
94
+ expect(client).to receive(:rate_limit).and_yield
73
95
 
74
- expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
75
- end
96
+ client.request(:get, "/path/to/resource")
76
97
  end
77
98
  end
78
99
  end
@@ -16,7 +16,10 @@ module Jira
16
16
  end
17
17
 
18
18
  describe "#board_controller" do
19
- before { allow(tool).to receive_messages(jira_client: instance_double(RateLimitedJiraClient)) }
19
+ before do
20
+ allow(tool)
21
+ .to receive_messages(jira_client: instance_double(RateLimitedJiraClient))
22
+ end
20
23
 
21
24
  it { expect(tool.board_controller).to be_a(BoardController) }
22
25
  end
@@ -118,7 +121,9 @@ module Jira
118
121
  end
119
122
 
120
123
  describe "#project" do
121
- let(:jira_client) { instance_double(RateLimitedJiraClient, Project: project_query) }
124
+ let(:jira_client) do
125
+ instance_double(RateLimitedJiraClient, Project: project_query)
126
+ end
122
127
  let(:jira_project) { instance_double(JIRA::Resource::Project) }
123
128
  let(:jira_project_key) { "JIRA_PROJECT_KEY" }
124
129
  let(:project_query) { double("project_query", find: jira_project) } # rubocop:disable RSpec/VerifiedDoubles
@@ -217,21 +222,7 @@ module Jira
217
222
  end
218
223
  end
219
224
 
220
- %i[
221
- expected_start_date_field_name
222
- implementation_team_field_name
223
- jat_tickets_for_team_sprint_ticket_dispatcher_jql
224
- jat_rate_limit_in_seconds
225
- jat_rate_interval_in_seconds
226
- jira_api_token
227
- jira_board_name
228
- jira_board_name_regex
229
- jira_context_path
230
- jira_http_debug
231
- jira_project_key
232
- jira_site_url jira_username
233
- jira_sprint_field_name
234
- ].each do |method_name|
225
+ described_class::ENVIRONMENT_BASED_VALUE_SYMBOLS.each do |method_name|
235
226
  describe "environment based values" do
236
227
  let(:object_with_overridable_value) { tool }
237
228
 
@@ -277,21 +268,24 @@ module Jira
277
268
  }
278
269
  end
279
270
 
271
+ let(:expected_jira_client) { instance_double(RateLimitedJiraClient) }
272
+
280
273
  before do
281
274
  allow(tool)
282
275
  .to receive_messages(jira_username: "jira_username_value", jira_site_url: "https://jira_site_url_value",
283
276
  jira_api_token: "jira_api_token_value",
284
277
  jira_context_path_when_defined_else: "/context_path_value",
285
278
  jira_http_debug?: false,
286
- jat_rate_limit_in_seconds_when_defined_else: "10",
279
+ jat_rate_limit_per_interval_when_defined_else: "10",
287
280
  jat_rate_interval_in_seconds_when_defined_else: "60")
288
281
  end
289
282
 
290
283
  it "has a jira client" do
291
- expected_jira_client = instance_double(RateLimitedJiraClient)
292
-
293
284
  allow(RateLimitedJiraClient)
294
- .to receive(:new).with(client_options, rate_limit: 10, rate_interval: 60)
285
+ .to receive(:implementation_class_for).with(tool).and_return(RateLimitedJiraClient::InProcessBased)
286
+
287
+ allow(RateLimitedJiraClient::InProcessBased)
288
+ .to receive(:new).with(client_options, rate_limit_per_interval: 10, rate_interval_in_seconds: 60)
295
289
  .and_return(expected_jira_client)
296
290
 
297
291
  expect(tool.jira_client).to equal(expected_jira_client)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jira-auto-tool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christophe Broult
@@ -163,6 +163,20 @@ dependencies:
163
163
  - - ">="
164
164
  - !ruby/object:Gem::Version
165
165
  version: '0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: ruby-limiter
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
166
180
  - !ruby/object:Gem::Dependency
167
181
  name: syslog
168
182
  requirement: !ruby/object:Gem::Requirement
@@ -276,6 +290,8 @@ files:
276
290
  - lib/jira/auto/tool/project/options.rb
277
291
  - lib/jira/auto/tool/project/ticket_fields.rb
278
292
  - lib/jira/auto/tool/rate_limited_jira_client.rb
293
+ - lib/jira/auto/tool/rate_limited_jira_client/in_process_based.rb
294
+ - lib/jira/auto/tool/rate_limited_jira_client/redis_based.rb
279
295
  - lib/jira/auto/tool/request_builder.rb
280
296
  - lib/jira/auto/tool/request_builder/field_context_fetcher.rb
281
297
  - lib/jira/auto/tool/request_builder/field_option_fetcher.rb
@@ -327,6 +343,8 @@ files:
327
343
  - spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb
328
344
  - spec/jira/auto/tool/project/ticket_fields_spec.rb
329
345
  - spec/jira/auto/tool/project_spec.rb
346
+ - spec/jira/auto/tool/rate_limited_jira_client/in_process_based_spec.rb
347
+ - spec/jira/auto/tool/rate_limited_jira_client/redis_based_spec.rb
330
348
  - spec/jira/auto/tool/rate_limited_jira_client_spec.rb
331
349
  - spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb
332
350
  - spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb