crawlscope 0.3.0 → 0.5.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.
@@ -23,7 +23,7 @@ class CrawlscopeReporterTest < Minitest::Test
23
23
  refute_includes output, "Status: FAILED"
24
24
  end
25
25
 
26
- def test_reports_failed_result_with_severity_counts
26
+ def test_reports_failed_result_with_grouped_counts_and_offenses
27
27
  io = StringIO.new
28
28
  issues = Crawlscope::IssueCollection.new
29
29
  issues.add(code: :missing_title, severity: :warning, category: :metadata, url: "https://example.com/a", message: "missing <title>", details: {})
@@ -42,9 +42,13 @@ class CrawlscopeReporterTest < Minitest::Test
42
42
 
43
43
  assert_includes output, "Status: FAILED"
44
44
  assert_includes output, "Issues: 2"
45
+ assert_includes output, "Severity:"
45
46
  assert_includes output, "notice: 1"
46
47
  assert_includes output, "warning: 1"
47
- assert_includes output, "- [warning] https://example.com/a missing <title>"
48
- assert_includes output, "- [notice] https://example.com/b broken internal link"
48
+ assert_includes output, "Category:"
49
+ assert_includes output, "links: 1"
50
+ assert_includes output, "metadata: 1"
51
+ assert_includes output, " - [warning] missing_title https://example.com/a missing <title>"
52
+ assert_includes output, " - [notice] broken_internal_link https://example.com/b broken internal link"
49
53
  end
50
54
  end
@@ -49,6 +49,30 @@ class CrawlscopeSitemapTest < Minitest::Test
49
49
  assert_equal ["https://www.example.com/features/reviews"], parser.urls(base_url: "https://www.example.com")
50
50
  end
51
51
 
52
+ def test_remote_sitemap_http_error_is_explicit
53
+ stub_request(:get, "https://www.example.com/sitemap.xml")
54
+ .to_return(status: 500, body: "<html><body>Error</body></html>")
55
+
56
+ parser = Crawlscope::Sitemap.new(path: "https://www.example.com/sitemap.xml")
57
+
58
+ error = assert_raises(Crawlscope::ValidationError) do
59
+ parser.urls(base_url: "https://www.example.com")
60
+ end
61
+ assert_equal "Sitemap https://www.example.com/sitemap.xml returned HTTP 500", error.message
62
+ end
63
+
64
+ def test_invalid_sitemap_root_is_explicit
65
+ stub_request(:get, "https://www.example.com/sitemap.xml")
66
+ .to_return(status: 200, body: "<html><body>Error</body></html>")
67
+
68
+ parser = Crawlscope::Sitemap.new(path: "https://www.example.com/sitemap.xml")
69
+
70
+ error = assert_raises(Crawlscope::ValidationError) do
71
+ parser.urls(base_url: "https://www.example.com")
72
+ end
73
+ assert_equal 'Sitemap https://www.example.com/sitemap.xml has unexpected root "html"', error.message
74
+ end
75
+
52
76
  def test_rebases_remote_sitemap_index_children_to_base_url
53
77
  stub_request(:get, "http://localhost:3000/sitemap.xml")
54
78
  .to_return(
@@ -79,6 +79,62 @@ class CrawlscopeStructuredDataRuleTest < Minitest::Test
79
79
  assert_equal ["json-ld", "microdata"], issues.to_a.first.details[:expected_sources]
80
80
  end
81
81
 
82
+ def test_reports_structured_data_missing_type
83
+ issues = Crawlscope::IssueCollection.new
84
+ rule = Crawlscope::Rules::StructuredData.new
85
+ page = page(
86
+ url: "https://example.com/articles/test",
87
+ body: <<~HTML
88
+ <html>
89
+ <head>
90
+ <script type="application/ld+json">
91
+ {"@context":"https://schema.org","headline":"Untyped article"}
92
+ </script>
93
+ </head>
94
+ <body><h1>Article</h1></body>
95
+ </html>
96
+ HTML
97
+ )
98
+
99
+ rule.call(
100
+ urls: [page.url],
101
+ pages: [page],
102
+ issues: issues,
103
+ context: {schema_registry: Crawlscope::SchemaRegistry.default}
104
+ )
105
+
106
+ assert_includes issues.to_a.map(&:code), :structured_data_missing_type
107
+ end
108
+
109
+ def test_reports_graph_entries_missing_type
110
+ issues = Crawlscope::IssueCollection.new
111
+ rule = Crawlscope::Rules::StructuredData.new
112
+ page = page(
113
+ url: "https://example.com/articles/test",
114
+ body: <<~HTML
115
+ <html>
116
+ <head>
117
+ <script type="application/ld+json">
118
+ {"@context":"https://schema.org","@type":"WebPage","@graph":[{"name":"Untyped node"}]}
119
+ </script>
120
+ </head>
121
+ <body><h1>Article</h1></body>
122
+ </html>
123
+ HTML
124
+ )
125
+
126
+ rule.call(
127
+ urls: [page.url],
128
+ pages: [page],
129
+ issues: issues,
130
+ context: {schema_registry: Crawlscope::SchemaRegistry.default}
131
+ )
132
+
133
+ issue = issues.to_a.find { |item| item.code == :structured_data_missing_type }
134
+ assert issue
135
+ assert_equal ["$.@graph[0]"], issue.details[:paths]
136
+ end
137
+
82
138
  def test_validates_job_posting_markup
83
139
  issues = Crawlscope::IssueCollection.new
84
140
  rule = Crawlscope::Rules::StructuredData.new
@@ -13,18 +13,74 @@ class CrawlscopeUniquenessRuleTest < Minitest::Test
13
13
 
14
14
  rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
15
15
 
16
- assert_equal %i[duplicate_content_fingerprint duplicate_meta_description duplicate_title].sort, issues.to_a.map(&:code).sort
16
+ assert_equal %i[duplicate_content_fingerprint duplicate_meta_description duplicate_pages_without_canonical duplicate_title].sort, issues.to_a.map(&:code).sort
17
+ end
18
+
19
+ def test_allows_duplicate_pages_when_canonicals_are_present
20
+ issues = Crawlscope::IssueCollection.new
21
+ rule = Crawlscope::Rules::Uniqueness.new
22
+ pages = [
23
+ page(url: "https://example.com/a", canonical: "https://example.com/a"),
24
+ page(url: "https://example.com/b", canonical: "https://example.com/a")
25
+ ]
26
+
27
+ rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
28
+
29
+ refute_includes issues.to_a.map(&:code), :duplicate_pages_without_canonical
30
+ end
31
+
32
+ def test_reports_near_duplicate_content
33
+ issues = Crawlscope::IssueCollection.new
34
+ rule = Crawlscope::Rules::Uniqueness.new
35
+ pages = [
36
+ page(url: "https://example.com/a", content: near_duplicate_content("reliable")),
37
+ page(url: "https://example.com/b", content: near_duplicate_content("dependable"))
38
+ ]
39
+
40
+ rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
41
+
42
+ issue = issues.to_a.find { |item| item.code == :near_duplicate_content }
43
+ assert issue
44
+ assert_operator issue.details[:similarity], :>=, issue.details[:threshold]
45
+ end
46
+
47
+ def test_skips_near_duplicate_scan_when_page_count_exceeds_limit
48
+ issues = Crawlscope::IssueCollection.new
49
+ rule = Crawlscope::Rules::Uniqueness.new(max_near_duplicate_pages: 1)
50
+ pages = [
51
+ page(url: "https://example.com/a", content: near_duplicate_content("reliable")),
52
+ page(url: "https://example.com/b", content: near_duplicate_content("dependable"))
53
+ ]
54
+
55
+ rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
56
+
57
+ skip_issue = issues.to_a.find { |item| item.code == :near_duplicate_scan_skipped }
58
+ refute issues.to_a.any? { |item| item.code == :near_duplicate_content }
59
+ assert_equal :warning, skip_issue.severity
60
+ assert_equal({max_pages: 1, page_count: 2}, skip_issue.details)
17
61
  end
18
62
 
19
63
  private
20
64
 
21
- def page(url:)
22
- repeated_text = ("Useful content " * 30).strip
65
+ def near_duplicate_content(adjective)
66
+ <<~TEXT.gsub(/\s+/, " ").strip
67
+ This page summarizes practical hotel review patterns for operators who need #{adjective}
68
+ service insights across locations. It compares recurring comments about staff, rooms,
69
+ cleanliness, check-in, breakfast, parking, and amenities so teams can prioritize fixes.
70
+ The analysis highlights repeat themes, explains why guests mention them, and keeps the
71
+ wording focused on decisions that improve daily operations.
72
+ TEXT
73
+ end
74
+
75
+ def page(url:, content: nil, canonical: nil)
76
+ repeated_text = content || ("Useful content " * 30).strip
77
+ canonical_tag = canonical ? %(<link rel="canonical" href="#{canonical}">) : ""
23
78
  body = <<~HTML
24
79
  <html>
25
80
  <head>
26
81
  <title>Example Title</title>
27
82
  <meta name="description" content="Example description">
83
+ #{canonical_tag}
28
84
  </head>
29
85
  <body>
30
86
  <main>#{repeated_text}</main>
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "rake"
5
+
6
+ unless respond_to?(:release_version, true)
7
+ load File.expand_path("../Rakefile", __dir__)
8
+ end
9
+
10
+ class ReleaseTaskTest < Minitest::Test
11
+ def test_release_version_increments_patch_from_current_version
12
+ major, minor, patch = Crawlscope::VERSION.split(".").map(&:to_i)
13
+
14
+ assert_equal "#{major}.#{minor}.#{patch + 1}", release_version("patch")
15
+ end
16
+
17
+ def test_release_version_accepts_explicit_semantic_version
18
+ assert_equal "0.3.0", release_version("0.3.0")
19
+ end
20
+
21
+ def test_validate_release_version_rejects_current_version
22
+ error = assert_raises(ArgumentError) do
23
+ validate_release_version!("0.2.7", "0.2.7")
24
+ end
25
+
26
+ assert_equal(
27
+ "Release version 0.2.7 must be newer than current version 0.2.7.",
28
+ error.message
29
+ )
30
+ end
31
+
32
+ def test_validate_release_version_rejects_existing_local_tag
33
+ @local_release_tag_exists = true
34
+ @remote_release_tag_exists = false
35
+
36
+ error = assert_raises(ArgumentError) do
37
+ validate_release_version!("0.2.8", "0.2.7")
38
+ end
39
+
40
+ assert_equal "Release tag v0.2.8 already exists locally.", error.message
41
+ end
42
+
43
+ def test_validate_release_version_rejects_existing_remote_tag
44
+ @local_release_tag_exists = false
45
+ @remote_release_tag_exists = true
46
+
47
+ error = assert_raises(ArgumentError) do
48
+ validate_release_version!("0.2.8", "0.2.7")
49
+ end
50
+
51
+ assert_equal "Release tag v0.2.8 already exists on origin.", error.message
52
+ end
53
+
54
+ def test_remote_release_tag_command_asks_git_to_fail_when_no_tag_matches
55
+ assert_equal(
56
+ "git ls-remote --exit-code --tags origin refs/tags/v0.2.8",
57
+ remote_release_tag_command("v0.2.8")
58
+ )
59
+ end
60
+
61
+ def test_changelog_command_prepends_the_next_release
62
+ assert_equal(
63
+ [
64
+ "git-cliff",
65
+ "-c",
66
+ "cliff.toml",
67
+ "--unreleased",
68
+ "--tag",
69
+ "v0.2.8",
70
+ "--prepend",
71
+ "CHANGELOG.md"
72
+ ],
73
+ changelog_command("0.2.8")
74
+ )
75
+ end
76
+
77
+ private
78
+
79
+ def local_release_tag_exists?(_tag)
80
+ @local_release_tag_exists || false
81
+ end
82
+
83
+ def remote_release_tag_exists?(_tag)
84
+ @remote_release_tag_exists || false
85
+ end
86
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crawlscope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Fidalgo
@@ -199,6 +199,7 @@ files:
199
199
  - lib/crawlscope/context.rb
200
200
  - lib/crawlscope/crawl.rb
201
201
  - lib/crawlscope/crawler.rb
202
+ - lib/crawlscope/document_text.rb
202
203
  - lib/crawlscope/http.rb
203
204
  - lib/crawlscope/issue.rb
204
205
  - lib/crawlscope/issue_collection.rb
@@ -208,6 +209,8 @@ files:
208
209
  - lib/crawlscope/reporter.rb
209
210
  - lib/crawlscope/result.rb
210
211
  - lib/crawlscope/rule_registry.rb
212
+ - lib/crawlscope/rules/content_quality.rb
213
+ - lib/crawlscope/rules/indexability.rb
211
214
  - lib/crawlscope/rules/links.rb
212
215
  - lib/crawlscope/rules/metadata.rb
213
216
  - lib/crawlscope/rules/structured_data.rb
@@ -228,12 +231,15 @@ files:
228
231
  - test/crawlscope/browser_test.rb
229
232
  - test/crawlscope/cli_test.rb
230
233
  - test/crawlscope/configuration_test.rb
234
+ - test/crawlscope/content_quality_rule_test.rb
231
235
  - test/crawlscope/crawl_test.rb
232
236
  - test/crawlscope/crawler_test.rb
233
237
  - test/crawlscope/http_test.rb
238
+ - test/crawlscope/indexability_rule_test.rb
234
239
  - test/crawlscope/links_rule_test.rb
235
240
  - test/crawlscope/loader_test.rb
236
241
  - test/crawlscope/metadata_rule_test.rb
242
+ - test/crawlscope/rake_tasks_test.rb
237
243
  - test/crawlscope/reporter_test.rb
238
244
  - test/crawlscope/rule_registry_test.rb
239
245
  - test/crawlscope/run_test.rb
@@ -247,6 +253,7 @@ files:
247
253
  - test/crawlscope/structured_data_writer_test.rb
248
254
  - test/crawlscope/uniqueness_rule_test.rb
249
255
  - test/crawlscope/url_test.rb
256
+ - test/release_task_test.rb
250
257
  - test/test_helper.rb
251
258
  homepage: https://www.ethos-link.com/opensource/crawlscope
252
259
  licenses:
@@ -275,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
275
282
  - !ruby/object:Gem::Version
276
283
  version: '0'
277
284
  requirements: []
278
- rubygems_version: 4.0.6
285
+ rubygems_version: 4.0.10
279
286
  specification_version: 4
280
287
  summary: Audit sitemap URLs for metadata, structured data, uniqueness, and links
281
288
  test_files: []