doc_repo 0.1.1 → 1.0.0.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,55 +1,110 @@
1
- require 'support/using_env'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe DocRepo::Configuration do
4
4
 
5
- context "without environment variables configured" do
5
+ shared_examples "has setting" do |setting_name|
6
+ it "allows configuring `#{setting_name}`" do
7
+ a_config = DocRepo::Configuration.new
8
+ expect {
9
+ a_config.public_send "#{setting_name}=", "Any Value"
10
+ }.to change(a_config, setting_name).to "Any Value"
11
+ end
12
+ end
13
+
14
+ context "new configuration" do
6
15
  subject(:default_config) { DocRepo::Configuration.new }
7
16
 
8
- around do |example|
9
- env = {
10
- 'DOC_REPO_ORG' => nil,
11
- 'DOC_REPO_REPONAME' => nil,
12
- 'DOC_REPO_BRANCH' => nil,
13
- }
14
- using_env(env, &example)
17
+ it "yields itself when given a block" do
18
+ a_config = DocRepo::Configuration.new { |c| c.repo = "Any Repo" }
19
+ expect(a_config.repo).to eq "Any Repo"
15
20
  end
16
21
 
17
- it "has no org" do
18
- expect(default_config.org).to be nil
22
+ it "defaults to the 'master' branch" do
23
+ expect(default_config.branch).to eq 'master'
19
24
  end
20
25
 
21
- it "has no repo" do
22
- expect(default_config.repo).to be nil
26
+ it "defaults to empty cache options" do
27
+ expect(default_config.cache_options).to eq({})
23
28
  end
24
29
 
25
- it "use the 'master' branch" do
26
- expect(default_config.branch).to eq 'master'
30
+ it "defaults to a null cache store" do
31
+ expect(default_config.cache_store).to be DocRepo::NullCache.instance
32
+ end
33
+
34
+ it "defaults to markdown and HTML as documentation formats" do
35
+ expect(default_config.doc_formats).to match_array %w[
36
+ .md
37
+ .markdown
38
+ .htm
39
+ .html
40
+ ]
27
41
  end
28
- end
29
42
 
30
- context "with environment variables configured" do
31
- subject(:default_env_config) { DocRepo::Configuration.new }
43
+ it "defaults to a 'docs' root path" do
44
+ expect(default_config.doc_root).to eq 'docs'
45
+ end
32
46
 
33
- around do |example|
34
- env = {
35
- 'DOC_REPO_ORG' => 'the-silence',
36
- 'DOC_REPO_REPONAME' => 'falls',
37
- 'DOC_REPO_BRANCH' => 'ask_the_question',
38
- }
39
- using_env(env, &example)
47
+ it "defaults to '.md' for the fallback extension" do
48
+ expect(default_config.fallback_ext).to eq '.md'
40
49
  end
41
50
 
42
- it "uses DOC_REPO_ORG for the default org" do
43
- expect(default_env_config.org).to eq 'the-silence'
51
+ it "has no org set" do
52
+ expect(default_config.org).to be nil
44
53
  end
45
54
 
46
- it "uses DOC_REPO_REPONAME for the default repo" do
47
- expect(default_env_config.repo).to eq 'falls'
55
+ it "has no repo set" do
56
+ expect(default_config.repo).to be nil
48
57
  end
49
58
 
50
- it "uses DOC_REPO_BRANCH for the default branch" do
51
- expect(default_env_config.branch).to eq 'ask_the_question'
59
+ it "converting to a hash includes all settings" do
60
+ expect(default_config.to_h.keys).to match_array %i[
61
+ branch
62
+ cache_options
63
+ cache_store
64
+ doc_formats
65
+ doc_root
66
+ fallback_ext
67
+ org
68
+ repo
69
+ ]
52
70
  end
53
71
  end
54
72
 
73
+ include_examples "has setting", :branch
74
+ include_examples "has setting", :cache_options
75
+ include_examples "has setting", :cache_store
76
+ include_examples "has setting", :doc_formats
77
+ include_examples "has setting", :doc_root
78
+ include_examples "has setting", :fallback_ext
79
+ include_examples "has setting", :org
80
+ include_examples "has setting", :repo
81
+
82
+ it "converting to a hash maps all settings to configured values" do
83
+ a_config = DocRepo::Configuration.new
84
+ a_config.branch = "Any Branch"
85
+ a_config.cache_options = "Any Cache Options"
86
+ a_config.cache_store = "Any Cache Store"
87
+ a_config.doc_formats = %w[ .any .formats ]
88
+ a_config.doc_root = "Any Doc Root"
89
+ a_config.fallback_ext = ".anything"
90
+ a_config.org = "Any Org"
91
+ a_config.repo = "Any Repo"
92
+ expect(a_config.to_h).to eq(
93
+ branch: "Any Branch",
94
+ cache_options: "Any Cache Options",
95
+ cache_store: "Any Cache Store",
96
+ doc_formats: %w[ .any .formats ],
97
+ doc_root: "Any Doc Root",
98
+ fallback_ext: ".anything",
99
+ org: "Any Org",
100
+ repo: "Any Repo",
101
+ )
102
+ end
103
+
104
+ it "doesn't allow creating new settings" do
105
+ expect {
106
+ DocRepo::Configuration.add_setting :new_setting
107
+ }.to raise_error NoMethodError
108
+ end
109
+
55
110
  end
@@ -0,0 +1,442 @@
1
+ # frozen_string_literal: true
2
+ require 'ostruct'
3
+
4
+ RSpec.describe DocRepo::Doc do
5
+
6
+ context "a standard document", "with cachable content" do
7
+ subject(:a_document) {
8
+ DocRepo::Doc.new("/any/uri", any_http_ok_response)
9
+ }
10
+
11
+ let(:any_http_ok_response) {
12
+ instance_double("Net::HTTPOK", code: "200", body: "Any Content Body").tap { |dbl|
13
+ allow(dbl).to receive(:[]) { |key| response_cache_headers[key] }
14
+ }
15
+ }
16
+
17
+ let(:response_cache_headers) {
18
+ # Make string keys mutable to allow testing mutations
19
+ {
20
+ "Last-Modified" => String.new("Sat, 01 Jul 2017 18:18:33 GMT"),
21
+ "Content-Type" => String.new("text/plain"),
22
+ "Cache-Control" => String.new("max-age=300, private, must-revalidate"),
23
+ }
24
+ }
25
+
26
+ it "is not an error" do
27
+ expect(a_document).not_to be_an_error
28
+ end
29
+
30
+ it "is not missing" do
31
+ expect(a_document).not_to be_not_found
32
+ end
33
+
34
+ it "is not a redirect" do
35
+ expect(a_document).not_to be_a_redirect
36
+ end
37
+
38
+ it "is successful" do
39
+ expect(a_document).to be_a_success
40
+ end
41
+
42
+ it "has a numeric status code" do
43
+ expect(a_document.code).to eq 200
44
+ end
45
+
46
+ it "has a URI" do
47
+ expect(a_document.uri).to eq "/any/uri"
48
+ end
49
+
50
+ it "sets the last modified time from the cache headers" do
51
+ modified_time = Time.gm(2017, 7, 1, 18, 18, 33)
52
+ expect(a_document.last_modified).to eq(modified_time).and be_frozen
53
+ expect(response_cache_headers["Last-Modified"]).not_to be_frozen
54
+ end
55
+
56
+ it "sets the content to the response body" do
57
+ expect(a_document.content).to eq "Any Content Body"
58
+ end
59
+
60
+ it "sets the content type according to the associated header" do
61
+ expect(a_document.content_type).to eq "text/plain"
62
+ end
63
+
64
+ it "uses the URI as the cache key" do
65
+ expect(a_document.cache_key).to eq "/any/uri"
66
+ end
67
+
68
+ it "sets the cache version based on the raw content" do
69
+ content_version = Digest::SHA1.hexdigest("Any Content Body")
70
+ expect(a_document.cache_version).to eq(content_version).and be_frozen
71
+ end
72
+
73
+ it "has a convenience reader for a versioned cache key" do
74
+ content_version = Digest::SHA1.hexdigest("Any Content Body")
75
+ expect(a_document.cache_key_with_version).to eq "/any/uri-#{content_version}"
76
+ end
77
+
78
+ it "allows access to the cache control settings" do
79
+ expect(a_document.cache_control).to eq "max-age=300, private, must-revalidate"
80
+ end
81
+ end
82
+
83
+ context "a standard document", "with missing headers" do
84
+ subject(:uncachable_document) {
85
+ DocRepo::Doc.new("/any/uri", headerless_http_ok_response)
86
+ }
87
+
88
+ let(:headerless_http_ok_response) {
89
+ # Use either `:[] => nil` or `"[]" => nil` as `[]: nil` is invalid Ruby
90
+ instance_double(
91
+ "Net::HTTPOK",
92
+ "code" => "200",
93
+ "body" => nil,
94
+ "[]" => nil,
95
+ )
96
+ }
97
+
98
+ EMPTY_STRING_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
99
+
100
+ it "may not have a last modified timestamp", :aggregate_failures do
101
+ expect(uncachable_document.last_modified).to be nil
102
+
103
+ allow(headerless_http_ok_response).to receive(:[]) { |key|
104
+ "Last-Modified" == key ? "0" : nil
105
+ }
106
+ invalid_time = DocRepo::Doc.new("/any/uri", headerless_http_ok_response)
107
+ expect(invalid_time.last_modified).to be nil
108
+ end
109
+
110
+ it "may not have any content type" do
111
+ expect(uncachable_document.content_type).to be nil
112
+ end
113
+
114
+ it "always has a cache version" do
115
+ expect(uncachable_document.cache_version).to eq EMPTY_STRING_SHA1
116
+ end
117
+
118
+ it "has a convenience reader for a versioned cache key" do
119
+ expect(uncachable_document.cache_key_with_version).to eq "/any/uri-#{EMPTY_STRING_SHA1}"
120
+ end
121
+
122
+ it "may not have any cache control settings" do
123
+ expect(uncachable_document.cache_control).to be nil
124
+ end
125
+ end
126
+
127
+ describe "converting markdown content to HTML" do
128
+ it "handles empty content" do
129
+ empty_content = OpenStruct.new(body: nil)
130
+ empty_doc = DocRepo::Doc.new("/any/uri", empty_content)
131
+ expect(empty_doc.to_html).to eq ""
132
+ end
133
+
134
+ it "does not parse emphasis inside of words" do
135
+ em_content = OpenStruct.new(body: <<~MARKDOWN)
136
+ _this is emphasis_
137
+
138
+ this_is_not
139
+ MARKDOWN
140
+ em_doc = DocRepo::Doc.new("/any/uri", em_content)
141
+ expect(em_doc.to_html).to eq <<~HTML
142
+ <p><em>this is emphasis</em></p>
143
+
144
+ <p>this_is_not</p>
145
+ HTML
146
+ end
147
+
148
+ it "parse tables" do
149
+ tables = OpenStruct.new(body: <<~MARKDOWN)
150
+ | Heading 1 | Heading 2 |
151
+ |-----------|-----------|
152
+ | Content A | Content B |
153
+ | Content C | Content D |
154
+
155
+ Heading 3 | Heading 4
156
+ -----------|-----------
157
+ Content E | Content F
158
+ Content G | Content H
159
+
160
+ | Left align | Right align | Center align |
161
+ |:-----------|------------:|:------------:|
162
+ | left | right | center |
163
+ | aligned | aligned | aligned |
164
+ MARKDOWN
165
+ table_doc = DocRepo::Doc.new("/any/uri", tables)
166
+ expect(table_doc.to_html).to eq <<~HTML
167
+ <table><thead>
168
+ <tr>
169
+ <th>Heading 1</th>
170
+ <th>Heading 2</th>
171
+ </tr>
172
+ </thead><tbody>
173
+ <tr>
174
+ <td>Content A</td>
175
+ <td>Content B</td>
176
+ </tr>
177
+ <tr>
178
+ <td>Content C</td>
179
+ <td>Content D</td>
180
+ </tr>
181
+ </tbody></table>
182
+
183
+ <table><thead>
184
+ <tr>
185
+ <th>Heading 3</th>
186
+ <th>Heading 4</th>
187
+ </tr>
188
+ </thead><tbody>
189
+ <tr>
190
+ <td>Content E</td>
191
+ <td>Content F</td>
192
+ </tr>
193
+ <tr>
194
+ <td>Content G</td>
195
+ <td>Content H</td>
196
+ </tr>
197
+ </tbody></table>
198
+
199
+ <table><thead>
200
+ <tr>
201
+ <th style="text-align: left">Left align</th>
202
+ <th style="text-align: right">Right align</th>
203
+ <th style="text-align: center">Center align</th>
204
+ </tr>
205
+ </thead><tbody>
206
+ <tr>
207
+ <td style="text-align: left">left</td>
208
+ <td style="text-align: right">right</td>
209
+ <td style="text-align: center">center</td>
210
+ </tr>
211
+ <tr>
212
+ <td style="text-align: left">aligned</td>
213
+ <td style="text-align: right">aligned</td>
214
+ <td style="text-align: center">aligned</td>
215
+ </tr>
216
+ </tbody></table>
217
+ HTML
218
+ end
219
+
220
+ it "supports fenced code blocks" do
221
+ fenced_code = OpenStruct.new(body: <<~MARKDOWN)
222
+ Plain Code Block:
223
+
224
+ ```
225
+ plain = code
226
+ block { do_something }
227
+ ```
228
+
229
+ Language Formatted Block:
230
+
231
+ ```ruby
232
+ str = "tagged code"
233
+ # with coments
234
+ block { do_something }
235
+ ```
236
+
237
+ Alternative Block Delimiter:
238
+
239
+ ~~~js
240
+ alternative = { style: "javascript" }
241
+ ~~~
242
+
243
+ End
244
+ MARKDOWN
245
+ code_doc = DocRepo::Doc.new("/any/uri", fenced_code)
246
+ expect(code_doc.to_html).to eq <<~HTML
247
+ <p>Plain Code Block:</p>
248
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
249
+ block { do_something }
250
+ </code></pre></div>
251
+ <p>Language Formatted Block:</p>
252
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
253
+ <span class="c1"># with coments</span>
254
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
255
+ </code></pre></div>
256
+ <p>Alternative Block Delimiter:</p>
257
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
258
+ </code></pre></div>
259
+ <p>End</p>
260
+ HTML
261
+ end
262
+
263
+ it "autolinks URLs" do
264
+ embedded_urls = OpenStruct.new(body: <<~MARKDOWN)
265
+ Many people use www.google.com to search. But they really should use
266
+ https://www.duckduckgo.com if they want some security and privacy.
267
+
268
+ Is it still common to use http://example.com? What about just example.com
269
+
270
+ And no one uses ftp://example.com any more.
271
+ MARKDOWN
272
+ linked_doc = DocRepo::Doc.new("/any/uri", embedded_urls)
273
+ expect(linked_doc.to_html).to eq <<~HTML
274
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
275
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
276
+
277
+ <p>Is it still common to use <a href="http://example.com">http://example.com</a>? What about just example.com</p>
278
+
279
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
280
+ HTML
281
+ end
282
+
283
+ it "autolinks email addresses" do
284
+ embedded_email = OpenStruct.new(body: <<~MARKDOWN)
285
+ If you need some help just contant support@example.com.
286
+ MARKDOWN
287
+ mail_doc = DocRepo::Doc.new("/any/uri", embedded_email)
288
+ expect(mail_doc.to_html).to eq <<~HTML
289
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
290
+ HTML
291
+ end
292
+
293
+ it "supports styling strikethrough content" do
294
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
295
+ this is ~~good~~ bad
296
+ MARKDOWN
297
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
298
+ expect(styled_doc.to_html).to eq <<~HTML
299
+ <p>this is <del>good</del> bad</p>
300
+ HTML
301
+ end
302
+
303
+ it "supports styling superscript content" do
304
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
305
+ this is the 2^(nd) time
306
+ MARKDOWN
307
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
308
+ expect(styled_doc.to_html).to eq <<~HTML
309
+ <p>this is the 2<sup>nd</sup> time</p>
310
+ HTML
311
+ end
312
+
313
+ it "adds HTML anchors to headers" do
314
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
315
+ # Heading 1
316
+
317
+ Content A
318
+
319
+ ## Sub-Heading 2
320
+
321
+ Content B
322
+ MARKDOWN
323
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
324
+ expect(styled_doc.to_html).to eq <<~HTML
325
+ <h1 id="heading-1">Heading 1</h1>
326
+
327
+ <p>Content A</p>
328
+
329
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
330
+
331
+ <p>Content B</p>
332
+ HTML
333
+ end
334
+
335
+ it "leaves markdown compliant HTML unchanged but may add whitespace" do
336
+ html_content = OpenStruct.new(body: <<~HTML)
337
+ <h1 id="heading-1">Heading 1</h1>
338
+ <p>Content A</p>
339
+ <p>this is <del>good</del> bad</p>
340
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
341
+ <p>Plain Code Block:</p>
342
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
343
+ block { do_something }
344
+ </code></pre></div>
345
+ <p>Language Formatted Block:</p>
346
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
347
+ <span class="c1"># with coments</span>
348
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
349
+ </code></pre></div>
350
+ <p>Alternative Block Delimiter:</p>
351
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
352
+ </code></pre></div>
353
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
354
+ <p>this is the 2<sup>nd</sup> time</p>
355
+ <h3>Sub-Sub-Heading 3</h3>
356
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
357
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
358
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
359
+ <table><thead>
360
+ <tr>
361
+ <th style="text-align: left">Left align</th>
362
+ <th style="text-align: right">Right align</th>
363
+ <th style="text-align: center">Center align</th>
364
+ </tr>
365
+ </thead><tbody>
366
+ <tr>
367
+ <td style="text-align: left">left</td>
368
+ <td style="text-align: right">right</td>
369
+ <td style="text-align: center">center</td>
370
+ </tr>
371
+ <tr>
372
+ <td style="text-align: left">aligned</td>
373
+ <td style="text-align: right">aligned</td>
374
+ <td style="text-align: center">aligned</td>
375
+ </tr>
376
+ </tbody></table>
377
+ <p>Final Content</p>
378
+ HTML
379
+ html_doc = DocRepo::Doc.new("/any/uri", html_content)
380
+ expect(html_doc.to_html).to eq <<~HTML
381
+ <h1 id="heading-1">Heading 1</h1>
382
+
383
+ <p>Content A</p>
384
+
385
+ <p>this is <del>good</del> bad</p>
386
+
387
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
388
+
389
+ <p>Plain Code Block:</p>
390
+
391
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
392
+ block { do_something }
393
+ </code></pre></div>
394
+
395
+ <p>Language Formatted Block:</p>
396
+
397
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
398
+ <span class="c1"># with coments</span>
399
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
400
+ </code></pre></div>
401
+
402
+ <p>Alternative Block Delimiter:</p>
403
+
404
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
405
+ </code></pre></div>
406
+
407
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
408
+
409
+ <p>this is the 2<sup>nd</sup> time</p>
410
+
411
+ <h3>Sub-Sub-Heading 3</h3>
412
+
413
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
414
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
415
+
416
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
417
+
418
+ <table><thead>
419
+ <tr>
420
+ <th style="text-align: left">Left align</th>
421
+ <th style="text-align: right">Right align</th>
422
+ <th style="text-align: center">Center align</th>
423
+ </tr>
424
+ </thead><tbody>
425
+ <tr>
426
+ <td style="text-align: left">left</td>
427
+ <td style="text-align: right">right</td>
428
+ <td style="text-align: center">center</td>
429
+ </tr>
430
+ <tr>
431
+ <td style="text-align: left">aligned</td>
432
+ <td style="text-align: right">aligned</td>
433
+ <td style="text-align: center">aligned</td>
434
+ </tr>
435
+ </tbody></table>
436
+
437
+ <p>Final Content</p>
438
+ HTML
439
+ end
440
+ end
441
+
442
+ end