kafka_command 0.0.1

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.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +179 -0
  3. data/.env +1 -0
  4. data/.env.test +1 -0
  5. data/.gitignore +41 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +12 -0
  8. data/.ruby-version +1 -0
  9. data/Gemfile +17 -0
  10. data/Gemfile.lock +194 -0
  11. data/LICENSE +21 -0
  12. data/README.md +138 -0
  13. data/Rakefile +34 -0
  14. data/app/assets/config/manifest.js +3 -0
  15. data/app/assets/images/.keep +0 -0
  16. data/app/assets/images/kafka_command/cluster_view.png +0 -0
  17. data/app/assets/images/kafka_command/kafka.png +0 -0
  18. data/app/assets/images/kafka_command/topic_view.png +0 -0
  19. data/app/assets/javascripts/kafka_command/application.js +14 -0
  20. data/app/assets/stylesheets/kafka_command/application.css +27 -0
  21. data/app/assets/stylesheets/kafka_command/clusters.css +8 -0
  22. data/app/assets/stylesheets/kafka_command/topics.css +3 -0
  23. data/app/channels/application_cable/channel.rb +6 -0
  24. data/app/channels/application_cable/connection.rb +6 -0
  25. data/app/controllers/kafka_command/application_controller.rb +96 -0
  26. data/app/controllers/kafka_command/brokers_controller.rb +26 -0
  27. data/app/controllers/kafka_command/clusters_controller.rb +46 -0
  28. data/app/controllers/kafka_command/consumer_groups_controller.rb +44 -0
  29. data/app/controllers/kafka_command/topics_controller.rb +187 -0
  30. data/app/helpers/kafka_command/application_helper.rb +29 -0
  31. data/app/helpers/kafka_command/consumer_group_helper.rb +13 -0
  32. data/app/jobs/application_job.rb +6 -0
  33. data/app/mailers/application_mailer.rb +8 -0
  34. data/app/models/kafka_command/broker.rb +47 -0
  35. data/app/models/kafka_command/client.rb +102 -0
  36. data/app/models/kafka_command/cluster.rb +172 -0
  37. data/app/models/kafka_command/consumer_group.rb +142 -0
  38. data/app/models/kafka_command/consumer_group_partition.rb +23 -0
  39. data/app/models/kafka_command/group_member.rb +18 -0
  40. data/app/models/kafka_command/partition.rb +36 -0
  41. data/app/models/kafka_command/topic.rb +153 -0
  42. data/app/views/kafka_command/brokers/index.html.erb +38 -0
  43. data/app/views/kafka_command/clusters/_tabs.html.erb +9 -0
  44. data/app/views/kafka_command/clusters/index.html.erb +54 -0
  45. data/app/views/kafka_command/clusters/new.html.erb +115 -0
  46. data/app/views/kafka_command/configuration_error.html.erb +1 -0
  47. data/app/views/kafka_command/consumer_groups/index.html.erb +32 -0
  48. data/app/views/kafka_command/consumer_groups/show.html.erb +115 -0
  49. data/app/views/kafka_command/shared/_alert.html.erb +13 -0
  50. data/app/views/kafka_command/shared/_search_bar.html.erb +31 -0
  51. data/app/views/kafka_command/shared/_title.html.erb +6 -0
  52. data/app/views/kafka_command/topics/_form_fields.html.erb +49 -0
  53. data/app/views/kafka_command/topics/edit.html.erb +17 -0
  54. data/app/views/kafka_command/topics/index.html.erb +46 -0
  55. data/app/views/kafka_command/topics/new.html.erb +36 -0
  56. data/app/views/kafka_command/topics/show.html.erb +126 -0
  57. data/app/views/layouts/kafka_command/application.html.erb +50 -0
  58. data/bin/rails +16 -0
  59. data/config/initializers/kafka.rb +13 -0
  60. data/config/initializers/kafka_command.rb +11 -0
  61. data/config/routes.rb +11 -0
  62. data/docker-compose.yml +18 -0
  63. data/kafka_command.gemspec +27 -0
  64. data/lib/assets/.keep +0 -0
  65. data/lib/core_extensions/kafka/broker/attr_readers.rb +11 -0
  66. data/lib/core_extensions/kafka/broker_pool/attr_readers.rb +11 -0
  67. data/lib/core_extensions/kafka/client/attr_readers.rb +11 -0
  68. data/lib/core_extensions/kafka/cluster/attr_readers.rb +11 -0
  69. data/lib/core_extensions/kafka/protocol/metadata_response/partition_metadata/attr_readers.rb +15 -0
  70. data/lib/kafka_command/configuration.rb +150 -0
  71. data/lib/kafka_command/engine.rb +11 -0
  72. data/lib/kafka_command/errors.rb +6 -0
  73. data/lib/kafka_command/version.rb +5 -0
  74. data/lib/kafka_command.rb +13 -0
  75. data/lib/tasks/.keep +0 -0
  76. data/spec/dummy/Rakefile +6 -0
  77. data/spec/dummy/app/assets/config/manifest.js +4 -0
  78. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  79. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  80. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  81. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  82. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  83. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  84. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  85. data/spec/dummy/app/jobs/application_job.rb +2 -0
  86. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  87. data/spec/dummy/app/models/application_record.rb +3 -0
  88. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  89. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  90. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  91. data/spec/dummy/bin/bundle +3 -0
  92. data/spec/dummy/bin/rails +4 -0
  93. data/spec/dummy/bin/rake +4 -0
  94. data/spec/dummy/bin/setup +36 -0
  95. data/spec/dummy/bin/update +31 -0
  96. data/spec/dummy/bin/yarn +11 -0
  97. data/spec/dummy/config/application.rb +19 -0
  98. data/spec/dummy/config/boot.rb +5 -0
  99. data/spec/dummy/config/cable.yml +10 -0
  100. data/spec/dummy/config/database.yml +25 -0
  101. data/spec/dummy/config/environment.rb +5 -0
  102. data/spec/dummy/config/environments/development.rb +61 -0
  103. data/spec/dummy/config/environments/production.rb +94 -0
  104. data/spec/dummy/config/environments/test.rb +46 -0
  105. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  106. data/spec/dummy/config/initializers/assets.rb +14 -0
  107. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  108. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  109. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  110. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  111. data/spec/dummy/config/initializers/inflections.rb +16 -0
  112. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  113. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  114. data/spec/dummy/config/kafka_command.yml +18 -0
  115. data/spec/dummy/config/locales/en.yml +33 -0
  116. data/spec/dummy/config/puma.rb +34 -0
  117. data/spec/dummy/config/routes.rb +3 -0
  118. data/spec/dummy/config/spring.rb +6 -0
  119. data/spec/dummy/config/ssl/test_ca_cert +1 -0
  120. data/spec/dummy/config/ssl/test_client_cert +1 -0
  121. data/spec/dummy/config/ssl/test_client_cert_key +1 -0
  122. data/spec/dummy/config/storage.yml +34 -0
  123. data/spec/dummy/config.ru +5 -0
  124. data/spec/dummy/db/schema.rb +42 -0
  125. data/spec/dummy/db/test.sqlite3 +0 -0
  126. data/spec/dummy/log/development.log +0 -0
  127. data/spec/dummy/log/hey.log +0 -0
  128. data/spec/dummy/log/test.log +2227 -0
  129. data/spec/dummy/package.json +5 -0
  130. data/spec/dummy/public/404.html +67 -0
  131. data/spec/dummy/public/422.html +67 -0
  132. data/spec/dummy/public/500.html +66 -0
  133. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  134. data/spec/dummy/public/apple-touch-icon.png +0 -0
  135. data/spec/dummy/public/favicon.ico +0 -0
  136. data/spec/examples.txt +165 -0
  137. data/spec/fast_helper.rb +20 -0
  138. data/spec/fixtures/files/kafka_command_sasl.yml +10 -0
  139. data/spec/fixtures/files/kafka_command_ssl.yml +10 -0
  140. data/spec/fixtures/files/kafka_command_ssl_file_paths.yml +11 -0
  141. data/spec/fixtures/files/kafka_command_staging.yml +8 -0
  142. data/spec/lib/kafka_command/configuration_spec.rb +311 -0
  143. data/spec/models/kafka_command/broker_spec.rb +83 -0
  144. data/spec/models/kafka_command/client_spec.rb +306 -0
  145. data/spec/models/kafka_command/cluster_spec.rb +163 -0
  146. data/spec/models/kafka_command/consumer_group_partition_spec.rb +43 -0
  147. data/spec/models/kafka_command/consumer_group_spec.rb +236 -0
  148. data/spec/models/kafka_command/partition_spec.rb +95 -0
  149. data/spec/models/kafka_command/topic_spec.rb +311 -0
  150. data/spec/rails_helper.rb +63 -0
  151. data/spec/requests/json/brokers_spec.rb +50 -0
  152. data/spec/requests/json/clusters_spec.rb +58 -0
  153. data/spec/requests/json/consumer_groups_spec.rb +139 -0
  154. data/spec/requests/json/topics_spec.rb +274 -0
  155. data/spec/spec_helper.rb +109 -0
  156. data/spec/support/factory_bot.rb +5 -0
  157. data/spec/support/json_helper.rb +13 -0
  158. data/spec/support/kafka_helper.rb +93 -0
  159. metadata +326 -0
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe KafkaCommand::Topic do
4
+ let(:topic_name) { "test-#{SecureRandom.hex(12)}" }
5
+ let(:num_partitions) { 5 }
6
+ let(:replication_factor) { 1 }
7
+ let(:topic_creation_kwargs) do
8
+ {
9
+ num_partitions: num_partitions, replication_factor: replication_factor
10
+ }
11
+ end
12
+
13
+ let(:topic) do
14
+ KafkaCommand::Cluster.all.first.topics.find { |t| t.name == topic_name }
15
+ end
16
+
17
+ before { create_topic(topic_name, **topic_creation_kwargs) }
18
+
19
+ describe '#new' do
20
+ it 'initializes Kafka::Topic with name, replication_factor, and partitions' do
21
+ expect(topic.name).to eq(topic_name)
22
+ expect(topic.replication_factor).to eq(1)
23
+ expect(topic.partitions.count).to eq(5)
24
+ expect(topic.partitions.first).to be_an_instance_of(KafkaCommand::Partition)
25
+ end
26
+ end
27
+
28
+ describe '#destroy' do
29
+ it 'deletes the topic' do
30
+ topic.destroy
31
+ expect(topic_exists?(topic_name)).to eq(false)
32
+ end
33
+
34
+ context 'consumer offsets topic' do
35
+ before do
36
+ allow(topic).to receive(:name).and_return(described_class::CONSUMER_OFFSET_TOPIC)
37
+ end
38
+
39
+ it 'raises' do
40
+ expect { topic.destroy }.to raise_error(described_class::DeletionError)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#set_partitions!' do
46
+ describe 'api not supported' do
47
+ before do
48
+ allow_any_instance_of(KafkaCommand::Client).to receive(:supports_api?).and_return(false)
49
+ end
50
+
51
+ it 'raises' do
52
+ expect do
53
+ topic.set_partitions!(num_partitions + 1)
54
+ end.to raise_error(KafkaCommand::UnsupportedApiError)
55
+ end
56
+ end
57
+
58
+ describe 'num_partitions > current num_partitions' do
59
+ it 'increases partitions' do
60
+ topic.set_partitions!(num_partitions + 1)
61
+ sleep_if_necessary
62
+ topic.refresh!
63
+ expect(topic.partitions.count).to eq(num_partitions + 1)
64
+ expect(partitions_for(topic_name)).to eq(num_partitions + 1)
65
+ end
66
+ end
67
+
68
+ describe 'num_partitions == current num_partitions' do
69
+ it 'raises an error and stays the same' do
70
+ expect { topic.set_partitions!(num_partitions) }.to raise_error(Kafka::InvalidPartitions)
71
+ expect(topic.partitions.count).to eq(num_partitions)
72
+ expect(partitions_for(topic_name)).to eq(num_partitions)
73
+ end
74
+ end
75
+
76
+ describe 'num_partitions < current num_partitions' do
77
+ it 'raises an error and stays the same' do
78
+ expect { topic.set_partitions!(num_partitions - 1) }.to raise_error(Kafka::InvalidPartitions)
79
+ expect(topic.partitions.count).to eq(num_partitions)
80
+ expect(partitions_for(topic_name)).to eq(num_partitions)
81
+ end
82
+ end
83
+ end
84
+
85
+ describe '#set_configs!' do
86
+ context 'max message bytes' do
87
+ describe 'greater than 0' do
88
+ let(:max_message_bytes) { 1024 }
89
+
90
+ it 'works' do
91
+ topic.set_configs!(max_message_bytes: max_message_bytes)
92
+ expect(topic.max_message_bytes).to eq(max_message_bytes)
93
+ end
94
+ end
95
+
96
+ describe 'is 0' do
97
+ let(:max_message_bytes) { 0 }
98
+
99
+ it 'works' do
100
+ topic.set_configs!(max_message_bytes: max_message_bytes)
101
+ expect(topic.max_message_bytes).to eq(max_message_bytes)
102
+ end
103
+ end
104
+
105
+ describe 'less than 0' do
106
+ let(:max_message_bytes) { -1 }
107
+
108
+ it 'errors' do
109
+ expect { topic.set_configs!(max_message_bytes: max_message_bytes) }.to raise_error(Kafka::InvalidRequest)
110
+ end
111
+ end
112
+ end
113
+
114
+ context 'retention ms' do
115
+ describe 'greater than 0' do
116
+ let(:retention_ms) { 102400000 }
117
+
118
+ it 'works' do
119
+ topic.set_configs!(retention_ms: retention_ms)
120
+ expect(topic.retention_ms).to eq(retention_ms)
121
+ end
122
+ end
123
+
124
+ describe 'equal to 0' do
125
+ let(:retention_ms) { 0 }
126
+
127
+ it 'works' do
128
+ topic.set_configs!(retention_ms: retention_ms)
129
+ expect(topic.retention_ms).to eq(retention_ms)
130
+ end
131
+ end
132
+
133
+ describe 'less than 0' do
134
+ let(:retention_ms) { -1 }
135
+
136
+ it 'works' do
137
+ topic.set_configs!(retention_ms: retention_ms)
138
+ expect(topic.retention_ms).to eq(retention_ms)
139
+ end
140
+ end
141
+ end
142
+
143
+ context 'retention bytes' do
144
+ describe 'greater than 0' do
145
+ let(:retention_bytes) { 102400000 }
146
+
147
+ it 'works' do
148
+ topic.set_configs!(retention_bytes: retention_bytes)
149
+ expect(topic.retention_bytes).to eq(retention_bytes)
150
+ end
151
+ end
152
+
153
+ describe 'equal to 0' do
154
+ let(:retention_bytes) { 0 }
155
+
156
+ it 'works' do
157
+ topic.set_configs!(retention_bytes: retention_bytes)
158
+ expect(topic.retention_bytes).to eq(retention_bytes)
159
+ end
160
+ end
161
+
162
+ describe 'less than 0' do
163
+ let(:retention_bytes) { -1 }
164
+
165
+ it 'works' do
166
+ topic.set_configs!(retention_bytes: retention_bytes)
167
+ expect(topic.retention_bytes).to eq(retention_bytes)
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#refresh!' do
174
+ it 'reloads the topic metadata' do
175
+ create_partitions_for(topic_name, num_partitions: num_partitions + 1)
176
+ topic.refresh!
177
+ expect(topic.partitions.count).to eq(num_partitions + 1)
178
+ end
179
+ end
180
+
181
+ describe '#offset_for' do
182
+ let(:partition) { double(partition_id: 0) }
183
+ let(:offset) { topic.offset_for(partition) }
184
+
185
+ describe 'no messsages' do
186
+ it 'returns 0' do
187
+ expect(offset).to eq(0)
188
+ end
189
+ end
190
+
191
+ describe 'messages' do
192
+ let(:num_messages) { 2 }
193
+ before do
194
+ num_messages.times { deliver_message('test', topic: topic_name, partition: partition.partition_id) }
195
+ end
196
+
197
+ it 'returns the latest offset' do
198
+ expect(offset).to eq(num_messages)
199
+ end
200
+ end
201
+ end
202
+
203
+ describe '#as_json' do
204
+ let(:partition_json) { topic.partitions.sort_by(&:partition_id).map(&:as_json) }
205
+
206
+ context 'including the config' do
207
+ let(:expected_json) do
208
+ {
209
+ name: topic_name,
210
+ replication_factor: replication_factor,
211
+ partitions: partition_json,
212
+ config: {
213
+ max_message_bytes: described_class::DEFAULT_MAX_MESSAGE_BYTES,
214
+ retention_ms: described_class::DEFAULT_RETENTION_MS,
215
+ retention_bytes: described_class::DEFAULT_RETENTION_BYTES
216
+ },
217
+ }.to_json
218
+ end
219
+
220
+ it 'equals the expected payload' do
221
+ expect(topic.as_json(include_config: true).to_json).to eq(expected_json)
222
+ end
223
+ end
224
+
225
+ context 'not including the config' do
226
+ let(:expected_json) do
227
+ {
228
+ name: topic_name,
229
+ replication_factor: replication_factor,
230
+ partitions: partition_json
231
+ }.to_json
232
+ end
233
+
234
+ it 'equals the expected payload' do
235
+ expect(topic.as_json.to_json).to eq(expected_json)
236
+ end
237
+ end
238
+ end
239
+
240
+ describe '#brokers_spread' do
241
+ let(:broker_doubles) do
242
+ [
243
+ double(:broker),
244
+ double(:broker),
245
+ double(:broker),
246
+ double(:broker),
247
+ double(:broker)
248
+ ]
249
+ end
250
+
251
+ before do
252
+ allow(topic).to receive(:replication_factor).and_return(replication_factor_double)
253
+ allow_any_instance_of(KafkaCommand::Client).to receive(:brokers).and_return(broker_doubles)
254
+ end
255
+
256
+ context '100%' do
257
+ let(:replication_factor_double) { broker_doubles.count }
258
+
259
+ it 'returns 100' do
260
+ expect(topic.brokers_spread).to eq(100)
261
+ end
262
+ end
263
+
264
+ context 'less than 20%' do
265
+ let(:replication_factor_double) { 1 }
266
+
267
+ it 'returns 20' do
268
+ expect(topic.brokers_spread).to eq(20)
269
+ end
270
+ end
271
+ end
272
+
273
+ describe '#groups' do
274
+ let(:group_id_1) { "test-group-#{SecureRandom.hex(12)}" }
275
+
276
+ context 'group consuming' do
277
+ it 'returns a list containing the group' do
278
+ run_consumer_group(topic_name, group_id_1) do
279
+ expect(topic.groups.map(&:group_id)).to include(group_id_1)
280
+ end
281
+ end
282
+ end
283
+
284
+ context 'group not consuming' do
285
+ it 'returns a list not containing the group' do
286
+ run_consumer_group(topic_name, group_id_1)
287
+ expect(topic.groups.map(&:group_id)).to_not include(group_id_1)
288
+ end
289
+ end
290
+ end
291
+
292
+ describe '#consumer_offset_topic?' do
293
+ context 'when consumer offset topic' do
294
+ let(:consumer_offset_topic) { described_class::CONSUMER_OFFSET_TOPIC }
295
+
296
+ before do
297
+ allow(topic).to receive(:name).and_return(consumer_offset_topic)
298
+ end
299
+
300
+ it 'returns true' do
301
+ expect(topic.consumer_offset_topic?).to eq(true)
302
+ end
303
+ end
304
+
305
+ context 'normal topic' do
306
+ it 'returns false' do
307
+ expect(topic.consumer_offset_topic?).to eq(false)
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
4
+ require 'spec_helper'
5
+ require 'support/kafka_helper'
6
+ ENV['RAILS_ENV'] ||= 'test'
7
+ require File.expand_path('../dummy/config/environment', __FILE__)
8
+ # Prevent database truncation if the environment is production
9
+ abort('The Rails environment is running in production mode!') if Rails.env.production?
10
+ Dotenv.load
11
+ require 'rspec/rails'
12
+ # Add additional requires below this line. Rails is not loaded until this point!
13
+ require 'support/json_helper'
14
+
15
+ # Requires supporting ruby files with custom matchers and macros, etc, in
16
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
17
+ # run as spec files by default. This means that files in spec/support that end
18
+ # in _spec.rb will both be required and run as specs, causing the specs to be
19
+ # run twice. It is recommended that you do not name files matching this glob to
20
+ # end with _spec.rb. You can configure this pattern with the --pattern
21
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
22
+ #
23
+ # The following line is provided for convenience purposes. It has the downside
24
+ # of increasing the boot-up time by auto-requiring all files in the support
25
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
26
+ # require only the support files necessary.
27
+ #
28
+ # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
29
+
30
+ # Checks for pending migrations and applies them before tests are run.
31
+ # If you are not using ActiveRecord, you can remove this line.
32
+ ActiveRecord::Migration.maintain_test_schema!
33
+
34
+ RSpec.configure do |config|
35
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
36
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
37
+
38
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
39
+ # examples within a transaction, remove the following line or assign false
40
+ # instead of true.
41
+ config.use_transactional_fixtures = true
42
+
43
+ # RSpec Rails can automatically mix in different behaviours to your tests
44
+ # based on their file location, for example enabling you to call `get` and
45
+ # `post` in specs under `spec/controllers`.
46
+ #
47
+ # You can disable this behaviour by removing the line below, and instead
48
+ # explicitly tag your specs with their type, e.g.:
49
+ #
50
+ # RSpec.describe UsersController, :type => :controller do
51
+ # # ...
52
+ # end
53
+ #
54
+ # The different available types are documented in the features, such as in
55
+ # https://relishapp.com/rspec/rspec-rails/docs
56
+ config.infer_spec_type_from_file_location!
57
+
58
+ # Filter lines from Rails gems in backtraces.
59
+ config.filter_rails_from_backtrace!
60
+ # arbitrary gems may also be filtered via:
61
+ # config.filter_gems_from_backtrace("gem name")
62
+ #
63
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Brokers Api', type: :request do
6
+ let(:cluster) { KafkaCommand::Cluster.all.first }
7
+ let(:broker) { cluster.brokers.first }
8
+ let(:uri_base) { '/clusters' }
9
+
10
+ describe 'listing all brokers' do
11
+ let(:broker_obj) { double(:broker_obj, node_id: 0, host: 'localhost', port: '9092') }
12
+ let(:broker_obj_two) { double(:broker_obj, node_id: 1, host: 'localhost2', port: '9092') }
13
+ let(:broker) { KafkaCommand::Broker.new(broker_obj) }
14
+ let(:broker_two) { KafkaCommand::Broker.new(broker_obj_two) }
15
+
16
+ before do
17
+ allow_any_instance_of(KafkaCommand::Cluster).to receive(:brokers).and_return(
18
+ [
19
+ broker,
20
+ broker_two
21
+ ]
22
+ )
23
+ end
24
+
25
+ it 'lists' do
26
+ get "#{uri_base}/#{cluster.name}/brokers.json"
27
+ expect(response.status).to eq(200)
28
+ expect(json['data']).to be_an_instance_of(Array)
29
+ expect(json['data'].map { |d| d['id'] }).to eq([broker.node_id, broker_two.node_id])
30
+ end
31
+ end
32
+
33
+ describe 'showing a single broker' do
34
+ context 'broker exists' do
35
+ it 'shows' do
36
+ get "#{uri_base}/#{cluster.name}/brokers/#{broker.node_id}.json"
37
+ expect(response.status).to eq(200)
38
+ expect(json['id']).to eq(broker.node_id)
39
+ expect(json['host']).to eq("#{broker.host}:#{broker.port}")
40
+ end
41
+ end
42
+
43
+ context 'broker does not exist' do
44
+ it 'returns 404' do
45
+ get "#{uri_base}/#{cluster.name}/brokers/doesnotexist.json"
46
+ expect(response.status).to eq(404)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Clusters Api', type: :request do
6
+ let!(:cluster) { KafkaCommand::Cluster.all.first }
7
+
8
+ describe 'listing all clusters' do
9
+ let!(:cluster_two) { KafkaCommand::Cluster.new(seed_brokers: ['localhost:9092'], name: 'cluster_two') }
10
+
11
+ before do
12
+ allow(KafkaCommand::Cluster).to receive(:all).and_return([cluster, cluster_two])
13
+ end
14
+
15
+ it 'lists' do
16
+ get '/clusters.json'
17
+ expect(response.status).to eq(200)
18
+ expect(json['data']).to be_an_instance_of(Array)
19
+ expect(json['data'].map { |d| d['name'] }).to eq([cluster.name, cluster_two.name])
20
+ end
21
+
22
+ context 'filtering' do
23
+ it 'filters by name' do
24
+ get "/clusters.json?name=#{cluster.name}"
25
+ expect(response.status).to eq(200)
26
+ expect(json['data']).to be_an_instance_of(Array)
27
+ expect(json['data'].map { |d| d['name'] }).to include(cluster.name)
28
+ expect(json['data'].map { |d| d['name'] }).to_not include(cluster_two.name)
29
+ end
30
+
31
+ it 'filters by name' do
32
+ get '/clusters.json?name=unknown'
33
+ expect(response.status).to eq(200)
34
+ expect(json['data']).to be_an_instance_of(Array)
35
+ expect(json['data']).to be_empty
36
+ end
37
+ end
38
+ end
39
+
40
+ describe 'showing a single cluster' do
41
+ context 'cluster exists' do
42
+ it 'shows' do
43
+ get "/clusters/#{cluster.name}.json"
44
+ expect(response.status).to eq(200)
45
+ expect(json['name']).to eq(cluster.name)
46
+ expect(json['version']).to eq(cluster.version)
47
+ expect(json['description']).to eq(cluster.description)
48
+ end
49
+ end
50
+
51
+ context 'cluster does not exist' do
52
+ it 'returns 404' do
53
+ get '/clusters/doesnotexist.json'
54
+ expect(response.status).to eq(404)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Consumer Groups Api', type: :request do
6
+ let(:cluster) { KafkaCommand::Cluster.all.first }
7
+ let(:topic_name) { "test-#{SecureRandom.hex(12)}" }
8
+ let(:group_id_1) { "test-group-#{SecureRandom.hex(12)}" }
9
+ let(:group_id_2) { "test-group-#{SecureRandom.hex(12)}" }
10
+ let(:uri_base) { "/clusters/#{cluster.id}/" }
11
+ let(:expected_json_group_1_running) do
12
+ {
13
+ group_id: group_id_1,
14
+ state: 'Stable',
15
+ topics: [
16
+ {
17
+ name: topic_name,
18
+ partitions: [
19
+ {
20
+ partition_id: 0,
21
+ lag: nil,
22
+ offset: nil
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }.with_indifferent_access
28
+ end
29
+
30
+ let(:expected_json_group_1_empty) do
31
+ {
32
+ group_id: group_id_1,
33
+ topics: [],
34
+ state: 'Empty'
35
+ }.with_indifferent_access
36
+ end
37
+
38
+ let(:expected_json_group_2_empty) do
39
+ {
40
+ group_id: group_id_2,
41
+ topics: [],
42
+ state: 'Empty'
43
+ }.with_indifferent_access
44
+ end
45
+
46
+ before { create_topic(topic_name) }
47
+
48
+ describe 'listing all consumer groups' do
49
+ describe 'dormant' do
50
+ before do
51
+ run_consumer_group(topic_name, group_id_1)
52
+ run_consumer_group(topic_name, group_id_2)
53
+ end
54
+
55
+ it 'returns empty groups' do
56
+ get "#{uri_base}/consumer_groups.json"
57
+ expect(response.status).to eq(200)
58
+ expect(json['data']).to be_an_instance_of(Array)
59
+ expect(json['data']).to include(expected_json_group_1_empty)
60
+ expect(json['data']).to include(expected_json_group_2_empty)
61
+ end
62
+
63
+ context 'filtering' do
64
+ it 'filters by group id' do
65
+ get "#{uri_base}/consumer_groups.json?group_id=#{group_id_1}"
66
+ expect(response.status).to eq(200)
67
+ expect(json['data']).to be_an_instance_of(Array)
68
+ expect(json['data']).to include(expected_json_group_1_empty)
69
+ expect(json['data']).to_not include(expected_json_group_2_empty)
70
+ end
71
+
72
+ it 'filters by group id' do
73
+ get "#{uri_base}/consumer_groups.json?group_id=unknown"
74
+ expect(response.status).to eq(200)
75
+ expect(json['data']).to be_an_instance_of(Array)
76
+ expect(json['data']).to be_empty
77
+ end
78
+ end
79
+ end
80
+
81
+ describe 'running ' do
82
+ let(:expected_json) do
83
+ {
84
+ group_id: group_id_1,
85
+ state: 'Stable',
86
+ topics: [
87
+ {
88
+ name: topic_name,
89
+ partitions: [
90
+ {
91
+ partition_id: 0,
92
+ lag: nil,
93
+ offset: nil
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }.with_indifferent_access
99
+ end
100
+
101
+ it 'returns non empty groups' do
102
+ run_consumer_group(topic_name, group_id_1) do
103
+ get "#{uri_base}/consumer_groups.json"
104
+ expect(json['data']).to include(expected_json)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ describe 'showing a consumer group' do
111
+ context 'dormant' do
112
+ before { run_consumer_group(topic_name, group_id_1) }
113
+
114
+ it 'returns an empty group' do
115
+ get "#{uri_base}/consumer_groups/#{group_id_1}.json"
116
+ expect(response.status).to eq(200)
117
+ expect(json).to eq(expected_json_group_1_empty)
118
+ end
119
+ end
120
+
121
+ context 'running' do
122
+ it 'returns an empty group' do
123
+ run_consumer_group(topic_name, group_id_1) do
124
+ get "#{uri_base}/consumer_groups/#{group_id_1}.json"
125
+ expect(response.status).to eq(200)
126
+ expect(json).to eq(expected_json_group_1_running)
127
+ end
128
+ end
129
+ end
130
+
131
+ context 'not found' do
132
+ it 'returns 404' do
133
+ get "#{uri_base}/consumer_groups/doesnotexists.json"
134
+ expect(response.status).to eq(404)
135
+ expect(response.body).to eq('Consumer group not found')
136
+ end
137
+ end
138
+ end
139
+ end