crawlscope 0.7.2 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +95 -18
- data/lib/crawlscope/browser.rb +18 -1
- data/lib/crawlscope/configuration.rb +6 -1
- data/lib/crawlscope/crawl.rb +5 -3
- data/lib/crawlscope/http.rb +13 -2
- data/lib/crawlscope/page.rb +8 -0
- data/lib/crawlscope/reporter.rb +144 -61
- data/lib/crawlscope/request_headers.rb +34 -0
- data/lib/crawlscope/result.rb +4 -0
- data/lib/crawlscope/rules/content_quality.rb +5 -5
- data/lib/crawlscope/rules/indexability.rb +1 -1
- data/lib/crawlscope/rules/links.rb +28 -34
- data/lib/crawlscope/rules/metadata.rb +25 -25
- data/lib/crawlscope/rules/structured_data.rb +13 -4
- data/lib/crawlscope/rules/uniqueness.rb +9 -7
- data/lib/crawlscope/run.rb +0 -10
- data/lib/crawlscope/schema_registry.rb +17 -1
- data/lib/crawlscope/server_timing/reporter.rb +114 -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/structured_data/error_formatter.rb +42 -0
- data/lib/crawlscope/structured_data/reporter.rb +10 -10
- 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 +65 -0
- data/test/crawlscope/http_test.rb +60 -1
- data/test/crawlscope/install_generator_test.rb +9 -11
- data/test/crawlscope/links_rule_test.rb +40 -4
- data/test/crawlscope/metadata_rule_test.rb +37 -4
- data/test/crawlscope/reporter_test.rb +249 -20
- data/test/crawlscope/run_test.rb +4 -12
- data/test/crawlscope/schema_registry_test.rb +33 -0
- 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
- data/test/crawlscope/structured_data_error_formatter_test.rb +46 -0
- data/test/crawlscope/structured_data_reporter_test.rb +26 -0
- data/test/crawlscope/structured_data_rule_test.rb +2 -0
- data/test/crawlscope/uniqueness_rule_test.rb +46 -3
- metadata +10 -2
|
@@ -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,
|
|
@@ -296,6 +325,42 @@ class CrawlscopeCrawlTest < Minitest::Test
|
|
|
296
325
|
assert_includes result.issues.to_a.map(&:code), :sitemap_redirect_url
|
|
297
326
|
end
|
|
298
327
|
|
|
328
|
+
def test_reports_redirects_without_duplicate_destination_content
|
|
329
|
+
File.write(
|
|
330
|
+
@sitemap_path,
|
|
331
|
+
<<~XML
|
|
332
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
333
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
334
|
+
<url><loc>https://example.com/old</loc></url>
|
|
335
|
+
<url><loc>https://example.com/alias</loc></url>
|
|
336
|
+
<url><loc>https://example.com/new</loc></url>
|
|
337
|
+
</urlset>
|
|
338
|
+
XML
|
|
339
|
+
)
|
|
340
|
+
["old", "alias"].each do |path|
|
|
341
|
+
stub_request(:get, "https://example.com/#{path}")
|
|
342
|
+
.to_return(status: 301, headers: {"Location" => "https://example.com/new"}, body: "")
|
|
343
|
+
end
|
|
344
|
+
body = %(<html><head><title>Current page</title><meta name="description" content="Current description"><link rel="canonical" href="https://example.com/new"></head><body>#{"Useful content " * 30}</body></html>)
|
|
345
|
+
stub_request(:get, "https://example.com/new")
|
|
346
|
+
.to_return(status: 200, headers: {"Content-Type" => "text/html"}, body: body)
|
|
347
|
+
|
|
348
|
+
result = Crawlscope::Crawl.new(
|
|
349
|
+
base_url: "https://example.com",
|
|
350
|
+
sitemap_path: @sitemap_path,
|
|
351
|
+
rules: [Crawlscope::Rules::Uniqueness.new, Crawlscope::Rules::Metadata.new],
|
|
352
|
+
schema_registry: Crawlscope::SchemaRegistry.default,
|
|
353
|
+
fetch_executor: :threaded
|
|
354
|
+
).call
|
|
355
|
+
|
|
356
|
+
assert_equal 3, result.pages.size
|
|
357
|
+
assert_equal ["https://example.com/old", "https://example.com/alias"], result.issues.select { |issue| issue.code == :sitemap_redirect_url }.map(&:url)
|
|
358
|
+
refute_includes result.issues.to_a.map(&:code), :redirected_page
|
|
359
|
+
assert_empty result.issues.select { |issue| issue.category == :uniqueness }
|
|
360
|
+
assert_empty result.issues.select { |issue| [:canonical_mismatch, :non_canonical_page_in_sitemap].include?(issue.code) }
|
|
361
|
+
assert_equal 1, result.issues.count { |issue| issue.code == :meta_description_too_short }
|
|
362
|
+
end
|
|
363
|
+
|
|
299
364
|
def test_resolves_uncrawled_link_targets_as_a_bounded_batch
|
|
300
365
|
File.write(
|
|
301
366
|
@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,
|
|
@@ -56,7 +56,7 @@ class CrawlscopeLinksRuleTest < Minitest::Test
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
assert_includes issues.to_a.map(&:code), :unresolved_internal_link
|
|
59
|
-
assert_includes issues.to_a.find { |issue| issue.code == :unresolved_internal_link }.message, "
|
|
59
|
+
assert_includes issues.to_a.find { |issue| issue.code == :unresolved_internal_link }.message, "cannot check linked URL"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def test_ignores_fetch_errors_for_urls_already_crawled
|
|
@@ -429,6 +429,42 @@ class CrawlscopeLinksRuleTest < Minitest::Test
|
|
|
429
429
|
assert_empty issues.to_a
|
|
430
430
|
end
|
|
431
431
|
|
|
432
|
+
def test_retains_all_link_sources_and_targets_in_issue_details
|
|
433
|
+
source_urls = 5.times.map { |index| "https://example.com/source-#{index}" }
|
|
434
|
+
target_urls = 5.times.map { |index| "https://example.com/target-#{index}" }
|
|
435
|
+
pages = source_urls.map do |url|
|
|
436
|
+
targets = target_urls.map { |target| %(<a href="#{target}" rel="nofollow">Target</a>) }.join
|
|
437
|
+
page(url: url, body: %(<html><body>#{targets}<a href="/missing">Missing</a><a href="/pricing">Pricing</a></body></html>))
|
|
438
|
+
end
|
|
439
|
+
resolver = lambda do |url|
|
|
440
|
+
{crawled: false, error: nil, final_url: url, html: true, status: url.end_with?("/missing") ? 404 : 200}
|
|
441
|
+
end
|
|
442
|
+
issues = Crawlscope::IssueCollection.new
|
|
443
|
+
|
|
444
|
+
Crawlscope::Rules::Links.new.call(urls: source_urls, pages: pages, issues: issues, context: context(resolver: resolver))
|
|
445
|
+
|
|
446
|
+
broken = issues.find { |issue| issue.code == :broken_internal_link }
|
|
447
|
+
assert_equal source_urls, broken.details[:source_urls]
|
|
448
|
+
outgoing = issues.select { |issue| issue.code == :nofollow_internal_outlinks }
|
|
449
|
+
assert_equal 5, outgoing.size
|
|
450
|
+
outgoing.each { |issue| assert_equal target_urls, issue.details[:target_urls] }
|
|
451
|
+
missing = issues.find { |issue| issue.code == :indexable_page_missing_from_sitemap && issue.url.end_with?("/pricing") }
|
|
452
|
+
assert_equal source_urls.first, missing.details[:source_url]
|
|
453
|
+
assert_equal source_urls, missing.details[:source_urls]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def test_does_not_check_destination_canonicals_under_redirect_aliases
|
|
457
|
+
pages = ["old", "alias"].map do |path|
|
|
458
|
+
page(url: "https://example.com/#{path}", final_url: "https://example.com/guide", body: '<html><head><link rel="canonical" href="https://example.com/missing"></head></html>')
|
|
459
|
+
end
|
|
460
|
+
issues = Crawlscope::IssueCollection.new
|
|
461
|
+
|
|
462
|
+
Crawlscope::Rules::Links.new.call(urls: pages.map(&:url), pages: pages, issues: issues, context: context)
|
|
463
|
+
|
|
464
|
+
canonical_issues = issues.select { |issue| [:canonical_no_internal_inlinks, :canonical_points_to_redirect, :canonical_points_to_error].include?(issue.code) }
|
|
465
|
+
assert_empty canonical_issues
|
|
466
|
+
end
|
|
467
|
+
|
|
432
468
|
private
|
|
433
469
|
|
|
434
470
|
def context(resolver: method(:resolve_target))
|
|
@@ -439,14 +475,14 @@ class CrawlscopeLinksRuleTest < Minitest::Test
|
|
|
439
475
|
}
|
|
440
476
|
end
|
|
441
477
|
|
|
442
|
-
def page(url:, body:)
|
|
478
|
+
def page(url:, body:, final_url: url)
|
|
443
479
|
doc = Nokogiri::HTML(body)
|
|
444
480
|
|
|
445
481
|
Crawlscope::Page.new(
|
|
446
482
|
url: url,
|
|
447
483
|
normalized_url: url,
|
|
448
|
-
final_url:
|
|
449
|
-
normalized_final_url:
|
|
484
|
+
final_url: final_url,
|
|
485
|
+
normalized_final_url: final_url,
|
|
450
486
|
status: 200,
|
|
451
487
|
headers: {"content-type" => "text/html"},
|
|
452
488
|
body: body,
|
|
@@ -80,13 +80,46 @@ class CrawlscopeMetadataRuleTest < Minitest::Test
|
|
|
80
80
|
assert_includes codes, :multiple_title_tags
|
|
81
81
|
assert_includes codes, :multiple_meta_descriptions
|
|
82
82
|
assert_includes codes, :empty_h1
|
|
83
|
-
|
|
83
|
+
refute_includes codes, :canonical_mismatch
|
|
84
84
|
assert_includes codes, :non_canonical_page_in_sitemap
|
|
85
|
+
mismatch = issues.find { |issue| issue.code == :non_canonical_page_in_sitemap }
|
|
86
|
+
assert_equal "sitemap lists this URL, but its canonical points to https://example.com/canonical-about", mismatch.message
|
|
87
|
+
assert_equal "https://example.com/canonical-about", mismatch.details[:canonical]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_length_messages_include_measurement_units_and_limit
|
|
91
|
+
issues = Crawlscope::IssueCollection.new
|
|
92
|
+
long_page = page(body: %(<html><head><title>#{"T" * 73}</title><meta name="description" content="#{"D" * 161}"></head></html>))
|
|
93
|
+
|
|
94
|
+
Crawlscope::Rules::Metadata.new.call(urls: [long_page.url], pages: [long_page], issues: issues)
|
|
95
|
+
|
|
96
|
+
assert_equal "title: 73 characters; maximum 72", issues.find { |issue| issue.code == :title_too_long }.message
|
|
97
|
+
assert_equal "meta description: 161 characters; maximum 160", issues.find { |issue| issue.code == :meta_description_too_long }.message
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_does_not_apply_destination_metadata_to_redirect_alias
|
|
101
|
+
redirected = page(url: "https://example.com/old", final_url: "https://example.com/new", body: '<html><head><link rel="canonical" href="https://example.com/new"></head></html>')
|
|
102
|
+
issues = Crawlscope::IssueCollection.new
|
|
103
|
+
|
|
104
|
+
Crawlscope::Rules::Metadata.new.call(urls: [redirected.url], pages: [redirected], issues: issues)
|
|
105
|
+
|
|
106
|
+
assert_empty issues.to_a
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_keeps_canonical_mismatch_for_a_page_outside_the_sitemap
|
|
110
|
+
mismatched = page(body: '<html><head><link rel="canonical" href="https://example.com/preferred"></head></html>')
|
|
111
|
+
issues = Crawlscope::IssueCollection.new
|
|
112
|
+
|
|
113
|
+
Crawlscope::Rules::Metadata.new.call(urls: [], pages: [mismatched], issues: issues)
|
|
114
|
+
|
|
115
|
+
canonical_issues = issues.select { |issue| [:canonical_mismatch, :non_canonical_page_in_sitemap].include?(issue.code) }
|
|
116
|
+
assert_equal [:canonical_mismatch], canonical_issues.map(&:code)
|
|
117
|
+
assert_equal "https://example.com/preferred", canonical_issues.first.details[:canonical]
|
|
85
118
|
end
|
|
86
119
|
|
|
87
120
|
private
|
|
88
121
|
|
|
89
|
-
def page(url: "https://example.com/about", body: nil)
|
|
122
|
+
def page(url: "https://example.com/about", body: nil, final_url: url)
|
|
90
123
|
body ||= <<~HTML
|
|
91
124
|
<html>
|
|
92
125
|
<head>
|
|
@@ -102,8 +135,8 @@ class CrawlscopeMetadataRuleTest < Minitest::Test
|
|
|
102
135
|
Crawlscope::Page.new(
|
|
103
136
|
url: url,
|
|
104
137
|
normalized_url: Crawlscope::Url.normalize(url, base_url: url),
|
|
105
|
-
final_url:
|
|
106
|
-
normalized_final_url: Crawlscope::Url.normalize(
|
|
138
|
+
final_url: final_url,
|
|
139
|
+
normalized_final_url: Crawlscope::Url.normalize(final_url, base_url: url),
|
|
107
140
|
status: 200,
|
|
108
141
|
headers: {"content-type" => "text/html"},
|
|
109
142
|
body: body,
|