integtest/spec/preview_spec.rb (581 lines of code) (raw):
# frozen_string_literal: true
require 'fileutils'
require 'net/http'
require 'securerandom'
##
# Test for a preview that is usually deployed in Elastic Apps. It previews all
# branches of the `--target_repo`. The test runs everything in the defined
# order because starting the preview is fairly heavy and the preview is
# designed to update itself as its target repo changes so we start it once and
# play with the target repo during the tests.
RSpec.describe 'previewing built docs', order: :defined do
very_large_text = 'muchtext' * 1024 * 1024 * 5 # 40mb
repo_root = File.expand_path '../../', __dir__
readme_resources = "#{repo_root}/resources/readme"
convert_before do |src, dest|
repo = src.repo_with_index 'repo', <<~ASCIIDOC
Some text.
image::resources/readme/cat.jpg[A cat]
image::resources/readme/example.svg[An example svg]
image::resources/very_large.jpg[Not a jpg but very big]
ASCIIDOC
repo.cp "#{readme_resources}/cat.jpg", 'resources/readme/cat.jpg'
repo.cp "#{readme_resources}/example.svg", 'resources/readme/example.svg'
repo.write 'resources/very_large.jpg', very_large_text
repo.commit 'add images'
book = src.book 'Test'
book.source repo, 'index.asciidoc'
book.source repo, 'resources'
dest.prepare_convert_all(src.conf).convert
end
before(:context) do
@preview = @dest.start_preview
end
after(:context) do
@preview&.exit
end
let(:repo) { @dest.bare_repo.sub '.git', '' }
let(:preview) { @preview }
let(:logs) { preview.logs }
# The preview server reads the branch from the `Host` header. It throws out
# everything after and including the first `.` so you can hit a branch
# at urls like `http://master.docs-preview.app.elstc.co/`. Also, if it
# can't find the `.` then it just assumes master.
let(:host) { "#{branch}.localhost" }
def wait_for_logs(regexp, timeout: 10)
preview.wait_for_logs(regexp, timeout)
rescue Timeout::Error
expect(preview.logs).to match(regexp)
end
def wait_for_access(path)
wait_for_logs(/^#{watermark} #{host}.+#{path}.+$/)
end
def get(watermark, branch, path)
uri = URI("http://localhost:8000/#{path}")
req = Net::HTTP::Get.new(uri)
raise "branches can't contain [.]" if branch.include? '.'
req['X-Opaque-Id'] = watermark
req['Host'] = host
Net::HTTP.start(uri.hostname, uri.port, read_timeout: 20) do |http|
http.request(req)
end
end
shared_context 'docs for branch' do
watermark = SecureRandom.uuid
let(:watermark) { watermark }
let(:current_url) { 'guide/test/current' }
let(:liveness) { get watermark, branch, 'liveness' }
let(:diff) { get watermark, branch, 'diff' }
let(:robots_txt) { get watermark, branch, 'robots.txt' }
let(:root) { get watermark, branch, '' }
let(:guide_root) { get watermark, branch, 'guide/index.html' }
let(:current_index) { get watermark, branch, "#{current_url}/index.html" }
let(:current_toc) { get watermark, branch, "#{current_url}/toc.html" }
let(:cat_image) do
get watermark, branch, "#{current_url}/resources/readme/cat.jpg"
end
let(:svg_image) do
get watermark, branch, "#{current_url}/resources/readme/example.svg"
end
let(:very_large) do
get watermark, branch, "#{current_url}/resources/very_large.jpg"
end
let(:directory) do
get watermark, branch, 'guide'
end
let(:legacy_redirect) do
get watermark, branch, 'guide/reference/setup/'
end
let(:outside_of_guide) do
get watermark, branch, 'cloud/elasticsearch-service/signup'
end
end
let(:expected_js_state) { {} }
let(:expected_language) { 'en' }
it 'logs that the built docs are ready' do
wait_for_logs(/Built docs are ready/)
end
include_examples 'the favicon'
shared_examples 'serves some docs' do |supports_gapped: true|
context 'the liveness check' do
it '200s' do
expect(liveness).to serve(include("R'lyeh"))
end
end
context 'the root' do
it 'redirects to the guide root' do
expect(root).to redirect_to(eq('/guide/index.html'))
end
it 'logs the access to the docs root' do
wait_for_access '/'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET / HTTP/1.1 301
LOGS
end
end
context 'the docs root' do
it 'contains a link to the current index' do
expect(guide_root).to serve(doc_body(include(<<~HTML.strip)))
<a href="test/current/index.html" class="ulink" target="_top">Test</a>
HTML
end
it 'logs access to the docs root' do
wait_for_access '/'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/index.html HTTP/1.1 200
LOGS
end
it 'contains the gtag js' do
expect(guide_root).to serve(include(<<~HTML.strip))
https://www.googletagmanager.com/gtag/js
HTML
end
it 'logs the access to the guide root' do
wait_for_access '/guide/index.html'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/index.html HTTP/1.1 200
LOGS
end
if supports_gapped
context 'when air gapped' do
let(:host) { "gapped_#{branch}.localhost" }
it "doesn't contain the gtag js" do
expect(guide_root).not_to serve(include(<<~HTML.strip))
https://www.googletagmanager.com/gtag/js
HTML
end
it 'logs the access to the air gapped docs root' do
wait_for_access '/guide/index.html'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/index.html HTTP/1.1 200
LOGS
end
end
end
end
context 'the current index' do
it 'has the correct initial_js_state' do
expect(current_index).to serve(initial_js_state(eq(expected_js_state)))
end
it 'has the correct language' do
expect(current_index).to serve(include(<<~HTML.strip))
<section id="guide" lang="#{expected_language}">
HTML
end
end
context 'the current table of contents' do
it "isn't templated" do
expect(current_toc).to serve(start_with('<div class="toc">'))
end
end
it 'serves a "go away" robots.txt' do
expect(robots_txt).to serve(eq(<<~TXT))
User-agent: *
Disallow: /
TXT
expect(robots_txt['Content-Type']).to eq('text/plain')
end
context 'for a legacy redirect' do
it 'serves the redirect' do
expect(legacy_redirect).to redirect_to(
eq(
'/guide/en/elasticsearch/reference/current/setup.html'
)
)
end
it 'logs the access to the legacy redirect' do
wait_for_access '/guide/reference/setup/'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/reference/setup/ HTTP/1.1 301
LOGS
end
end
context 'for a url outside of the docs' do
it 'redirects to the public site' do
expect(outside_of_guide).to redirect_to(
eq('https://www.elastic.co/cloud/elasticsearch-service/signup'),
'302'
)
end
it 'logs the access to the url outside of the docs' do
wait_for_access '/cloud/elasticsearch-service/signup'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /cloud/elasticsearch-service/signup HTTP/1.1 302
LOGS
end
if supports_gapped
context 'when air gapped' do
let(:host) { "gapped_#{branch}.localhost" }
it '404s' do
expect(outside_of_guide.code).to eq('404')
end
it 'logs the access to the url outside of the docs' do
wait_for_access '/cloud/elasticsearch-service/signup'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /cloud/elasticsearch-service/signup HTTP/1.1 404
LOGS
end
end
end
end
end
shared_examples '404s' do
context 'the liveness check' do
it '200s' do
expect(liveness).to serve(include("R'lyeh"))
end
end
it '404s for the docs root' do
expect(guide_root.code).to eq('404')
end
it 'logs the access to the docs root' do
wait_for_access '/guide/index.html'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/index.html HTTP/1.1 404
LOGS
end
it '404s for the diff' do
expect(diff.code).to eq('404')
end
it 'logs the access to the diff' do
wait_for_access '/diff'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /diff HTTP/1.1 404
LOGS
end
end
shared_examples 'valid diff' do
it 'has the html5 doctype' do
expect(diff).to serve(include('<!DOCTYPE html>'))
end
it 'has the branch in the title' do
expect(diff).to serve(include("<title>Diff for #{branch}</title>"))
end
it "doesn't contain a link to the sitemap" do
expect(diff).not_to serve(include('sitemap.xml'))
end
it "doesn't contain a link to the revision file" do
expect(diff).not_to serve(include('revisions.txt'))
end
it "doesn't contain a link to the branch tracker file" do
expect(diff).not_to serve(include('branches.yaml'))
end
it "doesn't warn about unprocesed output" do
expect(diff).not_to serve(include('Unprocessed results from git'))
end
it 'logs access to the diff when it is accessed' do
wait_for_access '/diff'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /diff HTTP/1.1 200
LOGS
end
end
describe 'for the master branch' do
let(:branch) { 'master' }
include_context 'docs for branch'
include_examples 'serves some docs'
context 'for a JPG' do
it 'serves the right bytes' do
bytes = File.open("#{readme_resources}/cat.jpg", 'rb', &:read)
expect(cat_image).to serve(eq(bytes))
end
it 'serves the right Content-Type' do
expect(cat_image['Content-Type']).to eq('image/jpeg')
end
end
context 'for an SVG' do
it 'serves the right bytes' do
bytes = File.open("#{readme_resources}/example.svg", 'rb', &:read)
expect(svg_image).to serve(eq(bytes))
end
it 'serves the right Content-Type' do
expect(svg_image['Content-Type']).to eq('image/svg+xml')
end
end
it 'serves a very large image' do
expect(very_large).to serve(eq(very_large_text))
end
context 'when you request a directory' do
it 'redirects to index.html' do
expect(directory).to redirect_to(eq('/guide/index.html'))
end
end
end
describe "when the host header doesn't have a `.` " \
'it serves that master branch' do
let(:host) { 'localhost' }
let(:branch) { 'master' }
# This doesn't support air gapped docs because we can't ask for them without
# the `.`.
include_examples 'serves some docs', supports_gapped: false
include_context 'docs for branch'
context 'the diff' do
include_examples 'valid diff'
end
end
describe 'for the test branch' do
let(:branch) { 'test' }
include_context 'docs for branch'
include_examples '404s'
end
describe 'when we commit to the test branch of the target repo' do
before(:context) do
repo = @src.repo 'repo'
repo.write 'index.asciidoc', <<~ASCIIDOC
= Title
[[moved_chapter]]
== Chapter
Some text.
ASCIIDOC
repo.commit 'test change for test branch'
@dest.prepare_convert_all(@src.conf).target_branch('test').convert
end
it 'logs the fetch' do
wait_for_logs(/\[new branch\]\s+test\s+->\s+test/)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
* [new branch] test -> test
LOGS
end
describe 'for the test branch' do
let(:branch) { 'test' }
include_context 'docs for branch'
include_examples 'serves some docs'
context 'the diff' do
include_examples 'valid diff'
it 'contains a link to the index which has changed' do
expect(diff).to serve(include(<<~HTML))
+4 -4 <a href="/guide/test/master/index.html">test/master/index.html</a>
HTML
end
it 'contains a link to the moved chapter' do
# TODO: We didn't *just* move the chapter. We dropped some images.
# This seems like a mistake but fixing that breaks other tests.
expect(diff).to serve(include(<<~HTML))
+1 -16 <a href="/guide/test/master/moved_chapter.html">test/master/chapter.html -> test/master/moved_chapter.html</a>
HTML
end
it "doesn't have a message saying there aren't any differences" do
expect(diff).not_to serve(include(<<~HTML))
<p>There aren't any differences!</p>
HTML
end
end
shared_examples 'logs the fetch' do
it 'logs the fetch' do
wait_for_logs(/#{@before_hash}\.\.#{@after_hash}\s+test\s+->\s+test/)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
#{@before_hash}..#{@after_hash} test -> test
LOGS
end
end
describe 'when we modify the template' do
before(:context) do
# This simulates modifying the template in the docs repo and running
# build_docs --all
work = @src.repo 'work'
work.clone_from @dest.bare_repo
work.switch_to_branch 'test'
@before_hash = work.short_hash
old_template = work.read 'template.html'
work.write 'template.html', old_template + 'trailing garbage'
work.commit 'add garbage to template'
work.push_to @dest.bare_repo
@after_hash = work.short_hash
end
include_examples 'logs the fetch'
it 'is immediately reflected in the root' do
expect(guide_root).to serve(include(<<~HTML.strip))
</html>\ntrailing garbage
HTML
end
end
describe 'for a very very large html page' do
before(:context) do
# This simulates adding a very large html page without having to
# render it through asciidoc and docbook which would be very slow
work = @src.repo 'work'
@before_hash = work.short_hash
work.write 'raw/very_large.html', <<~HTML
<!DOCTYPE html>
<html>
<head><title>very large</title></head>
<body>#{very_large_text}</body>
</html>
HTML
work.commit 'add huge page'
work.push_to @dest.bare_repo
@after_hash = work.short_hash
end
let(:very_large_html) do
get watermark, branch, 'guide/very_large.html'
end
include_examples 'logs the fetch'
it 'serves the very large page without crashing' do
expect(very_large_html).to serve(include(very_large_text))
end
it 'logs the access to the very large page' do
wait_for_access '/guide/very_large.html'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/very_large.html HTTP/1.1 200
LOGS
end
end
describe 'when we remove the template' do
before(:context) do
# This simulates what preview branches looked like before committing
# the template.
work = @src.repo 'work'
@before_hash = work.short_hash
work.delete 'template.html'
work.commit 'remove template'
work.push_to @dest.bare_repo
@after_hash = work.short_hash
end
include_examples 'logs the fetch'
describe 'everything still works because we fall back' do
include_examples 'serves some docs', supports_gapped: false
end
end
end
end
describe 'after we remove the test branch from the target repo' do
before(:context) do
@dest.remove_target_brach 'test'
end
it 'logs the fetch' do
wait_for_logs(/\[deleted\]\s+\(none\)\s+->\s+test/)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
- [deleted] (none) -> test
LOGS
end
describe 'for the test branch' do
let(:branch) { 'test' }
include_context 'docs for branch'
include_examples '404s'
end
end
describe 'when we commit a noop change' do
before(:context) do
repo = @src.repo 'repo'
repo.write 'index.asciidoc', <<~ASCIIDOC
= Title
[[chapter]]
== Chapter
Some text.
image::resources/readme/cat.jpg[A cat]
image::resources/readme/example.svg[An example svg]
image::resources/very_large.jpg[Not a jpg but very big]
ASCIIDOC
repo.commit 'test change for test_noop branch2'
@dest.prepare_convert_all(@src.conf).target_branch('test_noop').convert
end
it 'logs the fetch' do
wait_for_logs(/\[new branch\]\s+test_noop\s+->\s+test_noop/)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
* [new branch] test_noop -> test_noop
LOGS
end
describe 'for the test branch' do
let(:branch) { 'test_noop' }
include_context 'docs for branch'
include_examples 'serves some docs'
context 'the diff' do
include_examples 'valid diff'
it 'is empty' do
expect(diff).to serve(include("<ul>\n</ul>"))
end
it "has a message saying there aren't any differences" do
expect(diff).to serve(include("<p>There aren't any differences!</p>"))
end
end
end
end
describe 'when there are alternative examples' do
before(:context) do
# We don't have any examples in our source document so we'll just make a
# dummy file so the checkout works. This is good enough to make a
# distinct initial_js_state
csharp_repo = @src.repo 'csharp'
csharp_repo.write 'examples/dummy', 'dummy'
csharp_repo.commit 'add example'
book = @src.book 'Test'
book.source(
csharp_repo,
'examples',
alternatives: { source_lang: 'console', alternative_lang: 'csharp' }
)
@dest.prepare_convert_all(@src.conf)
.target_branch('alternative_examples')
.convert
end
it 'logs the fetch' do
wait_for_logs(
/\[new branch\]\s+alternative_examples\s+->\s+alternative_examples/
)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
* [new branch] alternative_examples -> alternative_examples
LOGS
end
let(:branch) { 'alternative_examples' }
let(:expected_js_state) do
{
alternatives: {
console: {
csharp: { hasAny: false },
},
},
}
end
include_context 'docs for branch'
include_examples 'serves some docs'
end
describe 'when the language is something other than `en`' do
before(:context) do
book = @src.book 'Test'
book.lang = 'foo'
@dest.prepare_convert_all(@src.conf).target_branch('foolang').convert
end
it 'logs the fetch' do
wait_for_logs(
/\[new branch\]\s+foolang\s+->\s+foolang/
)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
* [new branch] foolang -> foolang
LOGS
end
let(:branch) { 'foolang' }
let(:expected_js_state) do
{
alternatives: {
console: {
csharp: { hasAny: false },
},
},
}
end
let(:expected_language) { 'foo' }
include_context 'docs for branch'
include_examples 'serves some docs'
end
describe "when we can't pull from the repo" do
before(:context) do
FileUtils.rm_rf '/tmp/backup'
FileUtils.mv @dest.bare_repo, '/tmp/backup'
end
it 'logs an angry message' do
wait_for_logs(/Could not read from remote repository./)
end
end
describe 'when we can pull again' do
before(:context) do
FileUtils.mv '/tmp/backup', @dest.bare_repo
@dest.prepare_convert_all(@src.conf).target_branch('restored').convert
end
it 'fetches' do
wait_for_logs(
/\[new branch\]\s+restored\s+->\s+restored/
)
# The leading space in the second line is important because it causes
# filebeat to group the two log lines.
expect(logs).to include("\n" + <<~LOGS)
From #{repo}
* [new branch] restored -> restored
LOGS
end
let(:branch) { 'restored' }
include_context 'docs for branch'
context 'the docs root' do
it 'contains a link to the current index' do
expect(guide_root).to serve(doc_body(include(<<~HTML.strip)))
<a href="test/current/index.html" class="ulink" target="_top">Test</a>
HTML
end
it 'logs access to the docs root' do
wait_for_access '/'
expect(logs).to include(<<~LOGS)
#{watermark} #{host} GET /guide/index.html HTTP/1.1 200
LOGS
end
end
end
end