zenrows 0.1.0 → 0.2.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.
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "webmock/minitest"
5
+
6
+ class ApiClientTest < Minitest::Test
7
+ def setup
8
+ Zenrows.configure do |config|
9
+ config.api_key = "test_api_key"
10
+ end
11
+ @client = Zenrows::ApiClient.new
12
+ end
13
+
14
+ def teardown
15
+ Zenrows.reset_configuration!
16
+ WebMock.reset!
17
+ end
18
+
19
+ def test_initialization_with_global_config
20
+ assert_equal "test_api_key", @client.api_key
21
+ end
22
+
23
+ def test_initialization_with_custom_api_key
24
+ client = Zenrows::ApiClient.new(api_key: "custom_key")
25
+
26
+ assert_equal "custom_key", client.api_key
27
+ end
28
+
29
+ def test_get_basic_request
30
+ stub_request(:get, "https://api.zenrows.com/v1/")
31
+ .with(query: hash_including(apikey: "test_api_key", url: "https://example.com"))
32
+ .to_return(status: 200, body: "<html>Test</html>")
33
+
34
+ response = @client.get("https://example.com")
35
+
36
+ assert_equal 200, response.status
37
+ assert_equal "<html>Test</html>", response.html
38
+ end
39
+
40
+ def test_get_with_autoparse
41
+ stub_request(:get, "https://api.zenrows.com/v1/")
42
+ .with(query: hash_including(autoparse: "true"))
43
+ .to_return(status: 200, body: '{"title":"Product"}')
44
+
45
+ response = @client.get("https://amazon.com/dp/123", autoparse: true)
46
+
47
+ assert_equal "Product", response.parsed["title"]
48
+ end
49
+
50
+ def test_get_with_css_extractor_hash
51
+ stub_request(:get, "https://api.zenrows.com/v1/")
52
+ .with(query: hash_including(css_extractor: '{"title":"h1"}'))
53
+ .to_return(status: 200, body: '{"title":"Page Title"}')
54
+
55
+ response = @client.get("https://example.com", css_extractor: {title: "h1"})
56
+
57
+ assert_equal "Page Title", response.extracted["title"]
58
+ end
59
+
60
+ def test_get_with_css_extractor_dsl
61
+ extractor = Zenrows::CssExtractor.build do
62
+ extract :title, "h1"
63
+ links :urls, "a"
64
+ end
65
+
66
+ stub_request(:get, "https://api.zenrows.com/v1/")
67
+ .with(query: hash_including(css_extractor: '{"title":"h1","urls":"a @href"}'))
68
+ .to_return(status: 200, body: '{"title":"Test","urls":["link1"]}')
69
+
70
+ response = @client.get("https://example.com", css_extractor: extractor)
71
+
72
+ assert_equal "Test", response.extracted["title"]
73
+ end
74
+
75
+ def test_get_with_markdown_response
76
+ stub_request(:get, "https://api.zenrows.com/v1/")
77
+ .with(query: hash_including(response_type: "markdown"))
78
+ .to_return(status: 200, body: "# Title\n\nText")
79
+
80
+ response = @client.get("https://example.com", response_type: "markdown")
81
+
82
+ assert_equal "# Title\n\nText", response.markdown
83
+ end
84
+
85
+ def test_get_with_js_render
86
+ stub_request(:get, "https://api.zenrows.com/v1/")
87
+ .with(query: hash_including(js_render: "true"))
88
+ .to_return(status: 200, body: "<html>Rendered</html>")
89
+
90
+ response = @client.get("https://example.com", js_render: true)
91
+
92
+ assert_equal "<html>Rendered</html>", response.html
93
+ end
94
+
95
+ def test_get_with_premium_proxy
96
+ stub_request(:get, "https://api.zenrows.com/v1/")
97
+ .with(query: hash_including(premium_proxy: "true", proxy_country: "us"))
98
+ .to_return(status: 200, body: "OK")
99
+
100
+ response = @client.get("https://example.com", premium_proxy: true, proxy_country: "us")
101
+
102
+ assert_predicate response, :success?
103
+ end
104
+
105
+ def test_get_with_antibot
106
+ stub_request(:get, "https://api.zenrows.com/v1/")
107
+ .with(query: hash_including(antibot: "true"))
108
+ .to_return(status: 200, body: "OK")
109
+
110
+ response = @client.get("https://example.com", antibot: true)
111
+
112
+ assert_predicate response, :success?
113
+ end
114
+
115
+ def test_authentication_error
116
+ stub_request(:get, /api\.zenrows\.com/)
117
+ .with(query: hash_including(apikey: "test_api_key"))
118
+ .to_return(status: 401, body: "Unauthorized")
119
+
120
+ assert_raises Zenrows::AuthenticationError do
121
+ @client.get("https://example.com")
122
+ end
123
+ end
124
+
125
+ def test_rate_limit_error
126
+ stub_request(:get, /api\.zenrows\.com/)
127
+ .with(query: hash_including(apikey: "test_api_key"))
128
+ .to_return(status: 429, body: "Too Many Requests", headers: {"Retry-After" => "60"})
129
+
130
+ error = assert_raises Zenrows::RateLimitError do
131
+ @client.get("https://example.com")
132
+ end
133
+
134
+ assert_equal 60, error.retry_after
135
+ end
136
+
137
+ def test_bot_detected_error
138
+ stub_request(:get, /api\.zenrows\.com/)
139
+ .with(query: hash_including(apikey: "test_api_key"))
140
+ .to_return(status: 403, body: "Bot detected")
141
+
142
+ error = assert_raises Zenrows::BotDetectedError do
143
+ @client.get("https://example.com")
144
+ end
145
+
146
+ assert_includes error.suggestion, "premium_proxy"
147
+ end
148
+
149
+ def test_post_request
150
+ stub_request(:post, "https://api.zenrows.com/v1/")
151
+ .with(
152
+ query: hash_including(apikey: "test_api_key", url: "https://example.com"),
153
+ body: "form_data=value"
154
+ )
155
+ .to_return(status: 200, body: "Posted")
156
+
157
+ response = @client.post("https://example.com", body: "form_data=value")
158
+
159
+ assert_predicate response, :success?
160
+ end
161
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ApiResponseTest < Minitest::Test
6
+ def test_html_response
7
+ http_response = mock_response("<html><body>Test</body></html>", 200)
8
+ response = Zenrows::ApiResponse.new(http_response)
9
+
10
+ assert_equal "<html><body>Test</body></html>", response.html
11
+ assert_equal "<html><body>Test</body></html>", response.body
12
+ assert_equal 200, response.status
13
+ assert_predicate response, :success?
14
+ end
15
+
16
+ def test_json_response_with_html
17
+ body = '{"html":"<html>Test</html>","xhr":[]}'
18
+ http_response = mock_response(body, 200)
19
+ response = Zenrows::ApiResponse.new(http_response, json_response: true)
20
+
21
+ assert_equal "<html>Test</html>", response.html
22
+ assert_empty response.xhr
23
+ end
24
+
25
+ def test_markdown_response
26
+ body = "# Title\n\nParagraph text"
27
+ http_response = mock_response(body, 200)
28
+ response = Zenrows::ApiResponse.new(http_response, response_type: "markdown")
29
+
30
+ assert_equal "# Title\n\nParagraph text", response.markdown
31
+ end
32
+
33
+ def test_autoparse_response
34
+ body = '{"title":"Product Name","price":"$29.99"}'
35
+ http_response = mock_response(body, 200)
36
+ response = Zenrows::ApiResponse.new(http_response, autoparse: true)
37
+
38
+ assert_equal "Product Name", response.parsed["title"]
39
+ assert_equal "$29.99", response.parsed["price"]
40
+ end
41
+
42
+ def test_css_extractor_response
43
+ body = '{"title":"Page Title","links":["link1","link2"]}'
44
+ http_response = mock_response(body, 200)
45
+ response = Zenrows::ApiResponse.new(http_response, css_extractor: {title: "h1"})
46
+
47
+ assert_equal "Page Title", response.extracted["title"]
48
+ assert_equal ["link1", "link2"], response.extracted["links"]
49
+ end
50
+
51
+ def test_js_instructions_report
52
+ body = '{"html":"...","js_instructions_report":{"instructions_executed":2}}'
53
+ http_response = mock_response(body, 200)
54
+ response = Zenrows::ApiResponse.new(http_response, json_response: true)
55
+
56
+ assert_equal({"instructions_executed" => 2}, response.js_instructions_report)
57
+ end
58
+
59
+ def test_headers_accessors
60
+ headers = {
61
+ "Concurrency-Limit" => "200",
62
+ "Concurrency-Remaining" => "199",
63
+ "X-Request-Cost" => "0.001",
64
+ "Zr-Final-Url" => "https://example.com/final"
65
+ }
66
+ http_response = mock_response("", 200, headers)
67
+ response = Zenrows::ApiResponse.new(http_response)
68
+
69
+ assert_equal 200, response.concurrency_limit
70
+ assert_equal 199, response.concurrency_remaining
71
+ assert_in_delta 0.001, response.request_cost
72
+ assert_equal "https://example.com/final", response.final_url
73
+ end
74
+
75
+ def test_success_status_codes
76
+ [200, 201, 204, 299].each do |code|
77
+ http_response = mock_response("", code)
78
+ response = Zenrows::ApiResponse.new(http_response)
79
+
80
+ assert_predicate response, :success?, "Expected #{code} to be success"
81
+ end
82
+ end
83
+
84
+ def test_non_success_status_codes
85
+ [400, 401, 403, 404, 500].each do |code|
86
+ http_response = mock_response("", code)
87
+ response = Zenrows::ApiResponse.new(http_response)
88
+
89
+ refute_predicate response, :success?, "Expected #{code} to not be success"
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def mock_response(body, status, headers = {})
96
+ MockHttpResponse.new(body, status, headers)
97
+ end
98
+
99
+ class MockHttpResponse
100
+ attr_reader :body, :headers
101
+
102
+ def initialize(body, status, headers = {})
103
+ @body = MockBody.new(body)
104
+ @status = MockStatus.new(status)
105
+ @headers = MockHeaders.new(headers)
106
+ end
107
+
108
+ attr_reader :status
109
+
110
+ class MockBody
111
+ def initialize(content)
112
+ @content = content
113
+ end
114
+
115
+ def to_s
116
+ @content
117
+ end
118
+ end
119
+
120
+ class MockStatus
121
+ def initialize(code)
122
+ @code = code
123
+ end
124
+
125
+ attr_reader :code
126
+ end
127
+
128
+ class MockHeaders
129
+ def initialize(headers)
130
+ @headers = headers
131
+ end
132
+
133
+ def [](key)
134
+ @headers[key]
135
+ end
136
+
137
+ def to_h
138
+ @headers
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CssExtractorTest < Minitest::Test
6
+ def test_build_with_block
7
+ extractor = Zenrows::CssExtractor.build do
8
+ extract :title, "h1"
9
+ extract :price, ".price"
10
+ end
11
+
12
+ assert_equal({title: "h1", price: ".price"}, extractor.to_h)
13
+ end
14
+
15
+ def test_extract_with_attribute
16
+ extractor = Zenrows::CssExtractor.new
17
+ extractor.extract(:link, "a.main", attribute: "href")
18
+
19
+ assert_equal({link: "a.main @href"}, extractor.to_h)
20
+ end
21
+
22
+ def test_links_helper
23
+ extractor = Zenrows::CssExtractor.new
24
+ extractor.links(:nav_links, "nav a")
25
+
26
+ assert_equal({nav_links: "nav a @href"}, extractor.to_h)
27
+ end
28
+
29
+ def test_images_helper
30
+ extractor = Zenrows::CssExtractor.new
31
+ extractor.images(:product_images, "img.product")
32
+
33
+ assert_equal({product_images: "img.product @src"}, extractor.to_h)
34
+ end
35
+
36
+ def test_chaining
37
+ extractor = Zenrows::CssExtractor.new
38
+ .extract(:title, "h1")
39
+ .extract(:price, ".price")
40
+ .links(:links, "a")
41
+
42
+ assert_equal 3, extractor.size
43
+ end
44
+
45
+ def test_to_json
46
+ extractor = Zenrows::CssExtractor.build do
47
+ extract :title, "h1"
48
+ extract :link, "a", attribute: "href"
49
+ end
50
+
51
+ json = extractor.to_json
52
+ parsed = JSON.parse(json)
53
+
54
+ assert_equal "h1", parsed["title"]
55
+ assert_equal "a @href", parsed["link"]
56
+ end
57
+
58
+ def test_empty
59
+ extractor = Zenrows::CssExtractor.new
60
+
61
+ assert_empty extractor
62
+
63
+ extractor.extract(:title, "h1")
64
+
65
+ refute_empty extractor
66
+ end
67
+
68
+ def test_size
69
+ extractor = Zenrows::CssExtractor.build do
70
+ extract :one, ".one"
71
+ extract :two, ".two"
72
+ extract :three, ".three"
73
+ end
74
+
75
+ assert_equal 3, extractor.size
76
+ end
77
+
78
+ def test_string_name_converted_to_symbol
79
+ extractor = Zenrows::CssExtractor.new
80
+ extractor.extract("title", "h1")
81
+
82
+ assert_equal({title: "h1"}, extractor.to_h)
83
+ end
84
+ end
@@ -10,7 +10,7 @@ class JsInstructionsTest < Minitest::Test
10
10
  end
11
11
 
12
12
  assert_equal 2, instructions.size
13
- refute instructions.empty?
13
+ refute_empty instructions
14
14
  end
15
15
 
16
16
  def test_click
@@ -113,6 +113,7 @@ class JsInstructionsTest < Minitest::Test
113
113
  end
114
114
 
115
115
  json = instructions.to_json
116
+
116
117
  assert_equal '[{"click":".btn"},{"wait":500}]', json
117
118
  end
118
119
 
@@ -111,4 +111,43 @@ class ProxyTest < Minitest::Test
111
111
  @proxy.build(wait: 200_000)
112
112
  end
113
113
  end
114
+
115
+ def test_build_with_device_mobile
116
+ config = @proxy.build(device: "mobile")
117
+
118
+ assert_includes config[:password], "device=mobile"
119
+ end
120
+
121
+ def test_build_with_device_desktop
122
+ config = @proxy.build(device: :desktop)
123
+
124
+ assert_includes config[:password], "device=desktop"
125
+ end
126
+
127
+ def test_build_with_antibot
128
+ config = @proxy.build(antibot: true)
129
+
130
+ assert_includes config[:password], "antibot=true"
131
+ end
132
+
133
+ def test_build_with_session_ttl
134
+ config = @proxy.build(session_id: true, session_ttl: "30m")
135
+
136
+ assert_includes config[:password], "session_ttl=30m"
137
+ assert_match(/session_id=\d+/, config[:password])
138
+ end
139
+
140
+ def test_build_with_all_valid_session_ttl_values
141
+ %w[30s 5m 30m 1h 1d].each do |ttl|
142
+ config = @proxy.build(session_ttl: ttl)
143
+
144
+ assert_includes config[:password], "session_ttl=#{ttl}"
145
+ end
146
+ end
147
+
148
+ def test_raises_on_invalid_session_ttl
149
+ assert_raises ArgumentError do
150
+ @proxy.build(session_ttl: "invalid")
151
+ end
152
+ end
114
153
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zenrows
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernest Bursa
@@ -46,27 +46,50 @@ executables: []
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
+ - ".mcp.json"
49
50
  - ".standard.yml"
51
+ - ".tool-versions"
50
52
  - ".yardopts"
51
53
  - CHANGELOG.md
52
54
  - CLAUDE.md
53
55
  - LICENSE.txt
56
+ - Makefile
54
57
  - README.md
55
58
  - Rakefile
56
59
  - lib/zenrows.rb
60
+ - lib/zenrows/api_client.rb
61
+ - lib/zenrows/api_response.rb
57
62
  - lib/zenrows/backends/base.rb
58
63
  - lib/zenrows/backends/http_rb.rb
64
+ - lib/zenrows/backends/net_http.rb
59
65
  - lib/zenrows/client.rb
60
66
  - lib/zenrows/configuration.rb
67
+ - lib/zenrows/css_extractor.rb
61
68
  - lib/zenrows/errors.rb
62
69
  - lib/zenrows/js_instructions.rb
63
70
  - lib/zenrows/proxy.rb
64
71
  - lib/zenrows/railtie.rb
65
72
  - lib/zenrows/version.rb
66
73
  - plan.md
74
+ - sig/manifest.yaml
67
75
  - sig/zenrows.rbs
76
+ - sig/zenrows/api_client.rbs
77
+ - sig/zenrows/api_response.rbs
78
+ - sig/zenrows/backends.rbs
79
+ - sig/zenrows/backends/base.rbs
80
+ - sig/zenrows/backends/http_rb.rbs
81
+ - sig/zenrows/backends/net_http.rbs
82
+ - sig/zenrows/client.rbs
83
+ - sig/zenrows/configuration.rbs
84
+ - sig/zenrows/css_extractor.rbs
85
+ - sig/zenrows/errors.rbs
86
+ - sig/zenrows/js_instructions.rbs
87
+ - sig/zenrows/proxy.rbs
68
88
  - test/test_helper.rb
89
+ - test/zenrows/api_client_test.rb
90
+ - test/zenrows/api_response_test.rb
69
91
  - test/zenrows/client_test.rb
92
+ - test/zenrows/css_extractor_test.rb
70
93
  - test/zenrows/js_instructions_test.rb
71
94
  - test/zenrows/proxy_test.rb
72
95
  - test/zenrows_test.rb
@@ -86,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
86
109
  requirements:
87
110
  - - ">="
88
111
  - !ruby/object:Gem::Version
89
- version: 3.1.0
112
+ version: 3.2.0
90
113
  required_rubygems_version: !ruby/object:Gem::Requirement
91
114
  requirements:
92
115
  - - ">="