# frozen_string_literal: true require 'spec_helper' require 'nokogiri' module Gitlab RSpec.describe Asciidoc do include FakeBlobHelpers before do allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) end context "without project" do let(:input) { 'ascii' } let(:context) { {} } let(:html) { 'H2O' } it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }), extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) expect(render(input, context)).to eq(html) end context "with asciidoc_opts" do it "merges the options with default ones" do expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }), extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) render(input, context) end end context "with requested path" do input = <<~ADOC Document name: {docname}. ADOC it "ignores {docname} when not available" do expect(render(input, {})).to include(input.strip) end [ ['/', '', 'root'], ['README', 'README', 'just a filename'], ['doc/api/', '', 'a directory'], ['doc/api/README.adoc', 'README', 'a complete path'] ].each do |path, basename, desc| it "sets {docname} for #{desc}" do expect(render(input, { requested_path: path })).to include(": #{basename}.") end end end context "XSS" do items = { 'link with extra attribute' => { input: 'link:mylink"onmouseover="alert(1)[Click Here]', output: "
\">
\nAn admonition paragraph, like this note, grabs the reader’s attention. |
This paragraph has a footnote.[1]
Learn how to use cross references.
A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
checked
not checked
Werewolves are allergic to cassia cinnamon.
Did the werewolves read the small print?
Where did all the cores run off to?
We need ten make that twenty VMs.
Once upon an infinite loop.
console.log('hello world')
#include <stdio.h>
for (int i = 0; i < 5; i++) {
std::cout<<"*"<<std::endl;
}
eta_x gamma
')
expect(render(input, context)).to include('2+2
is 4
graph LR A[Square Rect] -- Link text --> B((Circle)) A --> C(Round Rect) B --> D{Rhombus} C --> DHTML expect(render(input, context)).to include(output.strip) end it 'applies subs in diagram block' do input = <<~MD :class-name: AveryLongClass [mermaid,subs=+attributes] .... classDiagram Class01 <|-- {class-name} : Cool .... MD output = <<~HTML
classDiagram Class01 <|-- AveryLongClass : CoolHTML expect(render(input, context)).to include(output.strip) end end context 'with Kroki enabled' do before do allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true) allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io') end it 'converts a graphviz diagram to image' do input = <<~ADOC [graphviz] .... digraph G { Hello->World } .... ADOC output = <<~HTML HTML expect(render(input, context)).to include(output.strip) end end end context 'with project' do let(:context) do { commit: commit, project: project, ref: ref, requested_path: requested_path } end let(:commit) { project.commit(ref) } let(:project) { create(:project, :repository) } let(:ref) { 'asciidoc' } let(:requested_path) { '/' } context 'include directive' do subject(:output) { render(input, context) } let(:input) { "Include this:\n\ninclude::#{include_path}[]" } before do current_file = requested_path current_file += 'README.adoc' if requested_path.end_with? '/' create_file(current_file, "= AsciiDoc\n") end def many_includes(target) Array.new(10, "include::#{target}[]").join("\n") end context 'cyclic imports' do before do create_file('doc/api/a.adoc', many_includes('b.adoc')) create_file('doc/api/b.adoc', many_includes('a.adoc')) end let(:include_path) { 'a.adoc' } let(:requested_path) { 'doc/api/README.md' } it 'completes successfully' do is_expected.to include('
Include this:
') end end context 'with path to non-existing file' do let(:include_path) { 'not-exists.adoc' } it 'renders Unresolved directive placeholder' do is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]") end end shared_examples :invalid_include do let(:include_path) { 'dk.png' } before do allow(project.repository).to receive(:blob_at).and_return(blob) end it 'does not read the blob' do expect(blob).not_to receive(:data) end it 'renders Unresolved directive placeholder' do is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]") end end context 'with path to a binary file' do let(:blob) { fake_blob(path: 'dk.png', binary: true) } include_examples :invalid_include end context 'with path to file in external storage' do let(:blob) { fake_blob(path: 'dk.png', lfs: true) } before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) project.update_attribute(:lfs_enabled, true) end include_examples :invalid_include end context 'with path to a textual file' do let(:include_path) { 'sample.adoc' } before do create_file(file_path, "Content from #{include_path}") end shared_examples :valid_include do [ ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'], ['sample.adoc', 'doc/api/sample.adoc', 'relative path'], ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'], ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'], ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories'] ].each do |include_path_, file_path_, desc| context "the file is specified by #{desc}" do let(:include_path) { include_path_ } let(:file_path) { file_path_ } it 'includes content of the file' do is_expected.to include('Include this:
') is_expected.to include("Content from #{include_path}
") end end end end context 'when requested path is a file in the repo' do let(:requested_path) { 'doc/api/README.adoc' } include_examples :valid_include context 'without a commit (only ref)' do let(:commit) { nil } include_examples :valid_include end end context 'when requested path is a directory in the repo' do let(:requested_path) { 'doc/api/' } include_examples :valid_include context 'without a commit (only ref)' do let(:commit) { nil } include_examples :valid_include end end end context 'when repository is passed into the context' do let(:wiki_repo) { project.wiki.repository } let(:include_path) { 'wiki_file.adoc' } before do project.create_wiki context.merge!(repository: wiki_repo) end context 'when the file exists' do before do create_file(include_path, 'Content from wiki', repository: wiki_repo) end it { is_expected.to include('Content from wiki
') } end context 'when the file does not exist' do it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")} end end context 'recursive includes with relative paths' do let(:input) do <<~ADOC Source: requested file include::doc/README.adoc[] include::license.adoc[] ADOC end before do create_file 'doc/README.adoc', <<~ADOC Source: doc/README.adoc include::../license.adoc[] include::api/hello.adoc[] ADOC create_file 'license.adoc', <<~ADOC Source: license.adoc ADOC create_file 'doc/api/hello.adoc', <<~ADOC Source: doc/api/hello.adoc include::./common.adoc[] ADOC create_file 'doc/api/common.adoc', <<~ADOC Source: doc/api/common.adoc ADOC end it 'includes content of the included files recursively' do expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip Source: requested file Source: doc/README.adoc Source: license.adoc Source: doc/api/hello.adoc Source: doc/api/common.adoc Source: license.adoc ADOC end end def create_file(path, content, repository: project.repository) repository.create_file(project.creator, path, content, message: "Add #{path}", branch_name: 'asciidoc') end end end def render(*args) described_class.render(*args) end end end