crawlscope 0.7.2 → 0.8.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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +74 -8
- data/lib/crawlscope/browser.rb +18 -1
- data/lib/crawlscope/configuration.rb +6 -1
- data/lib/crawlscope/crawl.rb +5 -2
- data/lib/crawlscope/http.rb +13 -2
- data/lib/crawlscope/page.rb +8 -0
- data/lib/crawlscope/reporter.rb +9 -4
- data/lib/crawlscope/request_headers.rb +34 -0
- data/lib/crawlscope/result.rb +4 -0
- data/lib/crawlscope/run.rb +0 -10
- data/lib/crawlscope/server_timing/reporter.rb +112 -0
- data/lib/crawlscope/server_timing/summary.rb +107 -0
- data/lib/crawlscope/server_timing.rb +138 -0
- data/lib/crawlscope/sitemap.rb +17 -5
- data/lib/crawlscope/version.rb +1 -1
- data/lib/generators/crawlscope/templates/initializer.rb.tt +1 -1
- data/test/crawlscope/browser_test.rb +56 -3
- data/test/crawlscope/configuration_test.rb +50 -10
- data/test/crawlscope/crawl_test.rb +29 -0
- data/test/crawlscope/http_test.rb +60 -1
- data/test/crawlscope/install_generator_test.rb +9 -11
- data/test/crawlscope/reporter_test.rb +64 -6
- data/test/crawlscope/run_test.rb +3 -11
- data/test/crawlscope/server_timing_summary_test.rb +87 -0
- data/test/crawlscope/server_timing_test.rb +101 -0
- data/test/crawlscope/sitemap_test.rb +21 -0
- metadata +7 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crawlscope
|
|
4
|
+
class ServerTiming
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
TOKEN = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/
|
|
8
|
+
NUMBER = /\A-?(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[eE][+-]?\d+)?\z/
|
|
9
|
+
QUOTED_STRING = /\A"(?:[^"\\\x00-\x1F]|\\[\t\x20-\x7E])*"\z/
|
|
10
|
+
EMPTY_LIST_MEMBER = /\A[ \t]*\z/
|
|
11
|
+
|
|
12
|
+
Metric = Data.define(:name, :duration, :description) do
|
|
13
|
+
def timed?
|
|
14
|
+
!duration.nil?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :invalid_count
|
|
19
|
+
|
|
20
|
+
def initialize(header)
|
|
21
|
+
@header = header
|
|
22
|
+
@invalid_count = 0
|
|
23
|
+
@metrics = parse.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each(&block)
|
|
27
|
+
@metrics.each(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def empty?
|
|
31
|
+
@metrics.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def present?
|
|
35
|
+
!@header.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def size
|
|
39
|
+
@metrics.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse
|
|
45
|
+
Array(@header).flatten.compact.flat_map do |header|
|
|
46
|
+
metrics_from(header.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def metrics_from(header)
|
|
51
|
+
fields, complete = split(header, ",")
|
|
52
|
+
|
|
53
|
+
unless complete
|
|
54
|
+
@invalid_count += 1
|
|
55
|
+
fields.pop
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
fields.filter_map do |field|
|
|
59
|
+
metric_from(field) unless EMPTY_LIST_MEMBER.match?(field)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def metric_from(field)
|
|
64
|
+
parts, complete = split(field, ";")
|
|
65
|
+
name = parts.shift.to_s.strip
|
|
66
|
+
|
|
67
|
+
if complete && TOKEN.match?(name)
|
|
68
|
+
metric(name, parameters_from(parts))
|
|
69
|
+
else
|
|
70
|
+
@invalid_count += 1
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parameters_from(parts)
|
|
76
|
+
parts.each_with_object({}) do |part, parameters|
|
|
77
|
+
name, value = part.strip.split("=", 2)
|
|
78
|
+
next unless name
|
|
79
|
+
|
|
80
|
+
name = name.downcase
|
|
81
|
+
next if parameters.key?(name)
|
|
82
|
+
|
|
83
|
+
parameters[name] = parameter_value(value.to_s.strip)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parameter_value(value)
|
|
88
|
+
if TOKEN.match?(value)
|
|
89
|
+
value
|
|
90
|
+
elsif QUOTED_STRING.match?(value)
|
|
91
|
+
value[1...-1].gsub(/\\(.)/m, '\1')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def metric(name, parameters)
|
|
96
|
+
duration = duration_from(parameters["dur"])
|
|
97
|
+
|
|
98
|
+
if (parameters.key?("dur") && duration.nil?) ||
|
|
99
|
+
(parameters.key?("desc") && parameters["desc"].nil?)
|
|
100
|
+
@invalid_count += 1
|
|
101
|
+
nil
|
|
102
|
+
else
|
|
103
|
+
Metric.new(
|
|
104
|
+
name: name,
|
|
105
|
+
duration: duration,
|
|
106
|
+
description: parameters["desc"]
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def duration_from(value)
|
|
112
|
+
if value && NUMBER.match?(value)
|
|
113
|
+
duration = Float(value)
|
|
114
|
+
duration if duration.finite?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def split(value, delimiter)
|
|
119
|
+
fields, quoted, escaped = [+""], false, false
|
|
120
|
+
|
|
121
|
+
value.each_char do |character|
|
|
122
|
+
if escaped
|
|
123
|
+
escaped = false
|
|
124
|
+
elsif quoted && character == "\\"
|
|
125
|
+
escaped = true
|
|
126
|
+
elsif character == "\""
|
|
127
|
+
quoted = !quoted
|
|
128
|
+
elsif character == delimiter && !quoted
|
|
129
|
+
fields << +""
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
fields.last << character unless character == delimiter && !quoted
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[fields, !quoted && !escaped]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/crawlscope/sitemap.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Crawlscope
|
|
|
9
9
|
class Sitemap
|
|
10
10
|
SITEMAP_NAMESPACE = {"xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9"}.freeze
|
|
11
11
|
|
|
12
|
-
def initialize(path:, adapter: nil, concurrency: Configuration::DEFAULT_CONCURRENCY, fetch_executor: Configuration::DEFAULT_FETCH_EXECUTOR, timeout_seconds: Configuration::DEFAULT_TIMEOUT_SECONDS)
|
|
12
|
+
def initialize(path:, adapter: nil, concurrency: Configuration::DEFAULT_CONCURRENCY, fetch_executor: Configuration::DEFAULT_FETCH_EXECUTOR, profile_token: nil, timeout_seconds: Configuration::DEFAULT_TIMEOUT_SECONDS)
|
|
13
13
|
@path = path
|
|
14
14
|
@adapter = adapter
|
|
15
15
|
@concurrency = concurrency
|
|
16
16
|
@fetch_executor = fetch_executor
|
|
17
|
+
@profile_token = profile_token
|
|
17
18
|
@timeout_seconds = timeout_seconds
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -34,7 +35,7 @@ module Crawlscope
|
|
|
34
35
|
end
|
|
35
36
|
return [] if already_visited
|
|
36
37
|
|
|
37
|
-
document = Nokogiri::XML(read(source))
|
|
38
|
+
document = Nokogiri::XML(read(source, base_url: base_url))
|
|
38
39
|
root_name = document.root&.name
|
|
39
40
|
unless %w[sitemapindex urlset].include?(root_name)
|
|
40
41
|
raise ValidationError, "Sitemap #{source} has unexpected root #{root_name.inspect}"
|
|
@@ -55,9 +56,16 @@ module Crawlscope
|
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
def read(source)
|
|
59
|
+
def read(source, base_url:)
|
|
59
60
|
if Url.remote?(source)
|
|
60
|
-
response = connection.get(source)
|
|
61
|
+
response = connection.get(source) do |request|
|
|
62
|
+
RequestHeaders.add_profile_token(
|
|
63
|
+
request.headers,
|
|
64
|
+
url: source,
|
|
65
|
+
base_url: base_url,
|
|
66
|
+
profile_token: @profile_token
|
|
67
|
+
)
|
|
68
|
+
end
|
|
61
69
|
unless response.status.to_i.between?(200, 299)
|
|
62
70
|
raise ValidationError, "Sitemap #{source} returned HTTP #{response.status}"
|
|
63
71
|
end
|
|
@@ -92,7 +100,11 @@ module Crawlscope
|
|
|
92
100
|
|
|
93
101
|
def connection
|
|
94
102
|
Faraday.new do |faraday|
|
|
95
|
-
faraday.response
|
|
103
|
+
faraday.response(
|
|
104
|
+
:follow_redirects,
|
|
105
|
+
limit: Http::MAX_REDIRECTS,
|
|
106
|
+
callback: RequestHeaders.method(:strip_profile_token_on_cross_origin_redirect)
|
|
107
|
+
)
|
|
96
108
|
faraday.options.timeout = @timeout_seconds
|
|
97
109
|
faraday.options.open_timeout = @timeout_seconds
|
|
98
110
|
faraday.adapter @adapter if @adapter
|
data/lib/crawlscope/version.rb
CHANGED
|
@@ -14,7 +14,7 @@ module CrawlscopeConfiguration
|
|
|
14
14
|
Crawlscope.configure do |config|
|
|
15
15
|
config.base_url = -> { ENV.fetch("CRAWLSCOPE_BASE_URL", "http://localhost:3000") }
|
|
16
16
|
config.sitemap_path = lambda {
|
|
17
|
-
ENV.fetch("SITEMAP",
|
|
17
|
+
ENV.fetch("SITEMAP", "#{config.base_url.to_s.chomp("/")}/sitemap.xml")
|
|
18
18
|
}
|
|
19
19
|
config.site_name = ENV.fetch("CRAWLSCOPE_SITE_NAME", "Application")
|
|
20
20
|
end
|
|
@@ -14,13 +14,14 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
class FakeNetwork
|
|
17
|
-
attr_reader :cleared, :idle_waits, :status
|
|
17
|
+
attr_reader :cleared, :idle_waits, :interceptions, :status
|
|
18
18
|
|
|
19
19
|
def initialize(response:, status: 200)
|
|
20
20
|
@response = response
|
|
21
21
|
@status = status
|
|
22
22
|
@cleared = []
|
|
23
23
|
@idle_waits = []
|
|
24
|
+
@interceptions = []
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def clear(scope)
|
|
@@ -29,6 +30,10 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
29
30
|
|
|
30
31
|
attr_reader :response
|
|
31
32
|
|
|
33
|
+
def intercept(**options)
|
|
34
|
+
@interceptions << options
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def wait_for_idle(duration:, timeout:)
|
|
33
38
|
@idle_waits << {duration: duration, timeout: timeout}
|
|
34
39
|
end
|
|
@@ -43,6 +48,7 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
43
48
|
@current_url = current_url
|
|
44
49
|
@url = url
|
|
45
50
|
@evaluations = []
|
|
51
|
+
@callbacks = {}
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
attr_reader :body
|
|
@@ -57,11 +63,40 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
57
63
|
@visited_url = url
|
|
58
64
|
end
|
|
59
65
|
|
|
66
|
+
def on(event, &callback)
|
|
67
|
+
@callbacks[event] = callback
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def publish(event, value)
|
|
71
|
+
@callbacks.fetch(event).call(value)
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
attr_reader :url
|
|
61
75
|
end
|
|
62
76
|
|
|
77
|
+
class FakeRequest
|
|
78
|
+
attr_reader :continued_headers, :headers, :url
|
|
79
|
+
|
|
80
|
+
def initialize(url:, headers: {})
|
|
81
|
+
@url = url
|
|
82
|
+
@headers = headers
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def continue(headers:)
|
|
86
|
+
@continued_headers = headers.to_h { |header| [header.fetch(:name), header.fetch(:value)] }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
63
90
|
def test_fetch_returns_rendered_page
|
|
64
|
-
network = FakeNetwork.new(
|
|
91
|
+
network = FakeNetwork.new(
|
|
92
|
+
response: Response.new(
|
|
93
|
+
url: "https://example.com/final",
|
|
94
|
+
headers: {
|
|
95
|
+
"content-type" => "text/html",
|
|
96
|
+
"server-timing" => "app;dur=47.2"
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
)
|
|
65
100
|
page = FakePage.new(network: network, body: "<html><body>Hello</body></html>")
|
|
66
101
|
browser = browser_with(page: page, scroll_page: false)
|
|
67
102
|
|
|
@@ -73,6 +108,7 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
73
108
|
assert_equal "https://example.com/final", result.normalized_final_url
|
|
74
109
|
assert_equal 200, result.status
|
|
75
110
|
assert result.html?
|
|
111
|
+
assert_equal 47.2, result.server_timing.first.duration
|
|
76
112
|
assert_equal [], page.evaluations
|
|
77
113
|
end
|
|
78
114
|
|
|
@@ -88,6 +124,21 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
88
124
|
assert_equal 4, network.idle_waits.size
|
|
89
125
|
end
|
|
90
126
|
|
|
127
|
+
def test_profile_token_is_limited_to_same_origin_documents
|
|
128
|
+
network = FakeNetwork.new(response: nil)
|
|
129
|
+
page = FakePage.new(network: network)
|
|
130
|
+
browser_with(page: page, profile_token: "profile-token")
|
|
131
|
+
same_origin_request = FakeRequest.new(url: "https://example.com/page")
|
|
132
|
+
cross_origin_request = FakeRequest.new(url: "https://cdn.example.net/frame")
|
|
133
|
+
|
|
134
|
+
page.publish(:request, same_origin_request)
|
|
135
|
+
page.publish(:request, cross_origin_request)
|
|
136
|
+
|
|
137
|
+
assert_equal [{resource_type: :document}], network.interceptions
|
|
138
|
+
assert_equal "profile-token", same_origin_request.continued_headers.fetch("X-Profile-Token")
|
|
139
|
+
refute_includes cross_origin_request.continued_headers, "X-Profile-Token"
|
|
140
|
+
end
|
|
141
|
+
|
|
91
142
|
def test_fetch_falls_back_to_page_url_and_original_url
|
|
92
143
|
page_url_network = FakeNetwork.new(response: nil)
|
|
93
144
|
page_url = FakePage.new(network: page_url_network, url: "https://example.com/page")
|
|
@@ -142,14 +193,16 @@ class CrawlscopeBrowserTest < Minitest::Test
|
|
|
142
193
|
|
|
143
194
|
private
|
|
144
195
|
|
|
145
|
-
def browser_with(page: FakePage.new(network: FakeNetwork.new(response: nil)), browser: FakeBrowser.new, scroll_page: false)
|
|
196
|
+
def browser_with(page: FakePage.new(network: FakeNetwork.new(response: nil)), browser: FakeBrowser.new, profile_token: nil, scroll_page: false)
|
|
146
197
|
Crawlscope::Browser.allocate.tap do |instance|
|
|
147
198
|
instance.instance_variable_set(:@base_url, "https://example.com")
|
|
148
199
|
instance.instance_variable_set(:@timeout_seconds, 20)
|
|
149
200
|
instance.instance_variable_set(:@network_idle_timeout_seconds, 5)
|
|
201
|
+
instance.instance_variable_set(:@profile_token, profile_token)
|
|
150
202
|
instance.instance_variable_set(:@scroll_page, scroll_page)
|
|
151
203
|
instance.instance_variable_set(:@browser, browser)
|
|
152
204
|
instance.instance_variable_set(:@page, page)
|
|
205
|
+
instance.send(:configure_profile_requests)
|
|
153
206
|
end
|
|
154
207
|
end
|
|
155
208
|
end
|
|
@@ -14,6 +14,7 @@ class CrawlscopeConfigurationTest < Minitest::Test
|
|
|
14
14
|
config.site_name = -> { "Example" }
|
|
15
15
|
config.concurrency = -> { 4 }
|
|
16
16
|
config.fetch_executor = -> { :threaded }
|
|
17
|
+
config.profile_token = -> { "profile-token" }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
audit = Crawlscope.configuration.audit
|
|
@@ -22,6 +23,7 @@ class CrawlscopeConfigurationTest < Minitest::Test
|
|
|
22
23
|
assert_equal "/tmp/sitemap.xml", audit.instance_variable_get(:@sitemap_path)
|
|
23
24
|
assert_equal 4, audit.instance_variable_get(:@concurrency)
|
|
24
25
|
assert_equal :threaded, audit.instance_variable_get(:@fetch_executor)
|
|
26
|
+
assert_equal "profile-token", audit.instance_variable_get(:@profile_token)
|
|
25
27
|
assert_equal %i[
|
|
26
28
|
indexability
|
|
27
29
|
metadata
|
|
@@ -53,17 +55,35 @@ class CrawlscopeConfigurationTest < Minitest::Test
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def test_defaults_are_normalized
|
|
56
|
-
|
|
58
|
+
with_profile_token(nil) do
|
|
59
|
+
config = Crawlscope::Configuration.new
|
|
60
|
+
|
|
61
|
+
assert_equal [200, 301, 302], config.allowed_statuses
|
|
62
|
+
assert_equal 10, config.concurrency
|
|
63
|
+
assert_equal :async, config.fetch_executor
|
|
64
|
+
assert_equal 4, config.browser_concurrency
|
|
65
|
+
assert_equal 5, config.network_idle_timeout_seconds
|
|
66
|
+
assert_equal :http, config.renderer
|
|
67
|
+
assert_equal 20, config.timeout_seconds
|
|
68
|
+
assert_equal $stdout, config.output
|
|
69
|
+
assert_nil config.profile_token
|
|
70
|
+
assert config.scroll_page?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
def test_profile_token_defaults_to_the_environment
|
|
75
|
+
with_profile_token("environment-profile-token") do
|
|
76
|
+
assert_equal "environment-profile-token", Crawlscope::Configuration.new.profile_token
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_configured_profile_token_precedes_the_environment
|
|
81
|
+
with_profile_token("environment-profile-token") do
|
|
82
|
+
config = Crawlscope::Configuration.new
|
|
83
|
+
config.profile_token = "configured-profile-token"
|
|
84
|
+
|
|
85
|
+
assert_equal "configured-profile-token", config.profile_token
|
|
86
|
+
end
|
|
67
87
|
end
|
|
68
88
|
|
|
69
89
|
def test_browser_renderer_defaults_to_threaded_fetch_executor
|
|
@@ -119,4 +139,24 @@ class CrawlscopeConfigurationTest < Minitest::Test
|
|
|
119
139
|
|
|
120
140
|
assert_equal "Crawlscope concurrency must be an integer >= 1", error.message
|
|
121
141
|
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def with_profile_token(value)
|
|
146
|
+
previous_value = ENV["CRAWLSCOPE_PROFILE_TOKEN"]
|
|
147
|
+
|
|
148
|
+
if value.nil?
|
|
149
|
+
ENV.delete("CRAWLSCOPE_PROFILE_TOKEN")
|
|
150
|
+
else
|
|
151
|
+
ENV["CRAWLSCOPE_PROFILE_TOKEN"] = value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
yield
|
|
155
|
+
ensure
|
|
156
|
+
if previous_value.nil?
|
|
157
|
+
ENV.delete("CRAWLSCOPE_PROFILE_TOKEN")
|
|
158
|
+
else
|
|
159
|
+
ENV["CRAWLSCOPE_PROFILE_TOKEN"] = previous_value
|
|
160
|
+
end
|
|
161
|
+
end
|
|
122
162
|
end
|
|
@@ -167,6 +167,35 @@ class CrawlscopeCrawlTest < Minitest::Test
|
|
|
167
167
|
].sort, result.issues.to_a.map(&:code).uniq.sort
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
def test_profile_token_reaches_remote_sitemap_and_page_requests
|
|
171
|
+
sitemap_request = stub_request(:get, "https://example.com/sitemap.xml")
|
|
172
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
173
|
+
.to_return(
|
|
174
|
+
status: 200,
|
|
175
|
+
body: <<~XML
|
|
176
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
177
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
178
|
+
<url><loc>https://example.com/about</loc></url>
|
|
179
|
+
</urlset>
|
|
180
|
+
XML
|
|
181
|
+
)
|
|
182
|
+
page_request = stub_request(:get, "https://example.com/about")
|
|
183
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
184
|
+
.to_return(status: 200, body: "<html><body>About</body></html>")
|
|
185
|
+
|
|
186
|
+
Crawlscope::Crawl.new(
|
|
187
|
+
base_url: "https://example.com",
|
|
188
|
+
sitemap_path: "https://example.com/sitemap.xml",
|
|
189
|
+
rules: [],
|
|
190
|
+
schema_registry: Crawlscope::SchemaRegistry.default,
|
|
191
|
+
fetch_executor: :threaded,
|
|
192
|
+
profile_token: "profile-token"
|
|
193
|
+
).call
|
|
194
|
+
|
|
195
|
+
assert_requested sitemap_request
|
|
196
|
+
assert_requested page_request
|
|
197
|
+
end
|
|
198
|
+
|
|
170
199
|
def test_uses_browser_when_renderer_is_browser
|
|
171
200
|
File.write(
|
|
172
201
|
@sitemap_path,
|
|
@@ -5,13 +5,24 @@ require "test_helper"
|
|
|
5
5
|
class CrawlscopeHttpTest < Minitest::Test
|
|
6
6
|
def test_fetch_parses_html_response
|
|
7
7
|
stub_request(:get, "https://example.com/page")
|
|
8
|
-
.to_return(
|
|
8
|
+
.to_return(
|
|
9
|
+
status: 200,
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type" => "text/html",
|
|
12
|
+
"Server-Timing" => 'db;dur=12.5;desc="Primary database"'
|
|
13
|
+
},
|
|
14
|
+
body: "<html><body>Hello</body></html>"
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
page = Crawlscope::Http.new(base_url: "https://example.com", timeout_seconds: 2).fetch("https://example.com/page")
|
|
11
18
|
|
|
12
19
|
assert_equal 200, page.status
|
|
13
20
|
assert page.html?
|
|
14
21
|
assert_equal "Hello", page.doc.at_css("body").text
|
|
22
|
+
timing = page.server_timing
|
|
23
|
+
assert_same timing, page.server_timing
|
|
24
|
+
assert_equal 12.5, timing.first.duration
|
|
25
|
+
assert_equal "Primary database", timing.first.description
|
|
15
26
|
end
|
|
16
27
|
|
|
17
28
|
def test_fetch_parses_responses_without_content_type_as_html
|
|
@@ -23,6 +34,54 @@ class CrawlscopeHttpTest < Minitest::Test
|
|
|
23
34
|
assert page.html?
|
|
24
35
|
end
|
|
25
36
|
|
|
37
|
+
def test_fetch_sends_profile_token_to_same_origin_requests
|
|
38
|
+
request = stub_request(:get, "https://example.com/page")
|
|
39
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
40
|
+
.to_return(status: 200, body: "<html></html>")
|
|
41
|
+
|
|
42
|
+
Crawlscope::Http.new(
|
|
43
|
+
base_url: "https://example.com",
|
|
44
|
+
timeout_seconds: 2,
|
|
45
|
+
profile_token: "profile-token"
|
|
46
|
+
).fetch("https://example.com/page")
|
|
47
|
+
|
|
48
|
+
assert_requested request
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_fetch_preserves_profile_token_on_same_origin_redirects
|
|
52
|
+
stub_request(:get, "https://example.com/start")
|
|
53
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
54
|
+
.to_return(status: 302, headers: {"Location" => "/final"})
|
|
55
|
+
final_request = stub_request(:get, "https://example.com/final")
|
|
56
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
57
|
+
.to_return(status: 200, body: "<html></html>")
|
|
58
|
+
|
|
59
|
+
Crawlscope::Http.new(
|
|
60
|
+
base_url: "https://example.com",
|
|
61
|
+
timeout_seconds: 2,
|
|
62
|
+
profile_token: "profile-token"
|
|
63
|
+
).fetch("https://example.com/start")
|
|
64
|
+
|
|
65
|
+
assert_requested final_request
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_fetch_removes_profile_token_from_cross_origin_redirects
|
|
69
|
+
stub_request(:get, "https://example.com/start")
|
|
70
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
71
|
+
.to_return(status: 302, headers: {"Location" => "https://other.example/final"})
|
|
72
|
+
final_request = stub_request(:get, "https://other.example/final")
|
|
73
|
+
.with { |request| !request.headers.key?("X-Profile-Token") }
|
|
74
|
+
.to_return(status: 200, body: "<html></html>")
|
|
75
|
+
|
|
76
|
+
Crawlscope::Http.new(
|
|
77
|
+
base_url: "https://example.com",
|
|
78
|
+
timeout_seconds: 2,
|
|
79
|
+
profile_token: "profile-token"
|
|
80
|
+
).fetch("https://example.com/start")
|
|
81
|
+
|
|
82
|
+
assert_requested final_request
|
|
83
|
+
end
|
|
84
|
+
|
|
26
85
|
def test_fetch_leaves_non_html_response_unparsed
|
|
27
86
|
stub_request(:get, "https://example.com/feed.xml")
|
|
28
87
|
.to_return(status: 200, headers: {"content-type" => "application/xml"}, body: "<feed></feed>")
|
|
@@ -16,11 +16,14 @@ class CrawlscopeInstallGeneratorTest < Minitest::Test
|
|
|
16
16
|
Crawlscope::Generators::InstallGenerator.start([], destination_root: destination)
|
|
17
17
|
|
|
18
18
|
initializer = File.join(destination, "config/initializers/crawlscope.rb")
|
|
19
|
+
initializer_contents = File.read(initializer)
|
|
19
20
|
rakefile = File.read(File.join(destination, "Rakefile"))
|
|
20
21
|
|
|
21
22
|
assert File.exist?(initializer)
|
|
22
|
-
assert_includes
|
|
23
|
-
assert_includes
|
|
23
|
+
assert_includes initializer_contents, "module CrawlscopeConfiguration"
|
|
24
|
+
assert_includes initializer_contents, "CrawlscopeConfiguration.apply if defined?(Crawlscope)"
|
|
25
|
+
assert_includes initializer_contents, "\#{config.base_url.to_s.chomp(\"/\")}/sitemap.xml"
|
|
26
|
+
refute_includes initializer_contents, "Rails.public_path"
|
|
24
27
|
assert_includes rakefile, 'require "crawlscope/tasks"'
|
|
25
28
|
assert_operator rakefile.index('require "crawlscope/tasks"'), :<, rakefile.index('require_relative "config/application"')
|
|
26
29
|
|
|
@@ -28,14 +31,6 @@ class CrawlscopeInstallGeneratorTest < Minitest::Test
|
|
|
28
31
|
assert status.success?, stderr
|
|
29
32
|
|
|
30
33
|
script = <<~RUBY
|
|
31
|
-
require "pathname"
|
|
32
|
-
|
|
33
|
-
module Rails
|
|
34
|
-
def self.public_path
|
|
35
|
-
Pathname.new("public")
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
34
|
require "crawlscope"
|
|
40
35
|
load ARGV.fetch(0)
|
|
41
36
|
|
|
@@ -43,7 +38,10 @@ class CrawlscopeInstallGeneratorTest < Minitest::Test
|
|
|
43
38
|
CrawlscopeConfiguration.apply
|
|
44
39
|
abort "configuration replaced" unless Crawlscope.configuration.equal?(configuration)
|
|
45
40
|
abort "base URL missing" unless configuration.base_url == "http://localhost:3000"
|
|
46
|
-
abort "sitemap missing" unless configuration.sitemap_path == "
|
|
41
|
+
abort "sitemap missing" unless configuration.sitemap_path == "http://localhost:3000/sitemap.xml"
|
|
42
|
+
|
|
43
|
+
ENV["CRAWLSCOPE_BASE_URL"] = "https://example.com"
|
|
44
|
+
abort "configured sitemap missing" unless configuration.sitemap_path == "https://example.com/sitemap.xml"
|
|
47
45
|
RUBY
|
|
48
46
|
_stdout, stderr, status = Open3.capture3(
|
|
49
47
|
RbConfig.ruby,
|