From 89128b28ce0fe4147853b549df254c851a1e13f6 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Tue, 20 May 2025 17:32:04 +0200 Subject: [PATCH 1/5] [JAY-656] Rename Stats#response to #indices_stats This is being done because the index-related statistics are not the only statistics that the API can deliver. In an upcoming commit node-related statistics will be needed, then a single #response method will no longer be suitable. --- lib/jay_api/elasticsearch/stats.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/jay_api/elasticsearch/stats.rb b/lib/jay_api/elasticsearch/stats.rb index be978d4..4639562 100644 --- a/lib/jay_api/elasticsearch/stats.rb +++ b/lib/jay_api/elasticsearch/stats.rb @@ -21,16 +21,16 @@ def initialize(transport_client) # to the Statistics API endpoint fails. def indices # DO NOT MEMOIZE! Leave it to the caller. - ::JayAPI::Elasticsearch::Stats::Indices.new(response['indices']) + ::JayAPI::Elasticsearch::Stats::Indices.new(indices_stats['indices']) end private - # @return [Hash] The Hash with the statistics returned by the - # Elasticsearch cluster. + # @return [Hash] The Hash with the index-related statistics returned by + # the Elasticsearch cluster. # @raise [Elasticsearch::Transport::Transport::ServerError] If the # request fails. - def response + def indices_stats # DO NOT MEMOIZE! Leave it to the caller. transport_client.indices.stats end From 44d28adc6a37ffd08cff246ff7d9159c2526c7a9 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Tue, 20 May 2025 18:01:08 +0200 Subject: [PATCH 2/5] [JAY-656] Add Elasticsearch::Stats::Node::Storage The class holds information about the storage space available on one of the nodes that make up the Elasticsearch cluster. It will be used in an upcoming commit to provide the user access to said information. --- .../elasticsearch/stats/node/storage.rb | 55 ++++++++++++ .../elasticsearch/stats/node/storage_spec.rb | 90 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 lib/jay_api/elasticsearch/stats/node/storage.rb create mode 100644 spec/jay_api/elasticsearch/stats/node/storage_spec.rb diff --git a/lib/jay_api/elasticsearch/stats/node/storage.rb b/lib/jay_api/elasticsearch/stats/node/storage.rb new file mode 100644 index 0000000..2ed083d --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/node/storage.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + class Stats + class Node + # Holds storage information related to one of the nodes in the + # Elasticsearch cluster. + class Storage + TOTAL_KEY = 'total_in_bytes' + FREE_KEY = 'free_in_bytes' + AVAILABLE_KEY = 'available_in_bytes' + + # @param [Hash] data Data about the Node's storage. + def initialize(data) + @data = data + end + + # @return [Integer] The total size of the storage (in bytes) + def total + @total ||= data[TOTAL_KEY] + end + + # @return [Integer] The total number of bytes that are free on the + # node. + def free + @free ||= data[FREE_KEY] + end + + # @return [Integer] The total number of bytes that are available on + # the node. In general this is equal to #free, but not always. + def available + @available ||= data[AVAILABLE_KEY] + end + + # @return [self] A new instance of the class with the added storage of + # the receiver and +other+. + def +(other) + raise ArgumentError, "Cannot add #{self.class} and #{other.class} together" unless other.is_a?(self.class) + + self.class.new( + TOTAL_KEY => total + other.total, + FREE_KEY => free + other.free, + AVAILABLE_KEY => available + other.available + ) + end + + private + + attr_reader :data + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/stats/node/storage_spec.rb b/spec/jay_api/elasticsearch/stats/node/storage_spec.rb new file mode 100644 index 0000000..6b0f82c --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/node/storage_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/node/storage' + +RSpec.describe JayAPI::Elasticsearch::Stats::Node::Storage do + subject(:storage) { described_class.new(data) } + + let(:data) do + { + 'total_in_bytes' => 5_126_127_616, + 'free_in_bytes' => 4_576_702_464, + 'available_in_bytes' => 4_559_925_248 + } + end + + describe '#total' do + subject(:method_call) { storage.total } + + it 'returns the total number of bytes' do + expect(method_call).to eq(5_126_127_616) + end + end + + describe '#free' do + subject(:method_call) { storage.free } + + it 'returns the total number of free bytes' do + expect(method_call).to eq(4_576_702_464) + end + end + + describe '#available' do + subject(:method_call) { storage.available } + + it 'returns the total number of available bytes' do + expect(method_call).to eq(4_559_925_248) + end + end + + describe '#+' do + subject(:method_call) { storage + other } + + let(:other) do + described_class.new( + 'total_in_bytes' => 316_863_741_952, + 'free_in_bytes' => 25_923_690_496, + 'available_in_bytes' => 25_906_913_280 + ) + end + + context "when 'other' is not an instance of #{described_class}" do + let(:other) do + { + 'total_in_bytes' => 316_863_741_952, + 'free_in_bytes' => 25_923_690_496, + 'available_in_bytes' => 25_906_913_280 + } + end + + it 'raises an ArgumentError' do + expect { method_call }.to raise_error( + ArgumentError, + 'Cannot add JayAPI::Elasticsearch::Stats::Node::Storage and Hash together' + ) + end + end + + it 'does not return the receiver nor other' do + expect(method_call).not_to be(storage) + expect(method_call).not_to be(other) + method_call + end + + it "returns a new instance of #{described_class}" do + expect(method_call).to be_a(described_class) + end + + it "returns an instance of #{described_class} with the expected number of total bytes" do + expect(method_call.total).to eq(321_989_869_568) + end + + it "returns an instance of #{described_class} with the expected number of free bytes" do + expect(method_call.free).to eq(30_500_392_960) + end + + it "returns an instance of #{described_class} with the expected number of available bytes" do + expect(method_call.available).to eq(30_466_838_528) + end + end +end From 82bebbd200797818675fd3c1b1323fae55fda597 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Wed, 21 May 2025 13:35:51 +0200 Subject: [PATCH 3/5] [JAY-656] Add the Elasticsearch::Stats::Node class The class holds information about one of the nodes that make up the Elasticsearch cluster. For the moment it only provides the #storage method, which gives the caller access to storage-related information about the node. The class will be used in an upcoming commit to give the user access to said data through the Elasticsearch::Client class. --- .../stats/errors/stats_data_not_available.rb | 15 +++ lib/jay_api/elasticsearch/stats/node.rb | 45 +++++++++ spec/jay_api/elasticsearch/stats/node_spec.rb | 92 +++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 lib/jay_api/elasticsearch/stats/errors/stats_data_not_available.rb create mode 100644 lib/jay_api/elasticsearch/stats/node.rb create mode 100644 spec/jay_api/elasticsearch/stats/node_spec.rb diff --git a/lib/jay_api/elasticsearch/stats/errors/stats_data_not_available.rb b/lib/jay_api/elasticsearch/stats/errors/stats_data_not_available.rb new file mode 100644 index 0000000..841ec22 --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/errors/stats_data_not_available.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative '../../../errors/error' + +module JayAPI + module Elasticsearch + class Stats + module Errors + # An error to be raised when a particular Stats element is requested for + # which there is no data in the response received from the cluster. + class StatsDataNotAvailable < ::JayAPI::Errors::Error; end + end + end + end +end diff --git a/lib/jay_api/elasticsearch/stats/node.rb b/lib/jay_api/elasticsearch/stats/node.rb new file mode 100644 index 0000000..a9a42c7 --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/node.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative 'errors/stats_data_not_available' +require_relative 'node/storage' + +module JayAPI + module Elasticsearch + class Stats + # Holds information about one of the nodes in the Elasticsearch cluster. + class Node + attr_reader :name + + # @param [String] name The name of the node. + # @param [Hash] data Information about the node. + def initialize(name, data) + @name = name + @data = data + end + + # @return [JayAPI::Elasticsearch::Stats::Node::Storage] Storage + # information about the node. + # @raise [JayAPI::Elasticsearch::Stats::Errors::StatsDataNotAvailable] + # If there is no storage information for the node. + def storage + @storage ||= ::JayAPI::Elasticsearch::Stats::Node::Storage.new(fs_totals) + end + + private + + attr_reader :data + + # @return [Hash] Aggregated information about the +Node+'s + # filesystem. + # @raise [JayAPI::Elasticsearch::Stats::Errors::StatsDataNotAvailable] + # If there is no filesystem information for the node. + def fs_totals + @fs_totals ||= data.dig('fs', 'total') || raise( + ::JayAPI::Elasticsearch::Stats::Errors::StatsDataNotAvailable, + "Filesystem data not available for node #{name}" + ) + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/stats/node_spec.rb b/spec/jay_api/elasticsearch/stats/node_spec.rb new file mode 100644 index 0000000..eda083d --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/node_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/node' + +RSpec.describe JayAPI::Elasticsearch::Stats::Node do + subject(:node) { described_class.new(name, data) } + + let(:name) { 'Q9CAvRyRSBSBV3mxxbnPjQ' } + let(:data) { {} } + + describe '#storage' do + subject(:method_call) { node.storage } + + shared_examples_for '#storage when no storage-related data is available' do + it 'raises a StatsDataNotAvailable error' do + expect { method_call }.to raise_error( + JayAPI::Elasticsearch::Stats::Errors::StatsDataNotAvailable, + 'Filesystem data not available for node Q9CAvRyRSBSBV3mxxbnPjQ' + ) + end + end + + context "when the node data doesn't include filesystem data" do + let(:data) { {} } + + it_behaves_like '#storage when no storage-related data is available' + end + + context 'when the node data includes filesystem data' do + let(:fs_data) do + { + 'data' => [{ + 'type' => 'ext4', + 'total_in_bytes' => 316_863_741_952, + 'free_in_bytes' => 24_591_085_568, + 'available_in_bytes' => 24_574_308_352 + }] + } + end + + let(:data) do + { 'fs' => fs_data } + end + + context "when the node data doesn't include aggregated data about the filesystem" do + it_behaves_like '#storage when no storage-related data is available' + end + + context 'when the node data contains aggregated data about the filesystem' do + let(:fs_data) do + super().merge( + 'total' => { + 'total_in_bytes' => 316_863_741_952, + 'free_in_bytes' => 24_591_085_568, + 'available_in_bytes' => 24_574_308_352 + } + ) + end + + let(:expected_storage_data) do + { + 'total_in_bytes' => 316_863_741_952, + 'free_in_bytes' => 24_591_085_568, + 'available_in_bytes' => 24_574_308_352 + } + end + + let(:storage) do + instance_double( + JayAPI::Elasticsearch::Stats::Node::Storage + ) + end + + before do + allow(JayAPI::Elasticsearch::Stats::Node::Storage) + .to receive(:new).and_return(storage) + end + + it 'initializes the Storage instance from the aggregated data' do + expect(JayAPI::Elasticsearch::Stats::Node::Storage).to receive(:new) + .with(expected_storage_data) + + method_call + end + + it 'returns the Storage instance' do + expect(method_call).to be(storage) + end + end + end + end +end From c601619a1149f764181c66f5be3bc0a58f1ce0a2 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Wed, 21 May 2025 14:01:52 +0200 Subject: [PATCH 4/5] [JAY-656] Add Elasticsearch::Stats::Nodes class The class holds the information for each of the nodes that make up the Elasticsearch cluster. It acts as a collection of Node objects, one for each node. The class uses a lazy enumerator to avoid instantiating Node objects needlessly, they are only initialized when they are accessed. The class will be used in an upcoming commit to give the user access to node-related statistics through the Elasticsearch::Client class. --- lib/jay_api/elasticsearch/stats/nodes.rb | 46 +++++++++++++++ .../jay_api/elasticsearch/stats/nodes_spec.rb | 59 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 lib/jay_api/elasticsearch/stats/nodes.rb create mode 100644 spec/jay_api/elasticsearch/stats/nodes_spec.rb diff --git a/lib/jay_api/elasticsearch/stats/nodes.rb b/lib/jay_api/elasticsearch/stats/nodes.rb new file mode 100644 index 0000000..87d4e5f --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/nodes.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'forwardable' + +require_relative 'node' + +module JayAPI + module Elasticsearch + class Stats + # Provides access to the list of nodes returned by Elasticsearch's Stats API + class Nodes + extend Forwardable + + def_delegator :nodes, :size + + # @param [Hash] nodes Information about the nodes in the Elasticsearch + # cluster. + def initialize(nodes) + @nodes = nodes + end + + # @return [Enumerator::Lazy] A lazy + # enumerator of +Node+ objects, one for each of the nodes. + def all + @all ||= with_lazy_instantiation { nodes } + end + + private + + attr_reader :nodes + + def build_node(args) + ::JayAPI::Elasticsearch::Stats::Node.new(*args) + end + + # Calls the given block and turns its return value into a lazy + # enumerator that instantiates a +Node+ object for each of the + # elements of the collection returned by the block. + # @return [Enumerator::Lazy] + def with_lazy_instantiation(&block) + block.call.lazy.map(&method(:build_node)) + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/stats/nodes_spec.rb b/spec/jay_api/elasticsearch/stats/nodes_spec.rb new file mode 100644 index 0000000..2379971 --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/nodes_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/nodes' + +RSpec.describe JayAPI::Elasticsearch::Stats::Nodes do + subject(:nodes) { described_class.new(nodes_hash) } + + let(:nodes_hash) do + { + 'Q9CAvRyRSBSBV3mxxbnPjQ' => { + 'indices' => { 'docs_count' => 218_737_641 } + }, + 'ntd-0MYVRe-PnPSAH6jDDg' => { + 'indices' => { 'docs_count' => 197_437_505 } + }, + 'MVSLeteKR_aiLjccChNYpA' => { + 'indices' => { 'docs_count' => 0 } + } + } + end + + describe '#size' do + subject(:method_call) { nodes.size } + + it 'does not initialize any Node objets' do + expect(JayAPI::Elasticsearch::Stats::Node).not_to receive(:new) + method_call + end + + it 'returns the number of nodes in the given hash' do + expect(method_call).to eq(3) + end + end + + describe '#all' do + subject(:method_call) { nodes.all } + + let(:expected_nodes) do + %w[Q9CAvRyRSBSBV3mxxbnPjQ ntd-0MYVRe-PnPSAH6jDDg MVSLeteKR_aiLjccChNYpA] + end + + it 'returns an Enumerator::Lazy' do + expect(method_call).to be_a(Enumerator::Lazy) + end + + it 'includes the expected number of nodes' do + expect(method_call.size).to eq(3) + end + + it 'includes only instances of JayAPI::Elasticsearch::Stats::Node' do + expect(method_call).to all(be_a(JayAPI::Elasticsearch::Stats::Node)) + end + + it 'includes the expected list of indices' do + # #to_a is needed here because of the lazy enumerator. + expect(method_call.map(&:name).to_a).to eq(expected_nodes) + end + end +end From 04139af4598c5554b0f29aa2195ede571efac0be Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Tue, 20 May 2025 17:28:04 +0200 Subject: [PATCH 5/5] [JAY-656] Add the #nodes method to Elasticsearch::Stats The method returns an instance of Elasticsearch::Stats::Nodes, a class which allows the caller to access information about the nodes that make up the Elasticsearch cluster. --- CHANGELOG.md | 4 + .../user_guidelines/elasticsearch/stats.rst | 32 ++++ lib/jay_api/elasticsearch/stats.rb | 20 +++ spec/jay_api/elasticsearch/stats_spec.rb | 160 +++++++++++++----- 4 files changed, 177 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36903f6..5e72677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] +### Added +- The `#nodes` method to the `Elasticsearch::Stats` class. This method gives the + user access to the node-related statistics of the Elasticsearch cluster. + ## [28.1.0] - 2025-05-19 ### Added diff --git a/documentation/source/user_guidelines/elasticsearch/stats.rst b/documentation/source/user_guidelines/elasticsearch/stats.rst index 4dfe27a..3b4b473 100644 --- a/documentation/source/user_guidelines/elasticsearch/stats.rst +++ b/documentation/source/user_guidelines/elasticsearch/stats.rst @@ -45,3 +45,35 @@ The ``Stats::Index`` objects have the following methods: ``#name`` The name of the index. + +#nodes +------ + +This method gives you access to node-related statistics. The method returns an +instance of the ``Stats::Nodes`` class, which in turn allows you to access +information on each of the nodes that make up the Elasticsearch cluster through +the following methods: + +#size ++++++ + +This method returns the number of nodes in the cluster. + +#all +++++ + +This method returns an ``Enumerator`` whose elements are instances of the +``Stats::Node`` class, one for each of the nodes in the cluster. + +The ``Stats::Node`` class has the following methods: + +``#name`` + The name of the node. (Usually a random string of numbers and letters) + +``#storage`` + The method returns an instance of ``Stats::Node::Storage``, a class which + offers information about the storage of the node. ``Storage`` objects can be + added together to calculate the total storage of the cluster. + + The ``Storage`` classes provides three methods that return number of bytes: + ``#total``, ``#free`` and ``#available``. diff --git a/lib/jay_api/elasticsearch/stats.rb b/lib/jay_api/elasticsearch/stats.rb index 4639562..7de960d 100644 --- a/lib/jay_api/elasticsearch/stats.rb +++ b/lib/jay_api/elasticsearch/stats.rb @@ -2,6 +2,8 @@ require_relative 'stats/index' require_relative 'stats/indices' +require_relative 'stats/node' +require_relative 'stats/nodes' module JayAPI module Elasticsearch @@ -24,6 +26,15 @@ def indices ::JayAPI::Elasticsearch::Stats::Indices.new(indices_stats['indices']) end + # @return [JayAPI::Elasticsearch::Stats::Nodes] Information about the + # nodes that make up the Elasticsearch cluster. + # @raise [Elasticsearch::Transport::Transport::ServerError] If the request + # to the Statistics API endpoint fails. + def nodes + # DO NOT MEMOIZE! Leave it to the caller. + ::JayAPI::Elasticsearch::Stats::Nodes.new(nodes_stats['nodes']) + end + private # @return [Hash] The Hash with the index-related statistics returned by @@ -34,6 +45,15 @@ def indices_stats # DO NOT MEMOIZE! Leave it to the caller. transport_client.indices.stats end + + # @return [Hash] The Hash with the node-related statistics returned by the + # Elasticsearch cluster. + # @raise [Elasticsearch::Transport::Transport::ServerError] If the + # request fails. + def nodes_stats + # DO NOT MEMOIZE! Leave it to the caller. + transport_client.nodes.stats + end end end end diff --git a/spec/jay_api/elasticsearch/stats_spec.rb b/spec/jay_api/elasticsearch/stats_spec.rb index 742b44f..fcb2dbc 100644 --- a/spec/jay_api/elasticsearch/stats_spec.rb +++ b/spec/jay_api/elasticsearch/stats_spec.rb @@ -6,53 +6,52 @@ RSpec.describe JayAPI::Elasticsearch::Stats do subject(:stats) { described_class.new(transport_client) } - let(:stats_hash) do - { - '_all' => { - 'total' => { - 'store' => { - 'size_in_bytes' => 830_124_136, - 'reserved_in_bytes' => 0 - } - } - }, - 'indices' => { - 'xyz01_integration_test' => { - 'uuid' => 'OXRb8_IYTseG2epNa9Ls3g' - }, - 'xyz01_unit_tests' => { - 'uuid' => 'hxDdhi-3TFSndxhLesspFw' - }, - '.kibana_views' => { - 'uuid' => 'pr-VjrPARlG3lAoAfPqNog' - }, - 'xyz02_manual_verification' => { - 'uuid' => 'uaZ_kKQuSM-HaKH_LcI7BQ' - }, - '.backup' => { - 'uuid' => 'N7TZOstjRHu8mTwsLZuQ5w' - } - } - } - end - - let(:indices_client) do - instance_double( - Elasticsearch::API::Indices::IndicesClient, - stats: stats_hash - ) - end - let(:transport_client) do instance_double( - Elasticsearch::Transport::Client, - indices: indices_client + Elasticsearch::Transport::Client ) end describe '#indices' do subject(:method_call) { stats.indices } + let(:stats_hash) do + { + '_all' => { + 'total' => { + 'store' => { + 'size_in_bytes' => 830_124_136, + 'reserved_in_bytes' => 0 + } + } + }, + 'indices' => { + 'xyz01_integration_test' => { + 'uuid' => 'OXRb8_IYTseG2epNa9Ls3g' + }, + 'xyz01_unit_tests' => { + 'uuid' => 'hxDdhi-3TFSndxhLesspFw' + }, + '.kibana_views' => { + 'uuid' => 'pr-VjrPARlG3lAoAfPqNog' + }, + 'xyz02_manual_verification' => { + 'uuid' => 'uaZ_kKQuSM-HaKH_LcI7BQ' + }, + '.backup' => { + 'uuid' => 'N7TZOstjRHu8mTwsLZuQ5w' + } + } + } + end + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient, + stats: stats_hash + ) + end + let(:indices) do instance_double( JayAPI::Elasticsearch::Stats::Indices @@ -80,6 +79,7 @@ end before do + allow(transport_client).to receive(:indices).and_return(indices_client) allow(JayAPI::Elasticsearch::Stats::Indices).to receive(:new).and_return(indices) end @@ -117,4 +117,86 @@ expect(method_call).to be(indices) end end + + describe '#nodes' do + subject(:method_call) { stats.nodes } + + let(:stats_hash) do + { + '_nodes' => { 'total' => 6, 'successful' => 6, 'failed' => 0 }, + 'cluster_name' => '130592744203', + 'nodes' => { + 'Q9CAvRyRSBSBV3mxxbnPjQ' => { 'name' => '3be27c55fbd0bea8278c8e67f5e0dafa' }, + 'ntd-0MYVRe-PnPSAH6jDDg' => { 'name' => '7aa07fa030f7e9ac302e7898fd400ded' }, + 'MVSLeteKR_aiLjccChNYpA' => { 'name' => '443b4912dfc7c104afa8074e62246c22' }, + 'uUZyFyxuThaHzsjmFRTxXw' => { 'name' => '30512cbcb29eeba7b854ef4f4663f42d' }, + 'cNvDkFDWQ2miz5MvMmmVZg' => { 'name' => '9703b4532fa4f05e9fb305722bec0623' }, + '4Yk4GeazTE6gSId3OuOV1A' => { 'name' => 'ef6f30de3e7d26567960e0247026c2b8' } + } + } + end + + let(:nodes_client) do + instance_double( + Elasticsearch::API::Nodes::NodesClient, + stats: stats_hash + ) + end + + let(:nodes) do + instance_double( + JayAPI::Elasticsearch::Stats::Nodes + ) + end + + let(:expected_nodes_data) do + { + 'Q9CAvRyRSBSBV3mxxbnPjQ' => { 'name' => '3be27c55fbd0bea8278c8e67f5e0dafa' }, + 'ntd-0MYVRe-PnPSAH6jDDg' => { 'name' => '7aa07fa030f7e9ac302e7898fd400ded' }, + 'MVSLeteKR_aiLjccChNYpA' => { 'name' => '443b4912dfc7c104afa8074e62246c22' }, + 'uUZyFyxuThaHzsjmFRTxXw' => { 'name' => '30512cbcb29eeba7b854ef4f4663f42d' }, + 'cNvDkFDWQ2miz5MvMmmVZg' => { 'name' => '9703b4532fa4f05e9fb305722bec0623' }, + '4Yk4GeazTE6gSId3OuOV1A' => { 'name' => 'ef6f30de3e7d26567960e0247026c2b8' } + } + end + + before do + allow(transport_client).to receive(:nodes).and_return(nodes_client) + allow(JayAPI::Elasticsearch::Stats::Nodes).to receive(:new).and_return(nodes) + end + + it "requests the nodes' statistics from the Elasticsearch cluster" do + expect(transport_client).to receive(:nodes) + expect(nodes_client).to receive(:stats) + method_call + end + + context 'when the API request fails' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::Forbidden, + 'You do not have permission to perform this action' + ] + end + + before do + allow(nodes_client).to receive(:stats).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + it 'creates an instance of JayAPI::Elasticsearch::Stats::Nodes and passes the nodes data to it' do + expect(JayAPI::Elasticsearch::Stats::Nodes) + .to receive(:new).with(expected_nodes_data) + + method_call + end + + it 'returns the instance of JayAPI::Elasticsearch::Stats::Nodes' do + expect(method_call).to be(nodes) + end + end end