metainspector 5.18.0 → 5.19.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/.circleci/config.yml +15 -0
- data/.github/dependabot.yml +1 -1
- data/.gitignore +1 -1
- data/CHANGELOG.md +15 -0
- data/README.md +56 -1
- data/lib/meta_inspector/document.rb +17 -3
- data/lib/meta_inspector/parser.rb +3 -2
- data/lib/meta_inspector/parsers/images.rb +17 -13
- data/lib/meta_inspector/parsers/links.rb +2 -1
- data/lib/meta_inspector/parsers/texts.rb +13 -4
- data/lib/meta_inspector/request.rb +34 -11
- data/lib/meta_inspector/version.rb +1 -1
- data/meta_inspector.gemspec +5 -6
- data/spec/document_spec.rb +20 -0
- data/spec/meta_inspector/head_first_spec.rb +195 -0
- data/spec/meta_inspector/images_spec.rb +233 -0
- data/spec/meta_inspector/links_spec.rb +22 -0
- data/spec/meta_inspector/redirections_spec.rb +20 -6
- data/spec/meta_inspector/texts_spec.rb +115 -0
- data/spec/request_spec.rb +29 -0
- metadata +16 -23
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe MetaInspector, 'head_first option' do
|
|
4
|
+
let(:page_url) { 'http://example.com/head-first' }
|
|
5
|
+
let(:html) { '<html><head><title>Downloaded page</title></head></html>' }
|
|
6
|
+
let!(:get_request) do
|
|
7
|
+
stub_request(:get, page_url).to_return(body: html, headers: { 'Content-Type' => 'text/html' })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'does not send HEAD by default' do
|
|
11
|
+
expect(MetaInspector.new(page_url).title).to eq('Downloaded page')
|
|
12
|
+
|
|
13
|
+
expect(a_request(:head, page_url)).not_to have_been_made
|
|
14
|
+
expect(get_request).to have_been_requested.once
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'does not send HEAD when explicitly disabled' do
|
|
18
|
+
expect(MetaInspector.new(page_url, head_first: false).title).to eq('Downloaded page')
|
|
19
|
+
|
|
20
|
+
expect(a_request(:head, page_url)).not_to have_been_made
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'sends HEAD before GET for HTML and exposes the GET response' do
|
|
24
|
+
requests = []
|
|
25
|
+
head_request = stub_request(:head, page_url).to_return do
|
|
26
|
+
requests << :head
|
|
27
|
+
{ headers: { 'Content-Type' => 'text/html; charset=UTF-8', 'X-Response' => 'head' } }
|
|
28
|
+
end
|
|
29
|
+
stub_request(:get, page_url).to_return do
|
|
30
|
+
requests << :get
|
|
31
|
+
{ body: html, headers: { 'Content-Type' => 'text/html', 'X-Response' => 'get' } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
page = MetaInspector.new(page_url, head_first: true)
|
|
35
|
+
|
|
36
|
+
expect(page.title).to eq('Downloaded page')
|
|
37
|
+
expect(page.content_type).to eq('text/html')
|
|
38
|
+
expect(page.response.headers['x-response']).to eq('get')
|
|
39
|
+
expect(page.to_s).to eq(html)
|
|
40
|
+
expect(head_request).to have_been_requested.once
|
|
41
|
+
expect(requests).to eq([:head, :get])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'rejects non-HTML content without sending GET' do
|
|
45
|
+
head_request = stub_request(:head, page_url)
|
|
46
|
+
.to_return(headers: { 'Content-Type' => 'application/pdf; version=1.7' })
|
|
47
|
+
|
|
48
|
+
expect do
|
|
49
|
+
MetaInspector.new(page_url, head_first: true)
|
|
50
|
+
end.to raise_error(MetaInspector::NonHtmlError,
|
|
51
|
+
'The url provided contains application/pdf content instead of text/html content')
|
|
52
|
+
|
|
53
|
+
expect(head_request).to have_been_requested.once
|
|
54
|
+
expect(get_request).not_to have_been_requested
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'downloads non-HTML content when explicitly allowed' do
|
|
58
|
+
head_request = stub_request(:head, page_url).to_return(headers: { 'Content-Type' => 'text/plain' })
|
|
59
|
+
stub_request(:get, page_url).to_return(body: 'Plain text', headers: { 'Content-Type' => 'text/plain' })
|
|
60
|
+
|
|
61
|
+
page = MetaInspector.new(page_url, head_first: true, allow_non_html_content: true)
|
|
62
|
+
|
|
63
|
+
expect(page.to_s).to eq('Plain text')
|
|
64
|
+
expect(page.content_type).to eq('text/plain')
|
|
65
|
+
expect(head_request).to have_been_requested.once
|
|
66
|
+
expect(get_request).to have_been_requested.once
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'continues to GET when HEAD has no content type' do
|
|
70
|
+
head_request = stub_request(:head, page_url).to_return(status: 200)
|
|
71
|
+
|
|
72
|
+
expect(MetaInspector.new(page_url, head_first: true).title).to eq('Downloaded page')
|
|
73
|
+
|
|
74
|
+
expect(head_request).to have_been_requested.once
|
|
75
|
+
expect(get_request).to have_been_requested.once
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[403, 404, 405, 501].each do |status|
|
|
79
|
+
it "continues to GET when HEAD returns #{status}" do
|
|
80
|
+
head_request = stub_request(:head, page_url)
|
|
81
|
+
.to_return(status: status, headers: { 'Content-Type' => 'text/plain' })
|
|
82
|
+
|
|
83
|
+
expect(MetaInspector.new(page_url, head_first: true).title).to eq('Downloaded page')
|
|
84
|
+
|
|
85
|
+
expect(head_request).to have_been_requested.once
|
|
86
|
+
expect(get_request).to have_been_requested.once
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'still rejects a non-HTML GET response when HEAD reports HTML' do
|
|
91
|
+
head_request = stub_request(:head, page_url).to_return(headers: { 'Content-Type' => 'text/html' })
|
|
92
|
+
stub_request(:get, page_url).to_return(headers: { 'Content-Type' => 'application/pdf' })
|
|
93
|
+
|
|
94
|
+
expect do
|
|
95
|
+
MetaInspector.new(page_url, head_first: true)
|
|
96
|
+
end.to raise_error(MetaInspector::NonHtmlError)
|
|
97
|
+
|
|
98
|
+
expect(head_request).to have_been_requested.once
|
|
99
|
+
expect(get_request).to have_been_requested.once
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'does not send requests for a supplied document' do
|
|
103
|
+
expect(MetaInspector.new(page_url, head_first: true, document: html).title).to eq('Downloaded page')
|
|
104
|
+
|
|
105
|
+
expect(a_request(:head, page_url)).not_to have_been_made
|
|
106
|
+
expect(get_request).not_to have_been_requested
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'sends custom headers on both requests' do
|
|
110
|
+
headers = { 'User-Agent' => 'Custom agent', 'Authorization' => 'Bearer test-token' }
|
|
111
|
+
head_request = stub_request(:head, page_url).with(headers: headers)
|
|
112
|
+
.to_return(headers: { 'Content-Type' => 'text/html' })
|
|
113
|
+
get_with_headers = stub_request(:get, page_url).with(headers: headers).to_return(body: html)
|
|
114
|
+
|
|
115
|
+
expect(MetaInspector.new(page_url, head_first: true, headers: headers).title).to eq('Downloaded page')
|
|
116
|
+
|
|
117
|
+
expect(head_request).to have_been_requested.once
|
|
118
|
+
expect(get_with_headers).to have_been_requested.once
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'follows HEAD redirects to reject non-HTML content before GET' do
|
|
122
|
+
target = 'http://example.com/large.pdf'
|
|
123
|
+
stub_request(:head, page_url).to_return(status: 302, headers: { 'Location' => target })
|
|
124
|
+
target_request = stub_request(:head, target).to_return(headers: { 'Content-Type' => 'application/pdf' })
|
|
125
|
+
|
|
126
|
+
expect do
|
|
127
|
+
MetaInspector.new(page_url, head_first: true)
|
|
128
|
+
end.to raise_error(MetaInspector::NonHtmlError)
|
|
129
|
+
|
|
130
|
+
expect(target_request).to have_been_requested.once
|
|
131
|
+
expect(a_request(:get, /example.com/)).not_to have_been_made
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'follows GET redirects independently of HEAD and exposes the final GET URL' do
|
|
135
|
+
head_target = 'http://example.com/head-target'
|
|
136
|
+
get_target = 'http://example.com/get-target'
|
|
137
|
+
head_request = stub_request(:head, page_url).to_return(status: 302, headers: { 'Location' => head_target })
|
|
138
|
+
stub_request(:head, head_target).to_return(headers: { 'Content-Type' => 'text/html' })
|
|
139
|
+
stub_request(:get, page_url).to_return(status: 302, headers: { 'Location' => get_target })
|
|
140
|
+
final_request = stub_request(:get, get_target).to_return(body: html, headers: { 'Content-Type' => 'text/html' })
|
|
141
|
+
|
|
142
|
+
page = MetaInspector.new(page_url, head_first: true)
|
|
143
|
+
|
|
144
|
+
expect(page.title).to eq('Downloaded page')
|
|
145
|
+
expect(page.url).to eq(get_target)
|
|
146
|
+
expect(head_request).to have_been_requested.once
|
|
147
|
+
expect(final_request).to have_been_requested.once
|
|
148
|
+
expect(a_request(:get, head_target)).not_to have_been_made
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'does not follow HEAD redirects when redirections are disabled' do
|
|
152
|
+
head_request = stub_request(:head, page_url)
|
|
153
|
+
.to_return(status: 302, headers: { 'Location' => 'http://example.com/target', 'Content-Type' => 'text/plain' })
|
|
154
|
+
|
|
155
|
+
page = MetaInspector.new(page_url, head_first: true, allow_redirections: false)
|
|
156
|
+
|
|
157
|
+
expect(page.title).to eq('Downloaded page')
|
|
158
|
+
expect(page.url).to eq(page_url)
|
|
159
|
+
expect(head_request).to have_been_requested.once
|
|
160
|
+
expect(a_request(:head, 'http://example.com/target')).not_to have_been_made
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'keeps cookies from HEAD for the GET request' do
|
|
164
|
+
head_request = stub_request(:head, page_url)
|
|
165
|
+
.to_return(headers: { 'Content-Type' => 'text/html', 'Set-Cookie' => 'session=checked; path=/' })
|
|
166
|
+
get_with_cookie = stub_request(:get, page_url).with(headers: { 'Cookie' => 'session=checked' }).to_return(body: html)
|
|
167
|
+
|
|
168
|
+
expect(MetaInspector.new(page_url, head_first: true).title).to eq('Downloaded page')
|
|
169
|
+
|
|
170
|
+
expect(head_request).to have_been_requested.once
|
|
171
|
+
expect(get_with_cookie).to have_been_requested.once
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'wraps HEAD timeouts without sending GET' do
|
|
175
|
+
head_request = stub_request(:head, page_url).to_raise(Net::ReadTimeout)
|
|
176
|
+
|
|
177
|
+
expect do
|
|
178
|
+
MetaInspector.new(page_url, head_first: true, retries: 0)
|
|
179
|
+
end.to raise_error(MetaInspector::TimeoutError)
|
|
180
|
+
|
|
181
|
+
expect(head_request).to have_been_requested.once
|
|
182
|
+
expect(get_request).not_to have_been_requested
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'wraps HEAD connection failures without sending GET' do
|
|
186
|
+
head_request = stub_request(:head, page_url).to_raise(Faraday::ConnectionFailed.new('connection failed'))
|
|
187
|
+
|
|
188
|
+
expect do
|
|
189
|
+
MetaInspector.new(page_url, head_first: true, retries: 0)
|
|
190
|
+
end.to raise_error(MetaInspector::RequestError)
|
|
191
|
+
|
|
192
|
+
expect(head_request).to have_been_requested.once
|
|
193
|
+
expect(get_request).not_to have_been_requested
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -70,6 +70,31 @@ describe MetaInspector do
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
describe "images.best" do
|
|
73
|
+
it 'should prefer the Parsely image over the largest image' do
|
|
74
|
+
html = '<meta name="parsely-image-url" content="/parsely.jpg"><img src="/largest.jpg" width="100" height="100">'
|
|
75
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
76
|
+
|
|
77
|
+
expect(page.images.best).to eq('http://example.com/parsely.jpg')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
['', ' '].each do |value|
|
|
81
|
+
it "should fall back to the largest image when the Parsely image is empty (#{value.inspect})" do
|
|
82
|
+
html = %(<meta name="parsely-image-url" content="#{value}"><img src="/largest.jpg" width="100" height="100">)
|
|
83
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
84
|
+
|
|
85
|
+
expect(page.images.best).to eq('http://example.com/largest.jpg')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "should find a valid image when another image URL cannot be parsed" do
|
|
90
|
+
html = '<html><body><img src="http://exa mple.com/x.png"><img src="/100x100"></body></html>'
|
|
91
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
92
|
+
|
|
93
|
+
allow(FastImage).to receive(:size).and_call_original
|
|
94
|
+
expect(FastImage).not_to receive(:size).with(nil)
|
|
95
|
+
expect(page.images.best).to eq("http://example.com/100x100")
|
|
96
|
+
end
|
|
97
|
+
|
|
73
98
|
it "should find the og image" do
|
|
74
99
|
page = MetaInspector.new('http://www.theonion.com/articles/apple-claims-new-iphone-only-visible-to-most-loyal,2772/')
|
|
75
100
|
|
|
@@ -103,6 +128,44 @@ describe MetaInspector do
|
|
|
103
128
|
end
|
|
104
129
|
|
|
105
130
|
describe "images.owner_suggested" do
|
|
131
|
+
it 'should find the Parsely image when no existing metadata is present' do
|
|
132
|
+
html = '<meta name="parsely-image-url" content="https://images.example.com/parsely.jpg?b=2&a=1">'
|
|
133
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
134
|
+
|
|
135
|
+
expect(page.images.owner_suggested).to eq('https://images.example.com/parsely.jpg?b=2&a=1')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'should resolve a relative Parsely image against the document base URL' do
|
|
139
|
+
html = '<base href="/assets/"><meta name="parsely-image-url" content="parsely.jpg">'
|
|
140
|
+
page = MetaInspector.new('http://example.com/articles/page', document: html)
|
|
141
|
+
|
|
142
|
+
expect(page.images.owner_suggested).to eq('http://example.com/assets/parsely.jpg')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'should use the Parsely image when existing metadata is empty' do
|
|
146
|
+
html = '<meta property="og:image" content=""><meta name="twitter:image" content=""><meta name="parsely-image-url" content="/parsely.jpg">'
|
|
147
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
148
|
+
|
|
149
|
+
expect(page.images.owner_suggested).to eq('http://example.com/parsely.jpg')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
['og:image', 'twitter:image'].each do |candidate|
|
|
153
|
+
it "should prefer #{candidate} over the Parsely image" do
|
|
154
|
+
html = %(<meta name="parsely-image-url" content="/parsely.jpg"><meta property="#{candidate}" content="/existing.jpg">)
|
|
155
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
156
|
+
|
|
157
|
+
expect(page.images.owner_suggested).to eq('http://example.com/existing.jpg')
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
['', ' '].each do |value|
|
|
162
|
+
it "should ignore an empty Parsely image (#{value.inspect})" do
|
|
163
|
+
page = MetaInspector.new('http://example.com/', document: %(<meta name="parsely-image-url" content="#{value}">))
|
|
164
|
+
|
|
165
|
+
expect(page.images.owner_suggested).to be_nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
106
169
|
it "should find the og image" do
|
|
107
170
|
page = MetaInspector.new('http://www.theonion.com/articles/apple-claims-new-iphone-only-visible-to-most-loyal,2772/')
|
|
108
171
|
|
|
@@ -135,6 +198,176 @@ describe MetaInspector do
|
|
|
135
198
|
end
|
|
136
199
|
|
|
137
200
|
describe "images.with_size" do
|
|
201
|
+
describe "max_image_downloads" do
|
|
202
|
+
let(:html) do
|
|
203
|
+
'<img src="/10x10"><img src="/100x100"><img src="/remaining.jpg">'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
before do
|
|
207
|
+
stub_request(:get, 'http://example.com/remaining.jpg').to_return(status: 404)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "should download only the first images needing dimensions in document order" do
|
|
211
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 2)
|
|
212
|
+
|
|
213
|
+
expect(page.images.with_size).to eq([
|
|
214
|
+
['http://example.com/100x100', 100, 100],
|
|
215
|
+
['http://example.com/10x10', 10, 10],
|
|
216
|
+
['http://example.com/remaining.jpg', 0, 0]
|
|
217
|
+
])
|
|
218
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
219
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/100x100').once
|
|
220
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/remaining.jpg')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "should preserve HTML dimensions without consuming the download budget" do
|
|
224
|
+
html = '<img src="/known.jpg" width="200" height="200"><img src="/10x10"><img src="/100x100"><img src="/later.jpg" width="300" height="300">'
|
|
225
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
226
|
+
|
|
227
|
+
expect(page.images.with_size).to eq([
|
|
228
|
+
['http://example.com/later.jpg', 300, 300],
|
|
229
|
+
['http://example.com/known.jpg', 200, 200],
|
|
230
|
+
['http://example.com/10x10', 10, 10],
|
|
231
|
+
['http://example.com/100x100', 0, 0]
|
|
232
|
+
])
|
|
233
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
234
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/100x100')
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "should exclude invalid and duplicate URLs before consuming the budget" do
|
|
238
|
+
html = '<img src="http://exa mple.com/broken.jpg"><img src="/10x10"><img src="http://example.com/10x10"><img src="/100x100"><img src="/remaining.jpg">'
|
|
239
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 2)
|
|
240
|
+
|
|
241
|
+
expect(page.images.with_size).to eq([
|
|
242
|
+
['http://example.com/100x100', 100, 100],
|
|
243
|
+
['http://example.com/10x10', 10, 10],
|
|
244
|
+
['http://example.com/remaining.jpg', 0, 0]
|
|
245
|
+
])
|
|
246
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
247
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/100x100').once
|
|
248
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/remaining.jpg')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "should treat an image missing one HTML dimension as needing a download" do
|
|
252
|
+
html = '<img src="/10x10" width="500"><img src="/100x100" height="500">'
|
|
253
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
254
|
+
|
|
255
|
+
expect(page.images.with_size).to eq([
|
|
256
|
+
['http://example.com/10x10', 10, 10],
|
|
257
|
+
['http://example.com/100x100', 0, 0]
|
|
258
|
+
])
|
|
259
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
260
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/100x100')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "should disable downloads with a zero limit while preserving HTML dimensions" do
|
|
264
|
+
html = '<img src="/10x10"><img src="/known.jpg" width="200" height="100">'
|
|
265
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 0)
|
|
266
|
+
|
|
267
|
+
expect(page.images.with_size).to eq([
|
|
268
|
+
['http://example.com/known.jpg', 200, 100],
|
|
269
|
+
['http://example.com/10x10', 0, 0]
|
|
270
|
+
])
|
|
271
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/10x10')
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "should respect download_images: false even with a positive limit" do
|
|
275
|
+
page = MetaInspector.new('http://example.com/', document: html, download_images: false, max_image_downloads: 2)
|
|
276
|
+
|
|
277
|
+
expect(page.images.with_size).to eq([
|
|
278
|
+
['http://example.com/10x10', 0, 0],
|
|
279
|
+
['http://example.com/100x100', 0, 0],
|
|
280
|
+
['http://example.com/remaining.jpg', 0, 0]
|
|
281
|
+
])
|
|
282
|
+
expect(WebMock).not_to have_requested(:get, %r{http://example.com/})
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
[nil, 10].each do |limit|
|
|
286
|
+
it "should download every candidate when the limit is #{limit.inspect}" do
|
|
287
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: limit)
|
|
288
|
+
|
|
289
|
+
expect(page.images.with_size).to eq([
|
|
290
|
+
['http://example.com/100x100', 100, 100],
|
|
291
|
+
['http://example.com/10x10', 10, 10],
|
|
292
|
+
['http://example.com/remaining.jpg', 0, 0]
|
|
293
|
+
])
|
|
294
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
295
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/100x100').once
|
|
296
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/remaining.jpg').once
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
[:not_found, :invalid_image, :timeout].each do |failure|
|
|
301
|
+
it "should count a #{failure} image attempt toward the limit" do
|
|
302
|
+
request = stub_request(:get, 'http://example.com/broken.jpg')
|
|
303
|
+
case failure
|
|
304
|
+
when :not_found then request.to_return(status: 404)
|
|
305
|
+
when :invalid_image then request.to_return(body: 'not an image')
|
|
306
|
+
when :timeout then request.to_timeout
|
|
307
|
+
end
|
|
308
|
+
html = '<img src="/broken.jpg"><img src="/100x100">'
|
|
309
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
310
|
+
|
|
311
|
+
expect(page.images.with_size).to eq([
|
|
312
|
+
['http://example.com/broken.jpg', 0, 0],
|
|
313
|
+
['http://example.com/100x100', 0, 0]
|
|
314
|
+
])
|
|
315
|
+
expect(request).to have_been_requested.once
|
|
316
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/100x100')
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "should reuse the limited results for repeated size and best-image queries" do
|
|
321
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
322
|
+
|
|
323
|
+
2.times do
|
|
324
|
+
expect(page.images.with_size.first).to eq(['http://example.com/10x10', 10, 10])
|
|
325
|
+
expect(page.images.largest).to eq('http://example.com/10x10')
|
|
326
|
+
expect(page.images.best).to eq('http://example.com/10x10')
|
|
327
|
+
end
|
|
328
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').once
|
|
329
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/100x100')
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "should keep the budget local to each document" do
|
|
333
|
+
2.times do
|
|
334
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
335
|
+
|
|
336
|
+
expect(page.images.with_size.first).to eq(['http://example.com/10x10', 10, 10])
|
|
337
|
+
end
|
|
338
|
+
expect(WebMock).to have_requested(:get, 'http://example.com/10x10').twice
|
|
339
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/100x100')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it "should keep all image URLs without downloading them when enumerating images" do
|
|
343
|
+
page = MetaInspector.new('http://example.com/', document: html, max_image_downloads: 1)
|
|
344
|
+
|
|
345
|
+
expect(page.images.to_a).to eq([
|
|
346
|
+
'http://example.com/10x10',
|
|
347
|
+
'http://example.com/100x100',
|
|
348
|
+
'http://example.com/remaining.jpg'
|
|
349
|
+
])
|
|
350
|
+
expect(WebMock).not_to have_requested(:get, %r{http://example.com/})
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
[true, false].each do |download_images|
|
|
355
|
+
it "should ignore unparseable image URLs with download_images: #{download_images}" do
|
|
356
|
+
html = '<html><body><img src="http://exa mple.com/x.png"><img src="/valid.png" width="100" height="100"></body></html>'
|
|
357
|
+
page = MetaInspector.new('https://example.com/', document: html, download_images: download_images)
|
|
358
|
+
|
|
359
|
+
expect(FastImage).not_to receive(:size).with(nil)
|
|
360
|
+
expect(page.images.with_size).to eq([["https://example.com/valid.png", 100, 100]])
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
it "should ignore unparseable image URLs even when HTML sizes are present" do
|
|
365
|
+
html = '<html><body><img src="http://exa mple.com/x.png" width="100" height="100"></body></html>'
|
|
366
|
+
page = MetaInspector.new('https://example.com/', document: html)
|
|
367
|
+
|
|
368
|
+
expect(page.images.with_size).to eq([])
|
|
369
|
+
end
|
|
370
|
+
|
|
138
371
|
it "should return sorted by area array of [img_url, width, height] using html sizes" do
|
|
139
372
|
page = MetaInspector.new('http://example.com/largest_image_in_html')
|
|
140
373
|
|
|
@@ -174,6 +174,28 @@ describe MetaInspector do
|
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
describe 'Relative links with relative base' do
|
|
177
|
+
[
|
|
178
|
+
['assets/', 'https://example.com/posts/article', 'https://example.com/posts/assets/next.html'],
|
|
179
|
+
['../assets/', 'https://example.com/posts/2026/article', 'https://example.com/posts/assets/next.html'],
|
|
180
|
+
['assets/base.html', 'https://example.com/posts/article', 'https://example.com/posts/assets/next.html'],
|
|
181
|
+
['assets/', 'https://example.com/posts/article/', 'https://example.com/posts/article/assets/next.html'],
|
|
182
|
+
['/assets/', 'https://example.com:8443/posts/article', 'https://example.com:8443/assets/next.html']
|
|
183
|
+
].each do |base_href, url, expected_link|
|
|
184
|
+
it "resolves base #{base_href.inspect} against #{url}" do
|
|
185
|
+
html = %(<html><head><base href="#{base_href}"></head><body><a href="next.html">Next</a></body></html>)
|
|
186
|
+
m = MetaInspector.new(url, document: html)
|
|
187
|
+
|
|
188
|
+
expect(m.links.all).to eq([expected_link])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'resolves image URLs against a document-relative base in a nested directory' do
|
|
193
|
+
html = '<html><head><base href="assets/"></head><body><img src="image.jpg"></body></html>'
|
|
194
|
+
m = MetaInspector.new('https://example.com/posts/article', document: html)
|
|
195
|
+
|
|
196
|
+
expect(m.images.to_a).to eq(['https://example.com/posts/assets/image.jpg'])
|
|
197
|
+
end
|
|
198
|
+
|
|
177
199
|
it 'should get the relative links with relative directory base' do
|
|
178
200
|
m = MetaInspector.new('http://relativewithrelativebase.com/relativedir')
|
|
179
201
|
expect(m.links.all).to eq(['http://relativewithrelativebase.com/other/about',
|
|
@@ -52,22 +52,36 @@ describe MetaInspector do
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
context "when there is a callback to be ran between redirects that blocks redirections to private IP addresses" do
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.to_return(:status => 302,
|
|
58
|
-
:headers => { "Location" => "http://10.0.0.0/" })
|
|
59
|
-
|
|
60
|
-
redirect_options = {
|
|
55
|
+
let(:redirect_options) do
|
|
56
|
+
{
|
|
61
57
|
callback: proc do |_previous_response, next_request|
|
|
62
58
|
ip_address = Resolv.getaddress(next_request.url.host)
|
|
63
59
|
raise PrivateIPAddressError if IPAddr.new(ip_address).private?
|
|
64
60
|
end
|
|
65
61
|
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
before do
|
|
65
|
+
stub_request(:get, "https://www.facebook.com/")
|
|
66
|
+
.to_return(:status => 302,
|
|
67
|
+
:headers => { "Location" => "http://10.0.0.0/" })
|
|
68
|
+
end
|
|
66
69
|
|
|
70
|
+
it "raises an exception" do
|
|
67
71
|
expect {
|
|
68
72
|
MetaInspector.new("https://www.facebook.com/", faraday_options: { redirect: redirect_options })
|
|
69
73
|
}.to raise_error PrivateIPAddressError
|
|
70
74
|
end
|
|
75
|
+
|
|
76
|
+
it "keeps running the callback when the same faraday_options are reused" do
|
|
77
|
+
faraday_options = { redirect: redirect_options }
|
|
78
|
+
|
|
79
|
+
2.times do
|
|
80
|
+
expect {
|
|
81
|
+
MetaInspector.new("https://www.facebook.com/", faraday_options: faraday_options)
|
|
82
|
+
}.to raise_error PrivateIPAddressError
|
|
83
|
+
end
|
|
84
|
+
end
|
|
71
85
|
end
|
|
72
86
|
end
|
|
73
87
|
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
3
|
describe MetaInspector do
|
|
4
|
+
describe '#language' do
|
|
5
|
+
['en', 'en-US', 'zh-Hant-TW'].each do |language|
|
|
6
|
+
it "should return the declared language #{language.inspect}" do
|
|
7
|
+
page = MetaInspector.new('http://example.com/', document: %(<html lang="#{language}"><body></body></html>))
|
|
8
|
+
|
|
9
|
+
expect(page.language).to eq(language)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'should strip surrounding whitespace from the declared language' do
|
|
14
|
+
page = MetaInspector.new('http://example.com/', document: "<html lang=\" \t en-US \n \"><body></body></html>")
|
|
15
|
+
|
|
16
|
+
expect(page.language).to eq('en-US')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
[nil, '', " \t\n "].each do |value|
|
|
20
|
+
it "should return nil for a missing or blank language (#{value.inspect})" do
|
|
21
|
+
attribute = value.nil? ? '' : %( lang="#{value}")
|
|
22
|
+
page = MetaInspector.new('http://example.com/', document: "<html#{attribute}><body></body></html>")
|
|
23
|
+
|
|
24
|
+
expect(page.language).to be_nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'should read the root language rather than a descendant language' do
|
|
29
|
+
page = MetaInspector.new('http://example.com/', document: '<html lang="en"><body lang="es"></body></html>')
|
|
30
|
+
|
|
31
|
+
expect(page.language).to eq('en')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'should not infer language from xml:lang, metadata, or descendants' do
|
|
35
|
+
html = '<html xml:lang="fr"><head><meta http-equiv="content-language" content="de"></head><body lang="es">Hello</body></html>'
|
|
36
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
37
|
+
|
|
38
|
+
expect(page.language).to be_nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'should return the declared language from a fetched document' do
|
|
42
|
+
page = MetaInspector.new('http://pagerankalert.com/')
|
|
43
|
+
|
|
44
|
+
expect(page.language).to eq('en')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
4
48
|
it "should get the title from the head section" do
|
|
5
49
|
page = MetaInspector.new('http://example.com')
|
|
6
50
|
expect(page.title).to eq('An example page')
|
|
@@ -49,6 +93,42 @@ describe MetaInspector do
|
|
|
49
93
|
end
|
|
50
94
|
|
|
51
95
|
describe '#best_title' do
|
|
96
|
+
it 'should use the Parsely title when no existing candidate is present' do
|
|
97
|
+
page = MetaInspector.new('http://example.com/', document: '<meta name="parsely-title" content=" A Parsely title ">')
|
|
98
|
+
|
|
99
|
+
expect(page.best_title).to eq('A Parsely title')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'should use the Parsely title when existing candidates are empty' do
|
|
103
|
+
html = '<html><head><meta name="title" content=" "><meta property="og:title" content=""><title> </title><meta name="parsely-title" content="Parsely title"></head><body><h1> </h1></body></html>'
|
|
104
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
105
|
+
|
|
106
|
+
expect(page.best_title).to eq('Parsely title')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
[
|
|
110
|
+
['<meta name="title" content="Existing title">', ''],
|
|
111
|
+
['<meta property="og:title" content="Existing title">', ''],
|
|
112
|
+
['<title>Existing title</title>', ''],
|
|
113
|
+
['', '<title>Existing title</title>'],
|
|
114
|
+
['', '<h1>Existing title</h1>']
|
|
115
|
+
].each do |head, body|
|
|
116
|
+
it "should prefer #{head + body} over the Parsely title" do
|
|
117
|
+
html = %(<html><head><meta name="parsely-title" content="Parsely title">#{head}</head><body>#{body}</body></html>)
|
|
118
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
119
|
+
|
|
120
|
+
expect(page.best_title).to eq('Existing title')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
['', ' '].each do |value|
|
|
125
|
+
it "should ignore an empty Parsely title (#{value.inspect})" do
|
|
126
|
+
page = MetaInspector.new('http://example.com/', document: %(<meta name="parsely-title" content="#{value}">))
|
|
127
|
+
|
|
128
|
+
expect(page.best_title).to be_nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
52
132
|
it "should find 'head title' when that's the only thing" do
|
|
53
133
|
page = MetaInspector.new('http://example.com/title_in_head')
|
|
54
134
|
expect(page.best_title).to eq('This title came from the head')
|
|
@@ -105,6 +185,41 @@ describe MetaInspector do
|
|
|
105
185
|
end
|
|
106
186
|
|
|
107
187
|
describe "#best_author" do
|
|
188
|
+
it 'should use the Parsely author when no existing candidate is present' do
|
|
189
|
+
page = MetaInspector.new('http://example.com/', document: '<meta name="parsely-author" content=" A Parsely author ">')
|
|
190
|
+
|
|
191
|
+
expect(page.best_author).to eq('A Parsely author')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'should use the Parsely author when existing candidates are empty' do
|
|
195
|
+
html = '<meta name="author" content=" "><meta name="twitter:creator" content=""><meta name="parsely-author" content="Parsely author"><a rel="author"> </a><address> </address>'
|
|
196
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
197
|
+
|
|
198
|
+
expect(page.best_author).to eq('Parsely author')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
[
|
|
202
|
+
'<meta name="author" content="Existing author">',
|
|
203
|
+
'<a rel="author">Existing author</a>',
|
|
204
|
+
'<address>Existing author</address>',
|
|
205
|
+
'<meta name="twitter:creator" content="Existing author">'
|
|
206
|
+
].each do |candidate|
|
|
207
|
+
it "should prefer #{candidate} over the Parsely author" do
|
|
208
|
+
html = '<meta name="parsely-author" content="Parsely author">' + candidate
|
|
209
|
+
page = MetaInspector.new('http://example.com/', document: html)
|
|
210
|
+
|
|
211
|
+
expect(page.best_author).to eq('Existing author')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
['', ' '].each do |value|
|
|
216
|
+
it "should ignore an empty Parsely author (#{value.inspect})" do
|
|
217
|
+
page = MetaInspector.new('http://example.com/', document: %(<meta name="parsely-author" content="#{value}">))
|
|
218
|
+
|
|
219
|
+
expect(page.best_author).to be_nil
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
108
223
|
it "should return the author meta tag content if present" do
|
|
109
224
|
page = MetaInspector.new('http://example.com/author_in_meta')
|
|
110
225
|
|