From 80fb804ef56f0a7167d0239cf6cf45ffa77e244e Mon Sep 17 00:00:00 2001
From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com>
Date: Sun, 5 Nov 2023 20:20:43 +0000
Subject: [PATCH 1/5] Bumps deps

---
 shodanz.gemspec | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/shodanz.gemspec b/shodanz.gemspec
index 3427959..571f85b 100644
--- a/shodanz.gemspec
+++ b/shodanz.gemspec
@@ -20,13 +20,13 @@ Gem::Specification.new do |spec|
   end
   spec.require_paths = ['lib']
 
-  spec.add_dependency 'async-http', '>= 0.38.1', '< 0.57.0'
-  spec.add_dependency 'async', '>= 1.17.1', '< 2.1.0'
+  spec.add_dependency 'async-http', '>= 0.38.1', '< 0.62.0'
+  spec.add_dependency 'async', '>= 1.17.1', '< 2.7.0'
 
-  spec.add_development_dependency 'async-rspec', '~> 1.16.1'
-  spec.add_development_dependency 'bundler', '~> 2.2.0'
+  spec.add_development_dependency 'async-rspec', '~> 1.17.0'
+  spec.add_development_dependency 'bundler', '~> 2.4.0'
   spec.add_development_dependency 'pry', '~> 0.14.1'
-  spec.add_development_dependency 'rake', '~> 13.0.0'
+  spec.add_development_dependency 'rake', '~> 13.1.0'
   spec.add_development_dependency 'rb-readline', '~> 0.5.5'
-  spec.add_development_dependency 'rspec', '~> 3.11.0'
+  spec.add_development_dependency 'rspec', '~> 3.12.0'
 end

From 97c66aed5880afa12e276830a93bcf99ec0acf85 Mon Sep 17 00:00:00 2001
From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com>
Date: Sun, 5 Nov 2023 20:20:57 +0000
Subject: [PATCH 2/5] Use `console` to set logger level

---
 lib/shodanz.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/shodanz.rb b/lib/shodanz.rb
index 45fd1ae..f1fb364 100644
--- a/lib/shodanz.rb
+++ b/lib/shodanz.rb
@@ -2,14 +2,14 @@
 
 require 'json'
 require 'async'
+require 'console'
 require 'async/http/internet'
 require 'shodanz/version'
 require 'shodanz/errors'
 require 'shodanz/api'
 require 'shodanz/client'
 
-# disable async logs by default
-Async.logger.level = 4
+Console.logger.level = 4
 
 # Shodanz is a modern Ruby gem for Shodan, the world's
 # first search engine for Internet-connected devices.

From 62b8ff9697a0b6d728a5b47e0d6c93de00b7c392 Mon Sep 17 00:00:00 2001
From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com>
Date: Sun, 5 Nov 2023 20:21:35 +0000
Subject: [PATCH 3/5] Use `async-rspec` for tests

---
 spec/exploits_api_spec.rb  |  22 +-
 spec/rest_api_spec.rb      | 417 ++++++++-----------------------------
 spec/streaming_api_spec.rb |  20 +-
 3 files changed, 98 insertions(+), 361 deletions(-)

diff --git a/spec/exploits_api_spec.rb b/spec/exploits_api_spec.rb
index f3812aa..8d6c04d 100644
--- a/spec/exploits_api_spec.rb
+++ b/spec/exploits_api_spec.rb
@@ -1,6 +1,8 @@
 require "spec_helper"
 
 RSpec.describe Shodanz::API::Exploits do
+  include_context Async::RSpec::Reactor
+
   before do
     @client = Shodanz.api.exploits.new
   end
@@ -11,24 +13,10 @@
   end
 
   describe '#search' do
-    def check
-      if Async::Task.current?
+    it 'should search across a variety of data sources for exploits' do
+      reactor.async do
         resp = @client.search("SQL", port: 443).wait
-      else
-        resp = @client.search("SQL", port: 443)
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'search across a variety of data sources for exploits' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
diff --git a/spec/rest_api_spec.rb b/spec/rest_api_spec.rb
index d16bfb9..c285ec0 100644
--- a/spec/rest_api_spec.rb
+++ b/spec/rest_api_spec.rb
@@ -1,6 +1,8 @@
 require "spec_helper"
 
 RSpec.describe Shodanz::API::REST do
+  include_context Async::RSpec::Reactor
+
   before do
     @client = Shodanz.api.rest.new
   end
@@ -11,50 +13,22 @@
   end
 
   describe '#scan' do
-    def check
-      if Async::Task.current?
+    it 'should be able to scan a host on the internet' do
+      reactor.async do
         resp = @client.scan("1.1.1.1").wait
-      else
-        resp = @client.scan("1.1.1.1")
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp["count"]).to be_a(Integer)
-      expect(resp["id"]).to be_a(String)
-      expect(resp["credits_left"]).to be_a(Integer)
-    end
-
-    describe 'scans host on the internet' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
+        expect(resp["count"]).to be_a(Integer)
+        expect(resp["id"]).to be_a(String)
+        expect(resp["credits_left"]).to be_a(Integer)
       end
     end
   end
 
   describe '#info' do
-    def check
-      if Async::Task.current?
+    it 'returns info about the underlying token' do
+      reactor.async do
         resp = @client.info.wait
-      else
-        resp = @client.info
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'returns info about the underlying token' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
@@ -62,24 +36,10 @@ def check
   describe '#host' do
     let(:ip) { "8.8.8.8" }
 
-    def check
-      if Async::Task.current?
+    it 'returns all services that have been found on the given host IP' do
+      reactor.async do
         resp = @client.host(ip).wait
-      else
-        resp = @client.host(ip)
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'returns all services that have been found on the given host IP' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
@@ -87,24 +47,10 @@ def check
   describe '#host_count' do
     let(:query) { "apache" }
 
-    def check
-      if Async::Task.current?
+    it 'returns the total number of results that matches a given query' do
+      reactor.async do
         resp = @client.host_count(query).wait
-      else
-        resp = @client.host_count(query)
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'returns the total number of results that matches a given query' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
@@ -112,24 +58,10 @@ def check
   describe '#host_search' do
     let(:query) { "apache" }
 
-    def check
-      if Async::Task.current?
+    it 'returns the total number of results that matches a given query' do
+      reactor.async do
         resp = @client.host_search(query).wait
-      else
-        resp = @client.host_search(query)
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'returns the total number of results that matches a given query' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
@@ -137,136 +69,67 @@ def check
   describe '#host_search_tokens' do
     let(:query) { "apache" }
 
-    def check
-      if Async::Task.current?
+    it 'returns a parsed version of the query' do
+      reactor.async do
         resp = @client.host_search_tokens(query).wait
-      else
-        resp = @client.host_search_tokens(query)
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp['attributes']).to be_a(Hash)
-      expect(resp['errors']).to be_a(Array)
-      expect(resp['filters']).to be_a(Array)
-      expect(resp['string']).to be_a(String)
-      expect(resp['string']).to eq(query)
-    end
-
-    describe 'returns a parsed version of the query' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
+        expect(resp['attributes']).to be_a(Hash)
+        expect(resp['errors']).to be_a(Array)
+        expect(resp['filters']).to be_a(Array)
+        expect(resp['string']).to be_a(String)
+        expect(resp['string']).to eq(query)
       end
     end
   end
 
   describe '#ports' do
-    def check
-      if Async::Task.current?
+    it 'returns a list of port numbers that the crawlers are looking for' do
+      reactor.async do
         resp = @client.ports.wait
-      else
-        resp = @client.ports
-      end
-      expect(resp).to be_a(Array)
-    end
-
-    describe 'returns a list of port numbers that the crawlers are looking for' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Array)
       end
     end
   end
 
   describe '#protocols' do
-    def check
-      if Async::Task.current?
+    it 'returns all protocols that can be used when performing on-demand scans' do
+      reactor.async do
         resp = @client.protocols.wait
-      else
-        resp = @client.protocols
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'returns all protocols that can be used when performing on-demand scans' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
 
   describe '#profile' do
-    def check
-      if Async::Task.current?
+    it 'returns information about the shodan account' do
+      reactor.async do
         resp = @client.profile.wait
-      else
-        resp = @client.profile
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp["member"]).to be(true).or be(false)
-      expect(resp["credits"]).to be_a(Integer)
-      expect(resp["created"]).to be_a(String)
-      expect(resp.key?("display_name")).to be(true)
-    end
-
-    describe 'returns information about the shodan account' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
+        expect(resp["member"]).to be(true).or be(false)
+        expect(resp["credits"]).to be_a(Integer)
+        expect(resp["created"]).to be_a(String)
+        expect(resp.key?("display_name")).to be(true)
       end
     end
   end
 
   describe '#community_queries' do
-    def check
-      if Async::Task.current?
+    it 'obtains a list of search queries that users have saved' do
+      reactor.async do
         resp = @client.community_queries.wait
-      else
-        resp = @client.community_queries
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp['total']).to be_a(Integer)
-      expect(resp['matches']).to be_a(Array)
-
-      example_match = resp['matches'].first
-
-      expect(example_match['votes']).to be_a(Integer)
-      expect(example_match['description']).to be_a(String)
-      expect(example_match['tags']).to be_a(Array)
-      expect(example_match['timestamp']).to be_a(String)
-      expect(example_match['title']).to be_a(String)
-      expect(example_match['query']).to be_a(String)
-    end
 
-    describe 'obtains a list of search queries that users have saved' do
-      it 'works synchronously' do
-        check
-      end
+        expect(resp).to be_a(Hash)
+        expect(resp['total']).to be_a(Integer)
+        expect(resp['matches']).to be_a(Array)
+
+        example_match = resp['matches'].first
 
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(example_match['votes']).to be_a(Integer)
+        expect(example_match['description']).to be_a(String)
+        expect(example_match['tags']).to be_a(Array)
+        expect(example_match['timestamp']).to be_a(String)
+        expect(example_match['title']).to be_a(String)
+        expect(example_match['query']).to be_a(String)
       end
     end
   end
@@ -274,35 +137,22 @@ def check
   describe '#search_for_community_query' do
     let(:query) { "apache" }
 
-    def check
-      if Async::Task.current?
+    it 'search the directory of search queries that users have saved' do
+      reactor.async do
         resp = @client.search_for_community_query(query).wait
-      else
-        resp = @client.search_for_community_query(query)
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp['total']).to be_a(Integer)
-      expect(resp['matches']).to be_a(Array)
-
-      example_match = resp['matches'].first
-
-      expect(example_match['votes']).to be_a(Integer)
-      expect(example_match['description']).to be_a(String)
-      expect(example_match['tags']).to be_a(Array)
-      expect(example_match['timestamp']).to be_a(String)
-      expect(example_match['title']).to be_a(String)
-      expect(example_match['query']).to be_a(String)
-    end
 
-    describe 'search the directory of search queries that users have saved' do
-      it 'works synchronously' do
-        check
-      end
+        expect(resp).to be_a(Hash)
+        expect(resp['total']).to be_a(Integer)
+        expect(resp['matches']).to be_a(Array)
 
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        example_match = resp['matches'].first
+
+        expect(example_match['votes']).to be_a(Integer)
+        expect(example_match['description']).to be_a(String)
+        expect(example_match['tags']).to be_a(Array)
+        expect(example_match['timestamp']).to be_a(String)
+        expect(example_match['title']).to be_a(String)
+        expect(example_match['query']).to be_a(String)
       end
     end
   end
@@ -310,24 +160,10 @@ def check
   describe '#resolve' do
     let(:hostname) { "google.com" }
 
-    def check
-      if Async::Task.current?
+    it 'resolves domains to ip addresses' do
+      reactor.async do
         resp = @client.resolve(hostname).wait
-      else
-        resp = @client.resolve(hostname)
-      end
-      expect(resp).to be_a(Hash)
-    end
-
-    describe 'resolves domains to ip addresses' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
       end
     end
   end
@@ -335,117 +171,38 @@ def check
   describe '#reverse_lookup' do
     let(:ip) { '8.8.8.8' }
 
-    def check
-      if Async::Task.current?
+    it 'resolves ip addresses to domains' do
+      reactor.async do
         resp = @client.reverse_lookup(ip).wait
-      else
-        resp = @client.reverse_lookup(ip)
-      end
-      expect(resp).to be_a(Hash)
-      expect('8.8.8.8').not_to be nil
-
-      # NOTE: this was the old behavior...
-      #
-      #  Now it's in the form ip => [ dns_names ... ]
-      # {"8.8.8.8"=>["dns.google"]}
-      #
-      # expect(resp[ip]).to be_a(Array)
-      # expect(resp[ip].first).to eq('google-public-dns-a.google.com')
-    end
-
-    describe 'resolves ip addresses to domains' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
+        expect('8.8.8.8').not_to be nil
       end
     end
   end
 
   describe '#http_headers' do
-    def check
-      if Async::Task.current?
+    it 'shows the HTTP headers that your client sends when connecting to a webserver' do
+      reactor.async do
         resp = @client.http_headers.wait
-      else
-        resp = @client.http_headers
-      end
-      expect(resp).to be_a(Hash)
-      expect(resp['Content-Length']).to be_a(String)
-      expect(resp['Content-Length']).to eq('')
-      # TODO maybe specify a content-type?
-      expect(resp['Content-Type']).to be_a(String)
-      expect(resp['Content-Type']).to eq('')
-      expect(resp['Host']).to be_a(String)
-      expect(resp['Host']).to eq('api.shodan.io')
-    end
 
-    describe 'shows the HTTP headers that your client sends when connecting to a webserver' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(Hash)
+        expect(resp['Content-Length']).to be_a(String)
+        expect(resp['Content-Length']).to eq('')
+        # TODO maybe specify a content-type?
+        expect(resp['Content-Type']).to be_a(String)
+        expect(resp['Content-Type']).to eq('')
+        expect(resp['Host']).to be_a(String)
+        expect(resp['Host']).to eq('api.shodan.io')
       end
     end
   end
 
   describe '#my_ip' do
-    def check
-      if Async::Task.current?
+    it 'shows the current IP address as seen from the internet' do
+      reactor.async do
         resp = @client.my_ip.wait
-      else
-        resp = @client.my_ip
-      end
-      expect(resp).to be_a(String)
-    end
-
-    describe 'shows the current IP address as seen from the internet' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
-        end
+        expect(resp).to be_a(String)
       end
     end
   end
-
-  # This no longer seems to be available, or at least it's not documented
-  # clearly anywhere I can find. It just stopped working.
-  #
-  # describe '#honeypot_score' do
-  #   let(:ip) { '8.8.8.8' }
-  # 
-  #   def check
-  #     if Async::Task.current?
-  #       resp = @client.honeypot_score(ip).wait
-  #     else
-  #       resp = @client.honeypot_score(ip)
-  #     end
-  #     expect(resp).to be_a(Float)
-  #     expect(resp).to eq(0.0)
-  #   end
-  #
-  #   describe 'returns the calculated likelihood a given IP is a honeypot' do
-  #     it 'works synchronously' do
-  #       check
-  #     end
-  #
-  #     it 'works asynchronously' do
-  #       Async do
-  #         check
-  #       end
-  #     end
-  #   end
-  # end
-
 end
diff --git a/spec/streaming_api_spec.rb b/spec/streaming_api_spec.rb
index 2f0de45..899562f 100644
--- a/spec/streaming_api_spec.rb
+++ b/spec/streaming_api_spec.rb
@@ -1,6 +1,8 @@
 require "spec_helper"
 
 RSpec.describe Shodanz::API::Streaming do
+  include_context Async::RSpec::Reactor
+
   before do
     @client = Shodanz.api.streaming.new
   end
@@ -11,20 +13,10 @@
   end
 
   describe '#banners' do
-    def check
-      @client.banners(limit: 1) do |banner|
-        expect(banner).to be_a(Hash)
-      end
-    end
-
-    describe 'stream any banner data Shodan collects' do
-      it 'works synchronously' do
-        check
-      end
-
-      it 'works asynchronously' do
-        Async do
-          check
+    it "should stream any banner data Shodan collects" do
+      reactor.async do
+        @client.banners(limit: 1) do |banner|
+          expect(banner).to be_a(Hash)
         end
       end
     end

From c0083650a5c1f73bfc654f795901c586b25d4dbf Mon Sep 17 00:00:00 2001
From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com>
Date: Sun, 5 Nov 2023 20:24:36 +0000
Subject: [PATCH 4/5] Update `dependabot.yml`

---
 .github/dependabot.yml | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d169ed9..56add0d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,10 +6,3 @@ updates:
     interval: daily
     time: "10:00"
   open-pull-requests-limit: 10
-  ignore:
-  - dependency-name: async-http
-    versions:
-    - 0.55.0
-  - dependency-name: pry
-    versions:
-    - 0.14.0

From d535a01e869d2d3e0a574781be82d159ac3a92e1 Mon Sep 17 00:00:00 2001
From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com>
Date: Sun, 5 Nov 2023 20:24:54 +0000
Subject: [PATCH 5/5] Bump `ruby` version in GHA tests

---
 .github/workflows/test.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 15c1146..36378fb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,7 +21,7 @@ jobs:
     - uses: actions/checkout@v3
     - uses: ruby/setup-ruby@v1
       with:
-        ruby-version: '3.0'
+        ruby-version: '3.2'
         bundler-cache: true
     - name: RSpec
       env: