Skip to content

Commit 1c3a0a6

Browse files
committed
test(github-ingestor): all related code is 100% covered
1 parent 1ceba7f commit 1c3a0a6

6 files changed

Lines changed: 251 additions & 22 deletions

File tree

lib/ingestors/github_ingestor.rb

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ def read(source_url)
5555

5656
# Add to material
5757
add_material to_material(repo_data)
58-
rescue StandardError => e
59-
@messages << "#{self.class.name} read failed for #{url}, #{e.message}"
6058
end
59+
rescue StandardError => e
60+
@messages << "#{self.class.name} read failed, #{e.message}"
6161
end
6262

6363
private
@@ -92,26 +92,14 @@ def github_api_from_com(uri)
9292
# One GitHub URL equals to 4 GitHub API requests.
9393
# key: string key for the cache
9494
# ttl: time-to-live in seconds (default 7 days)
95-
def get_or_set_cache(key, url) # rubocop:disable Metrics/AbcSize
96-
content = open_url(url)
97-
data = content ? JSON.parse(content.read) : nil
98-
99-
set_cache(key, data) if Rails.cache.read(key).nil? # sets cache only if there is no cache yet or is expired
100-
get_cache(key)
101-
data
95+
def get_or_set_cache(key, url)
96+
Rails.cache.fetch(key, expires_in: TTL) do
97+
JSON.parse(open_url(url).read)
98+
end
10299
rescue StandardError => e
103100
@messages << "#{self.class.name} get_or_set_cache failed for #{url}, #{e.message}"
104-
yield
105-
end
106-
107-
def set_cache(key, data, ttl: TTL)
108-
Rails.logger.info "[Github Cache] SET cache #{key}"
109-
Rails.cache.write(key, data, expires_in: ttl) unless data.nil?
110-
end
111-
112-
def get_cache(key)
113-
Rails.logger.info "[Github Cache] GET cache #{key}"
114-
Rails.cache.read(key)
101+
yield if block_given?
102+
nil
115103
end
116104

117105
# Sets material hash keys and values and add them to material

test/fixtures/files/ingestion/github/mock.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@
99
<h1>Prerequisites</h1>
1010
<p>1. Be kind</p>
1111
</body>
12-
</html>
13-
Week 4: Packaging and Quality Control
12+
</html>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require 'test_helper'
2+
3+
class SitemapHelperTest < ActionView::TestCase
4+
setup do
5+
@ingestor = DummyIngestor.new
6+
@messages = []
7+
end
8+
9+
test 'get_sources with url only returns url only' do
10+
url = 'https://test.com'
11+
assert_equal @ingestor.send(:get_sources, url), [url]
12+
end
13+
14+
test 'get_sources returns parsed URLs from sitemap.xml' do
15+
sitemap_xml = <<~XML
16+
<?xml version="1.0" encoding="UTF-8"?>
17+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
18+
<url>
19+
<loc>https://app.com/events/123</loc>
20+
</url>
21+
<url>
22+
<loc>https://app.com/events/456</loc>
23+
</url>
24+
</urlset>
25+
XML
26+
27+
stub_request(:get, 'https://app.com/events/sitemap.xml')
28+
.to_return(status: 200, body: sitemap_xml, headers: { 'Content-Type' => 'application/xml' })
29+
30+
urls = @ingestor.send(:get_sources, 'https://app.com/events/sitemap.xml')
31+
assert_equal ['https://app.com/events/123', 'https://app.com/events/456'], urls
32+
end
33+
34+
test 'get_sources returns parsed URLs from sitemap.txt' do
35+
sitemap_txt = <<~TXT
36+
https://app.com/events/123
37+
https://app.com/events/456
38+
TXT
39+
40+
stub_request(:get, 'https://app.com/events/sitemap.txt')
41+
.to_return(status: 200, body: sitemap_txt, headers: { 'Content-Type' => 'txt' })
42+
43+
urls = @ingestor.send(:get_sources, 'https://app.com/events/sitemap.txt')
44+
assert_equal ['https://app.com/events/123', 'https://app.com/events/456'], urls
45+
end
46+
47+
private
48+
49+
def webmock(url, filename)
50+
file = Rails.root.join('test', 'fixtures', 'files', 'ingestion', filename)
51+
WebMock.stub_request(:get, url).to_return(status: 200, headers: {}, body: file.read)
52+
end
53+
end

test/test_helper.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,21 @@ def mock_facets
400400
end
401401
end
402402
end
403+
404+
class DummyIngestor < Ingestors::Ingestor
405+
include Ingestors::Concerns::SitemapHelpers
406+
407+
def self.config
408+
{
409+
key: 'dummy'
410+
}
411+
end
412+
413+
def read(url)
414+
get_sources(url)
415+
open_url(url)
416+
end
417+
end
403418
end
404419

405420
# Minitest's `stub` method but ignores any blocks

test/unit/ingestors/github_ingestor_test.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class GithubIngestorTest < ActiveSupport::TestCase # rubocop:disable Metrics/Cla
3939
webmock('https://api.github.com/repos/hsf-training/cpluspluscourse/releases', 'github/releases.json')
4040
webmock('https://api.github.com/repos/swcarpentry/python-novice-inflammation/releases', 'github/releases.json')
4141
webmock('https://api.github.com/repos/hsf-training/hsf-training-scikit-hep-webpage/releases', 'github/releases.json')
42+
43+
# HTML
44+
html = File.read(Rails.root.join('test/fixtures/files/ingestion/github/mock.html'))
45+
@doc = Nokogiri::HTML(html)
4246
end
4347

4448
teardown do
@@ -235,6 +239,102 @@ class GithubIngestorTest < ActiveSupport::TestCase # rubocop:disable Metrics/Cla
235239
assert_nil sample_after_ttl_and_change.description
236240
end
237241

242+
test 'std errors when exception is raised' do
243+
@ingestor.stub(:get_sources, ->(_url) { raise StandardError, 'test failure' }) do
244+
@ingestor.send(:read, 'https://github.com/example')
245+
end
246+
assert_includes @ingestor.instance_variable_get(:@messages).last,
247+
'Ingestors::GithubIngestor read failed, test failure'
248+
249+
@ingestor.stub(:open_url, ->(_url) { raise StandardError, 'test failure' }) do
250+
@ingestor.send(:get_or_set_cache, 'key', 'https://github.com/example')
251+
end
252+
assert_includes @ingestor.instance_variable_get(:@messages).last,
253+
'Ingestors::GithubIngestor get_or_set_cache failed for https://github.com/example, test failure'
254+
255+
@ingestor.stub(:get_or_set_cache, ->(_key, _url) { raise StandardError, 'test failure' }) do
256+
@ingestor.send(:fetch_doi, 'full_name')
257+
end
258+
assert_includes @ingestor.instance_variable_get(:@messages).last,
259+
'Ingestors::GithubIngestor fetch_doi failed for https://api.github.com/repos/full_name/contents/README.md, test failure'
260+
261+
@ingestor.stub(:get_or_set_cache, ->(_key, _url) { raise StandardError, 'test failure' }) do
262+
@ingestor.send(:fetch_latest_release, 'full_name')
263+
end
264+
assert_includes @ingestor.instance_variable_get(:@messages).last,
265+
'Ingestors::GithubIngestor fetch_latest_release failed for https://api.github.com/repos/full_name/releases, test failure'
266+
267+
@ingestor.stub(:get_or_set_cache, ->(_key, _url) { raise StandardError, 'test failure' }) do
268+
@ingestor.send(:fetch_contributors, 'my.url', 'full_name')
269+
end
270+
assert_includes @ingestor.instance_variable_get(:@messages).last,
271+
'Ingestors::GithubIngestor fetch_contributors failed for my.url, test failure'
272+
end
273+
274+
test 'prereq_node? returns true when id includes prereq' do
275+
node = Nokogiri::XML::Node.new('div', @doc)
276+
node['id'] = 'prerequisites'
277+
assert @ingestor.send(:prereq_node?, node)
278+
end
279+
280+
test 'prereq_node? returns true when class includes prereq' do
281+
node = Nokogiri::XML::Node.new('div', @doc)
282+
node['class'] = 'has_prereq_section'
283+
assert @ingestor.send(:prereq_node?, node)
284+
end
285+
286+
test 'prereq_node? returns false for unrelated id/class' do
287+
node = Nokogiri::XML::Node.new('div', @doc)
288+
node['id'] = 'requirements'
289+
refute @ingestor.send(:prereq_node?, node)
290+
end
291+
292+
test 'extract_following_paragraphs finds immediate sibling paragraphs' do
293+
doc = Nokogiri::HTML(<<~HTML)
294+
<div id="prereq"></div>
295+
<p>First paragraph</p>
296+
<ul><li>List item</li></ul>
297+
<h2>Stop here</h2>
298+
HTML
299+
300+
node = doc.at_xpath('//*[@id="prereq"]')
301+
paragraphs = []
302+
@ingestor.send(:extract_following_paragraphs, node, paragraphs)
303+
304+
assert_equal 2, paragraphs.size
305+
assert_equal %w[p ul], paragraphs.map(&:name)
306+
end
307+
308+
test 'extract_nested_paragraphs finds paragraphs inside the node' do
309+
doc = Nokogiri::HTML(<<~HTML)
310+
<div id="prereq">
311+
<p>Nested paragraph</p>
312+
<ul><li>Nested list</li></ul>
313+
</div>
314+
HTML
315+
316+
node = doc.at_xpath('//*[@id="prereq"]')
317+
paragraphs = []
318+
@ingestor.send(:extract_nested_paragraphs, node, paragraphs)
319+
320+
assert_equal 2, paragraphs.size
321+
assert_equal %w[p ul], paragraphs.map(&:name)
322+
end
323+
324+
test 'fetch_prerequisites_from_id_or_class collects prerequisites correctly' do
325+
doc = Nokogiri::HTML(<<~HTML)
326+
<div id="prereq"></div>
327+
<p>Be kind</p>
328+
<p>Have Ruby installed</p>
329+
HTML
330+
331+
paragraphs = []
332+
result = @ingestor.send(:fetch_prerequisites_from_id_or_class, doc, paragraphs)
333+
334+
assert_equal 2, result.size
335+
assert_equal 'Be kind', result.first.text
336+
end
337+
238338
private
239339

240340
def webmock(url, filename)

test/unit/ingestors/ingestor_test.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,78 @@ class IngestorTest < ActiveSupport::TestCase
113113
assert_nil(event.language)
114114
end
115115

116+
test 'open_url returns content when URL is valid' do
117+
ingestor = DummyIngestor.new
118+
stub_request(:get, 'https://example.com').to_return(body: 'ok', status: 200)
119+
120+
result = ingestor.open_url('https://example.com')
121+
assert_equal 'ok', result.read
122+
end
123+
124+
test 'open_url raises HTTPRedirect after too many retries' do
125+
ingestor = DummyIngestor.new
126+
fake_uri = URI('https://example.com/')
127+
128+
URI.stub(:parse, fake_uri) do
129+
fake_uri.define_singleton_method(:open) do |*_args|
130+
raise OpenURI::HTTPRedirect.new('Redirect', 1, URI('https://redirected.com'))
131+
end
132+
133+
assert_raises(OpenURI::HTTPRedirect) do
134+
ingestor.open_url('https://example.com/')
135+
end
136+
end
137+
end
138+
139+
test 'open_url raises HTTPError' do
140+
ingestor = DummyIngestor.new
141+
stub_request(:get, 'https://bad.com')
142+
.to_raise(OpenURI::HTTPError.new('404 not found', StringIO.new))
143+
144+
result = ingestor.open_url('https://bad.com')
145+
assert_nil result
146+
assert_includes ingestor.instance_variable_get(:@messages).last,
147+
"Couldn't open URL https://bad.com: 404 not found"
148+
end
149+
150+
test 'get_redirected_url follows meta refresh redirect' do
151+
ingestor = DummyIngestor.new
152+
html_with_meta = '<html><head><meta http-equiv="Refresh" content="0; url=/redirected"></head></html>'
153+
redirected_html = '<html><head><title>Final</title></head><body>Done</body></html>'
154+
155+
HTTParty.stub(:get, lambda { |url, **|
156+
case url
157+
when 'https://example.com/'
158+
OpenStruct.new(headers: { 'content-type' => 'text/html' }, body: html_with_meta)
159+
when 'https://example.com//redirected'
160+
OpenStruct.new(headers: { 'content-type' => 'text/html' }, body: redirected_html)
161+
else
162+
raise "Unexpected URL: #{url}"
163+
end
164+
}) do
165+
result = ingestor.get_redirected_url('http://example.com/')
166+
assert_equal 'https://example.com//redirected', result
167+
end
168+
end
169+
170+
test 'get_redirected_url returns original url when no meta redirect' do
171+
ingestor = DummyIngestor.new
172+
html_no_meta = '<html><head><title>No redirect</title></head><body></body></html>'
173+
174+
HTTParty.stub(:get, ->(_url, **) { OpenStruct.new(headers: { 'content-type' => 'text/html' }, body: html_no_meta) }) do
175+
result = ingestor.get_redirected_url('http://example.com/')
176+
assert_equal 'https://example.com/', result
177+
end
178+
end
179+
180+
test 'get_redirected_url raises when too many redirects' do
181+
ingestor = DummyIngestor.new
182+
html_with_meta = '<html><head><meta http-equiv="Refresh" content="0; url=/loop"></head></html>'
183+
184+
HTTParty.stub(:get, ->(_url, **) { OpenStruct.new(headers: { 'content-type' => 'text/html' }, body: html_with_meta) }) do
185+
assert_raises(RuntimeError, 'Too many redirects') do
186+
ingestor.get_redirected_url('http://example.com/', 0)
187+
end
188+
end
189+
end
116190
end

0 commit comments

Comments
 (0)