Commit ccd748d7 authored by Jesse Hall's avatar Jesse Hall

Feature for #16654, embedded audio elements in markdown.

parent e7b3aa9e
...@@ -21,6 +21,7 @@ import Reference from './nodes/reference'; ...@@ -21,6 +21,7 @@ import Reference from './nodes/reference';
import TableOfContents from './nodes/table_of_contents'; import TableOfContents from './nodes/table_of_contents';
import Video from './nodes/video'; import Video from './nodes/video';
import Audio from './nodes/audio';
import BulletList from './nodes/bullet_list'; import BulletList from './nodes/bullet_list';
import OrderedList from './nodes/ordered_list'; import OrderedList from './nodes/ordered_list';
...@@ -78,6 +79,7 @@ export default [ ...@@ -78,6 +79,7 @@ export default [
new TableOfContents(), new TableOfContents(),
new Video(), new Video(),
new Audio(),
new BulletList(), new BulletList(),
new OrderedList(), new OrderedList(),
......
/* eslint-disable class-methods-use-this */
import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
// Transforms generated HTML back to GFM for Banzai::Filter::AudioLinkFilter
export default class Audio extends Node {
get name() {
return 'audio';
}
get schema() {
return {
attrs: {
src: {},
alt: {
default: null,
},
},
group: 'block',
draggable: true,
parseDOM: [
{
tag: '.audio-container',
skip: true,
},
{
tag: '.audio-container p',
priority: 51,
ignore: true,
},
{
tag: 'audio[src]',
getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
},
],
toDOM: node => [
'audio',
{
src: node.attrs.src,
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
},
],
};
}
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
state.closeBlock(node);
}
}
...@@ -37,7 +37,7 @@ module UploadsActions ...@@ -37,7 +37,7 @@ module UploadsActions
expires_in 0.seconds, must_revalidate: true, private: true expires_in 0.seconds, must_revalidate: true, private: true
end end
disposition = uploader.image_or_video? ? 'inline' : 'attachment' disposition = uploader.embeddable? ? 'inline' : 'attachment'
uploaders = [uploader, *uploader.versions.values] uploaders = [uploader, *uploader.versions.values]
uploader = uploaders.find { |version| version.filename == params[:filename] } uploader = uploaders.find { |version| version.filename == params[:filename] }
...@@ -112,8 +112,8 @@ module UploadsActions ...@@ -112,8 +112,8 @@ module UploadsActions
uploader uploader
end end
def image_or_video? def embeddable?
uploader && uploader.exists? && uploader.image_or_video? uploader && uploader.exists? && uploader.embeddable?
end end
def find_model def find_model
......
...@@ -4,7 +4,7 @@ class Groups::UploadsController < Groups::ApplicationController ...@@ -4,7 +4,7 @@ class Groups::UploadsController < Groups::ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? } skip_before_action :group, if: -> { action_name == 'show' && embeddable? }
before_action :authorize_upload_file!, only: [:create, :authorize] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]
......
...@@ -40,8 +40,8 @@ class HelpController < ApplicationController ...@@ -40,8 +40,8 @@ class HelpController < ApplicationController
end end
end end
# Allow access to images in the doc folder # Allow access to specific media files in the doc folder
format.any(:png, :gif, :jpeg, :mp4) do format.any(:png, :gif, :jpeg, :mp4, :mp3) do
# Note: We are purposefully NOT using `Rails.root.join` # Note: We are purposefully NOT using `Rails.root.join`
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}") path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
......
...@@ -6,7 +6,7 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::UploadsController < Projects::ApplicationController
# These will kick you out if you don't have access. # These will kick you out if you don't have access.
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && embeddable? }
before_action :authorize_upload_file!, only: [:create, :authorize] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]
......
...@@ -179,6 +179,10 @@ class Blob < SimpleDelegator ...@@ -179,6 +179,10 @@ class Blob < SimpleDelegator
UploaderHelper::SAFE_VIDEO_EXT.include?(extension) UploaderHelper::SAFE_VIDEO_EXT.include?(extension)
end end
def audio?
UploaderHelper::SAFE_AUDIO_EXT.include?(extension)
end
def readable_text? def readable_text?
text_in_repo? && !stored_externally? && !truncated? text_in_repo? && !stored_externally? && !truncated?
end end
......
...@@ -415,7 +415,7 @@ class Commit ...@@ -415,7 +415,7 @@ class Commit
if entry[:type] == :blob if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob blob.image? || blob.video? || blob.audio? ? :raw : :blob
else else
entry[:type] entry[:type]
end end
......
---
title: Feature enabling embedded audio elements in markdown.
merge_request: 17860
author: Jesse Hall @jessehall3
type: added
...@@ -11,12 +11,12 @@ to log the IP address of the user. ...@@ -11,12 +11,12 @@ to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you One way to mitigate this is by proxying any external images to a server you
control. control.
GitLab can be configured to use an asset proxy server when requesting external images/videos in GitLab can be configured to use an asset proxy server when requesting external images/videos/audio in
issues, comments, etc. This helps ensure that malicious images do not expose the user's IP address issues, comments, etc. This helps ensure that malicious images do not expose the user's IP address
when they are fetched. when they are fetched.
We currently recommend using [cactus/go-camo](https://github.com/cactus/go-camo#how-it-works) We currently recommend using [cactus/go-camo](https://github.com/cactus/go-camo#how-it-works)
as it supports proxying video and is more configurable. as it supports proxying video, audio, and is more configurable.
## Installing Camo server ## Installing Camo server
...@@ -52,7 +52,7 @@ To install a Camo server as an asset proxy: ...@@ -52,7 +52,7 @@ To install a Camo server as an asset proxy:
## Using the Camo server ## Using the Camo server
Once the Camo server is running and you've enabled the GitLab settings, any image or video that Once the Camo server is running and you've enabled the GitLab settings, any image, video, or audio that
references an external source will get proxied to the Camo server. references an external source will get proxied to the Camo server.
For example, the following is a link to an image in Markdown: For example, the following is a link to an image in Markdown:
......
...@@ -108,7 +108,7 @@ changing how standard markdown is used: ...@@ -108,7 +108,7 @@ changing how standard markdown is used:
| [code blocks](#code-spans-and-blocks) | [colored code and syntax highlighting](#colored-code-and-syntax-highlighting) | | [code blocks](#code-spans-and-blocks) | [colored code and syntax highlighting](#colored-code-and-syntax-highlighting) |
| [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis) | [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis)
| [headers](#headers) | [linkable Header IDs](#header-ids-and-links) | | [headers](#headers) | [linkable Header IDs](#header-ids-and-links) |
| [images](#images) | [embedded videos](#videos) | | [images](#images) | [embedded videos](#videos) and [audio](#audio) |
| [linebreaks](#line-breaks) | [more linebreak control](#newlines) | | [linebreaks](#line-breaks) | [more linebreak control](#newlines) |
| [links](#links) | [automatically linking URLs](#url-auto-linking) | | [links](#links) | [automatically linking URLs](#url-auto-linking) |
...@@ -899,6 +899,23 @@ Here's a sample video: ...@@ -899,6 +899,23 @@ Here's a sample video:
![Sample Video](img/markdown_video.mp4) ![Sample Video](img/markdown_video.mp4)
#### Audio
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#audio).
Similar to videos, link tags for files with an audio extension are automatically converted to
an audio player. The valid audio extensions are `.mp3`, `.ogg`, and `.wav`:
```md
Here's a sample audio clip:
![Sample Audio](img/markdown_audio.mp3)
```
Here's a sample audio clip:
![Sample Audio](img/markdown_audio.mp3)
### Inline HTML ### Inline HTML
> To see the markdown rendered within HTML in the second example, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#inline-html). > To see the markdown rendered within HTML in the second example, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#inline-html).
......
# frozen_string_literal: true
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/audio.js
module Banzai
module Filter
# Find every image that isn't already wrapped in an `a` tag, and that has
# a `src` attribute ending with an audio extension, add a new audio node and
# a "Download" link in the case the audio cannot be played.
class AudioLinkFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |el|
el.replace(audio_node(doc, el)) if has_audio_extension?(el)
end
doc
end
private
def has_audio_extension?(element)
src = element.attr('data-canonical-src').presence || element.attr('src')
return unless src.present?
src_ext = File.extname(src).sub('.', '').downcase
Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.include?(src_ext)
end
def audio_node(doc, element)
container = doc.document.create_element(
'div',
class: 'audio-container'
)
audio = doc.document.create_element(
'audio',
src: element['src'],
controls: true,
'data-setup' => '{}',
'data-title' => element['title'] || element['alt'])
link = doc.document.create_element(
'a',
element['title'] || element['alt'],
href: element['src'],
target: '_blank',
rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'")
# make sure the original non-proxied src carries over
if element['data-canonical-src']
audio['data-canonical-src'] = element['data-canonical-src']
link['data-canonical-src'] = element['data-canonical-src']
end
download_paragraph = doc.document.create_element('p')
download_paragraph.children = link
container.add_child(audio)
container.add_child(download_paragraph)
container
end
end
end
end
...@@ -65,7 +65,7 @@ module Banzai ...@@ -65,7 +65,7 @@ module Banzai
el.attribute('href') el.attribute('href')
end end
attrs += doc.search('img, video').flat_map do |el| attrs += doc.search('img, video, audio').flat_map do |el|
[el.attribute('src'), el.attribute('data-src')] [el.attribute('src'), el.attribute('data-src')]
end end
...@@ -83,7 +83,7 @@ module Banzai ...@@ -83,7 +83,7 @@ module Banzai
get_blob_types(paths).each do |name, type| get_blob_types(paths).each do |name, type|
if type == :blob if type == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: name), project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: name), project)
uri_types[name] = blob.image? || blob.video? ? :raw : :blob uri_types[name] = blob.image? || blob.video? || blob.audio? ? :raw : :blob
else else
uri_types[name] = type uri_types[name] = type
end end
......
...@@ -15,7 +15,7 @@ module Banzai ...@@ -15,7 +15,7 @@ module Banzai
doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) } doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) }
doc.search('video').each { |el| process_link(el.attribute('src'), el) } doc.search('video, audio').each { |el| process_link(el.attribute('src'), el) }
doc.search('img').each do |el| doc.search('img').each do |el|
attr = el.attribute('data-src') || el.attribute('src') attr = el.attribute('data-src') || el.attribute('src')
......
...@@ -26,6 +26,7 @@ module Banzai ...@@ -26,6 +26,7 @@ module Banzai
Filter::ColorFilter, Filter::ColorFilter,
Filter::MermaidFilter, Filter::MermaidFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::AudioLinkFilter,
Filter::ImageLazyLoadFilter, Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter, Filter::ImageLinkFilter,
Filter::InlineMetricsFilter, Filter::InlineMetricsFilter,
......
...@@ -10,14 +10,14 @@ module Gitlab ...@@ -10,14 +10,14 @@ module Gitlab
return unless name = markdown_name return unless name = markdown_name
markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})" markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
markdown = "!#{markdown}" if image_or_video? || dangerous_image_or_video? markdown = "!#{markdown}" if embeddable? || dangerous_embeddable?
markdown markdown
end end
def markdown_name def markdown_name
return unless filename.present? return unless filename.present?
image_or_video? ? File.basename(filename, File.extname(filename)) : filename embeddable? ? File.basename(filename, File.extname(filename)) : filename
end end
end end
end end
...@@ -26,11 +26,13 @@ module Gitlab ...@@ -26,11 +26,13 @@ module Gitlab
# on IE >= 9. # on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
SAFE_VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze SAFE_VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
SAFE_AUDIO_EXT = %w[mp3 oga ogg spx wav].freeze
# These extension types can contain dangerous code and should only be embedded inline with # These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
DANGEROUS_IMAGE_EXT = %w[svg].freeze DANGEROUS_IMAGE_EXT = %w[svg].freeze
DANGEROUS_VIDEO_EXT = [].freeze # None, yet DANGEROUS_VIDEO_EXT = [].freeze # None, yet
DANGEROUS_AUDIO_EXT = [].freeze # None, yet
def image? def image?
extension_match?(SAFE_IMAGE_EXT) extension_match?(SAFE_IMAGE_EXT)
...@@ -40,8 +42,12 @@ module Gitlab ...@@ -40,8 +42,12 @@ module Gitlab
extension_match?(SAFE_VIDEO_EXT) extension_match?(SAFE_VIDEO_EXT)
end end
def image_or_video? def audio?
image? || video? extension_match?(SAFE_AUDIO_EXT)
end
def embeddable?
image? || video? || audio?
end end
def dangerous_image? def dangerous_image?
...@@ -52,8 +58,12 @@ module Gitlab ...@@ -52,8 +58,12 @@ module Gitlab
extension_match?(DANGEROUS_VIDEO_EXT) extension_match?(DANGEROUS_VIDEO_EXT)
end end
def dangerous_image_or_video? def dangerous_audio?
dangerous_image? || dangerous_video? extension_match?(DANGEROUS_AUDIO_EXT)
end
def dangerous_embeddable?
dangerous_image? || dangerous_video? || dangerous_audio?
end end
private private
......
...@@ -178,6 +178,12 @@ describe 'Copy as GFM', :js do ...@@ -178,6 +178,12 @@ describe 'Copy as GFM', :js do
'![Video](https://example.com/video.mp4)' '![Video](https://example.com/video.mp4)'
) )
verify(
'AudioLinkFilter',
'![Audio](https://example.com/audio.wav)'
)
verify( verify(
'MathFilter: math as converted from GFM to HTML', 'MathFilter: math as converted from GFM to HTML',
......
...@@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do ...@@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do
expect(doc).to parse_video_links expect(doc).to parse_video_links
end end
aggregate_failures 'AudioLinkFilter' do
expect(doc).to parse_audio_links
end
aggregate_failures 'ColorFilter' do aggregate_failures 'ColorFilter' do
expect(doc).to parse_colors expect(doc).to parse_colors
end end
......
...@@ -101,7 +101,7 @@ describe 'Branches' do ...@@ -101,7 +101,7 @@ describe 'Branches' do
visit project_branches_filtered_path(project, state: 'all') visit project_branches_filtered_path(project, state: 'all')
expect(all('.all-branches').last).to have_selector('li', count: 20) expect(all('.all-branches').last).to have_selector('li', count: 20)
accept_confirm { find('.js-branch-add-pdf-text-binary .btn-remove').click } accept_confirm { find('.js-branch-invalid-utf8-diff-paths .btn-remove').click }
expect(all('.all-branches').last).to have_selector('li', count: 19) expect(all('.all-branches').last).to have_selector('li', count: 19)
end end
......
...@@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such: ...@@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4) ![My Video](/assets/videos/gitlab-demo.mp4)
### Audio
![My Audio Clip](/assets/audio/gitlab-demo.wav)
### Colors ### Colors
`#F00` `#F00`
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::AudioLinkFilter do
def filter(doc, contexts = {})
contexts.reverse_merge!({
project: project
})
described_class.call(doc, contexts)
end
def link_to_image(path)
return '<img/>' if path.nil?
%(<img src="#{path}"/>)
end
let(:project) { create(:project, :repository) }
shared_examples 'an audio element' do
let(:image) { link_to_image(src) }
it 'replaces the image tag with an audio tag' do
container = filter(image).children.first
expect(container.name).to eq 'div'
expect(container['class']).to eq 'audio-container'
audio, paragraph = container.children
expect(audio.name).to eq 'audio'
expect(audio['src']).to eq src
expect(paragraph.name).to eq 'p'
link = paragraph.children.first
expect(link.name).to eq 'a'
expect(link['href']).to eq src
expect(link['target']).to eq '_blank'
end
end
shared_examples 'an unchanged element' do |ext|
it 'leaves the document unchanged' do
element = filter(link_to_image(src)).children.first
expect(element.name).to eq 'img'
expect(element['src']).to eq src
end
end
context 'when the element src has an audio extension' do
Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.each do |ext|
it_behaves_like 'an audio element' do
let(:src) { "/path/audio.#{ext}" }
end
it_behaves_like 'an audio element' do
let(:src) { "/path/audio.#{ext.upcase}" }
end
end
end
context 'when the element has no src attribute' do
let(:src) { nil }
it_behaves_like 'an unchanged element'
end
context 'when the element src is an image' do
let(:src) { '/path/my_image.jpg' }
it_behaves_like 'an unchanged element'
end
context 'when the element src has an invalid file extension' do
let(:src) { '/path/my_audio.somewav' }
it_behaves_like 'an unchanged element'
end
context 'when data-canonical-src is empty' do
let(:image) { %(<img src="#{src}" data-canonical-src=""/>) }
context 'and src is audio' do
let(:src) { '/path/audio.wav' }
it_behaves_like 'an audio element'
end
context 'and src is an image' do
let(:src) { '/path/my_image.jpg' }
it_behaves_like 'an unchanged element'
end
end
context 'when data-canonical-src is set' do
it 'uses the correct src' do
proxy_src = 'https://assets.example.com/6d8b63'
canonical_src = 'http://example.com/test.wav'
image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
container = filter(image).children.first
expect(container['class']).to eq 'audio-container'
audio, paragraph = container.children
expect(audio['src']).to eq proxy_src
expect(audio['data-canonical-src']).to eq canonical_src
link = paragraph.children.first
expect(link['href']).to eq proxy_src
end
end
end
...@@ -29,6 +29,10 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -29,6 +29,10 @@ describe Banzai::Filter::RelativeLinkFilter do
%(<video src="#{path}"></video>) %(<video src="#{path}"></video>)
end end
def audio(path)
%(<audio src="#{path}"></audio>)
end
def link(path) def link(path)
%(<a href="#{path}">#{path}</a>) %(<a href="#{path}">#{path}</a>)
end end
...@@ -82,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -82,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4' expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
end end
it 'does not modify any relative URL in audio' do
doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
expect(doc.at_css('audio')['src']).to eq 'files/audio/sample.wav'
end
end end
context 'with a project_wiki' do context 'with a project_wiki' do
...@@ -218,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -218,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do
.to eq "/#{project_path}/raw/video/files/videos/intro.mp4" .to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
end end
it 'rebuilds relative URL for audio in the repo' do
doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
expect(doc.at_css('audio')['src'])
.to eq "/#{project_path}/raw/audio/files/audio/sample.wav"
end
it 'does not modify relative URL with an anchor only' do it 'does not modify relative URL with an anchor only' do
doc = filter(link('#section-1')) doc = filter(link('#section-1'))
expect(doc.at_css('a')['href']).to eq '#section-1' expect(doc.at_css('a')['href']).to eq '#section-1'
......
...@@ -60,6 +60,14 @@ describe Banzai::Filter::WikiLinkFilter do ...@@ -60,6 +60,14 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4") expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4")
end end
end end
context 'with "audio" html tag' do
it 'rewrites links' do
filtered_link = filter("<audio src='#{repository_upload_folder}/a.wav'></audio>", project_wiki: wiki).children[0]
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.wav")
end
end
end end
describe "invalid links" do describe "invalid links" do
......
...@@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do
end end
end end
describe 'videos' do describe 'videos and audio' do
let(:namespace) { create(:namespace, name: "wiki_link_ns") } let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) } let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) }
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
it 'generates video html structure' do it 'generates video html structure' do
markdown = "![video_file](video_file_name.mp4)" markdown = "![video_file](video_file_name.mp4)"
...@@ -279,5 +279,19 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -279,5 +279,19 @@ describe Banzai::Pipeline::WikiPipeline do
expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"') expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
end end
it 'generates audio html structure' do
markdown = "![audio_file](audio_file_name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio_file_name.wav"')
end
it 'rewrites and replaces audio links names with white spaces to %20' do
markdown = "![audio file](audio file name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio%20file%20name.wav"')
end
end end
end end
...@@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do ...@@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end end
end end
context 'when file is an image or video' do context 'when file is an image' do
let(:filename) { 'dk.png' } let(:filename) { 'my_image.png' }
it 'returns preview markdown link' do it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)' expect(custom_class.markdown_link).to eq '![my_image](/uploads/my_image.png)'
end end
end end
context 'when file is not an image or video' do context 'when file is video' do
let(:filename) { 'dk.zip' } let(:filename) { 'my_video.mp4' }
it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![my_video](/uploads/my_video.mp4)'
end
end
context 'when file is audio' do
let(:filename) { 'my_audio.wav' }
it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![my_audio](/uploads/my_audio.wav)'
end
end
context 'when file is not embeddable' do
let(:filename) { 'my_zip.zip' }
it 'returns markdown link' do it 'returns markdown link' do
expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)' expect(custom_class.markdown_link).to eq '[my_zip.zip](/uploads/my_zip.zip)'
end end
end end
...@@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do ...@@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end end
describe 'mardown_name' do describe 'mardown_name' do
context 'when file is an image or video' do context 'when file is an image' do
let(:filename) { 'dk.png' } let(:filename) { 'my_image.png' }
it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'my_image'
end
end
context 'when file is video' do
let(:filename) { 'my_video.mp4' }
it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'my_video'
end
end
context 'when file is audio' do
let(:filename) { 'my_audio.wav' }
it 'retrieves the name without the extension' do it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'dk' expect(custom_class.markdown_name).to eq 'my_audio'
end end
end end
context 'when file is not an image or video' do context 'when file is not embeddable' do
let(:filename) { 'dk.zip' } let(:filename) { 'my_zip.zip' }
it 'retrieves the name with the extesion' do it 'retrieves the name with the extesion' do
expect(custom_class.markdown_name).to eq 'dk.zip' expect(custom_class.markdown_name).to eq 'my_zip.zip'
end end
end end
......
...@@ -3,7 +3,21 @@ require 'spec_helper' ...@@ -3,7 +3,21 @@ require 'spec_helper'
describe Gitlab::FileTypeDetection do describe Gitlab::FileTypeDetection do
context 'when class is an uploader' do context 'when class is an uploader' do
shared_examples '#image? for an uploader' do let(:uploader) do
example_uploader = Class.new(CarrierWave::Uploader::Base) do
include Gitlab::FileTypeDetection
storage :file
end
example_uploader.new
end
def upload_fixture(filename)
fixture_file_upload(File.join('spec', 'fixtures', filename))
end
describe '#image?' do
it 'returns true for an image file' do it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png')) uploader.store!(upload_fixture('dk.png'))
...@@ -23,6 +37,12 @@ describe Gitlab::FileTypeDetection do ...@@ -23,6 +37,12 @@ describe Gitlab::FileTypeDetection do
expect(uploader).not_to be_image expect(uploader).not_to be_image
end end
it 'returns false for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).not_to be_image
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png')) uploader.store!(upload_fixture('dk.png'))
...@@ -32,7 +52,7 @@ describe Gitlab::FileTypeDetection do ...@@ -32,7 +52,7 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#video? for an uploader' do describe '#video?' do
it 'returns true for a video file' do it 'returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4')) uploader.store!(upload_fixture('video_sample.mp4'))
...@@ -45,8 +65,21 @@ describe Gitlab::FileTypeDetection do ...@@ -45,8 +65,21 @@ describe Gitlab::FileTypeDetection do
expect(uploader).not_to be_video expect(uploader).not_to be_video
end end
it 'returns false for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).not_to be_video
end
it 'returns false if file has a dangerous image extension' do
uploader.store!(upload_fixture('unsanitized.svg'))
expect(uploader).to be_dangerous_image
expect(uploader).not_to be_video
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png')) uploader.store!(upload_fixture('video_sample.mp4'))
allow(uploader).to receive(:filename).and_return(nil) allow(uploader).to receive(:filename).and_return(nil)
...@@ -54,7 +87,83 @@ describe Gitlab::FileTypeDetection do ...@@ -54,7 +87,83 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#dangerous_image? for an uploader' do describe '#audio?' do
it 'returns true for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).to be_audio
end
it 'returns false for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).not_to be_audio
end
it 'returns false for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).not_to be_audio
end
it 'returns false if file has a dangerous image extension' do
uploader.store!(upload_fixture('unsanitized.svg'))
expect(uploader).to be_dangerous_image
expect(uploader).not_to be_audio
end
it 'returns false if filename is blank' do
uploader.store!(upload_fixture('audio_sample.wav'))
allow(uploader).to receive(:filename).and_return(nil)
expect(uploader).not_to be_audio
end
end
describe '#embeddable?' do
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).to be_embeddable
end
it 'returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).to be_embeddable
end
it 'returns true for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).to be_embeddable
end
it 'returns false if not an embeddable file' do
uploader.store!(upload_fixture('doc_sample.txt'))
expect(uploader).not_to be_embeddable
end
it 'returns false if filename has a dangerous image extension' do
uploader.store!(upload_fixture('unsanitized.svg'))
expect(uploader).to be_dangerous_image
expect(uploader).not_to be_embeddable
end
it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png'))
allow(uploader).to receive(:filename).and_return(nil)
expect(uploader).not_to be_embeddable
end
end
describe '#dangerous_image?' do
it 'returns true if filename has a dangerous extension' do it 'returns true if filename has a dangerous extension' do
uploader.store!(upload_fixture('unsanitized.svg')) uploader.store!(upload_fixture('unsanitized.svg'))
...@@ -73,6 +182,12 @@ describe Gitlab::FileTypeDetection do ...@@ -73,6 +182,12 @@ describe Gitlab::FileTypeDetection do
expect(uploader).not_to be_dangerous_image expect(uploader).not_to be_dangerous_image
end end
it 'returns false for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).not_to be_dangerous_image
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png')) uploader.store!(upload_fixture('dk.png'))
...@@ -82,7 +197,7 @@ describe Gitlab::FileTypeDetection do ...@@ -82,7 +197,7 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#dangerous_video? for an uploader' do describe '#dangerous_video?' do
it 'returns false for a safe video file' do it 'returns false for a safe video file' do
uploader.store!(upload_fixture('video_sample.mp4')) uploader.store!(upload_fixture('video_sample.mp4'))
...@@ -101,6 +216,12 @@ describe Gitlab::FileTypeDetection do ...@@ -101,6 +216,12 @@ describe Gitlab::FileTypeDetection do
expect(uploader).not_to be_dangerous_video expect(uploader).not_to be_dangerous_video
end end
it 'returns false for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).not_to be_dangerous_video
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png')) uploader.store!(upload_fixture('dk.png'))
...@@ -110,49 +231,91 @@ describe Gitlab::FileTypeDetection do ...@@ -110,49 +231,91 @@ describe Gitlab::FileTypeDetection do
end end
end end
let(:uploader) do describe '#dangerous_audio?' do
example_uploader = Class.new(CarrierWave::Uploader::Base) do it 'returns false for a safe audio file' do
include Gitlab::FileTypeDetection uploader.store!(upload_fixture('audio_sample.wav'))
storage :file expect(uploader).not_to be_dangerous_audio
end end
example_uploader.new it 'returns false if filename is a dangerous image extension' do
end uploader.store!(upload_fixture('unsanitized.svg'))
def upload_fixture(filename) expect(uploader).not_to be_dangerous_audio
fixture_file_upload(File.join('spec', 'fixtures', filename)) end
end
describe '#image?' do it 'returns false for an image file' do
include_examples '#image? for an uploader' uploader.store!(upload_fixture('dk.png'))
end
describe '#video?' do expect(uploader).not_to be_dangerous_audio
include_examples '#video? for an uploader' end
end
describe '#image_or_video?' do it 'returns false for an video file' do
include_examples '#image? for an uploader' uploader.store!(upload_fixture('video_sample.mp4'))
include_examples '#video? for an uploader'
end
describe '#dangerous_image?' do expect(uploader).not_to be_dangerous_audio
include_examples '#dangerous_image? for an uploader' end
end
describe '#dangerous_video?' do it 'returns false if filename is blank' do
include_examples '#dangerous_video? for an uploader' uploader.store!(upload_fixture('dk.png'))
allow(uploader).to receive(:filename).and_return(nil)
expect(uploader).not_to be_dangerous_audio
end
end end
describe '#dangerous_image_or_video?' do describe '#dangerous_embeddable?' do
include_examples '#dangerous_image? for an uploader' it 'returns true if filename has a dangerous image extension' do
include_examples '#dangerous_video? for an uploader' uploader.store!(upload_fixture('unsanitized.svg'))
expect(uploader).to be_dangerous_embeddable
end
it 'returns false for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).not_to be_dangerous_embeddable
end
it 'returns false for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).not_to be_dangerous_embeddable
end
it 'returns false for an audio file' do
uploader.store!(upload_fixture('audio_sample.wav'))
expect(uploader).not_to be_dangerous_embeddable
end
it 'returns false for a non-embeddable file' do
uploader.store!(upload_fixture('doc_sample.txt'))
expect(uploader).not_to be_dangerous_embeddable
end
it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png'))
allow(uploader).to receive(:filename).and_return(nil)
expect(uploader).not_to be_dangerous_embeddable
end
end end
end end
context 'when class is a regular class' do context 'when class is a regular class' do
shared_examples '#image? for a regular class' do let(:custom_class) do
custom_class = Class.new do
include Gitlab::FileTypeDetection
end
custom_class.new
end
describe '#image?' do
it 'returns true for an image file' do it 'returns true for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png') allow(custom_class).to receive(:filename).and_return('dk.png')
...@@ -166,12 +329,18 @@ describe Gitlab::FileTypeDetection do ...@@ -166,12 +329,18 @@ describe Gitlab::FileTypeDetection do
expect(custom_class).not_to be_image expect(custom_class).not_to be_image
end end
it 'returns false for any non image file' do it 'returns false for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4') allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).not_to be_image expect(custom_class).not_to be_image
end end
it 'returns false for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_image
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil) allow(custom_class).to receive(:filename).and_return(nil)
...@@ -179,19 +348,25 @@ describe Gitlab::FileTypeDetection do ...@@ -179,19 +348,25 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#video? for a regular class' do describe '#video?' do
it 'returns true for a video file' do it 'returns true for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4') allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).to be_video expect(custom_class).to be_video
end end
it 'returns false for any non-video file' do it 'returns false for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png') allow(custom_class).to receive(:filename).and_return('dk.png')
expect(custom_class).not_to be_video expect(custom_class).not_to be_video
end end
it 'returns false for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_video
end
it 'returns false if file has a dangerous image extension' do it 'returns false if file has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('unsanitized.svg') allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
...@@ -206,7 +381,79 @@ describe Gitlab::FileTypeDetection do ...@@ -206,7 +381,79 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#dangerous_image? for a regular class' do describe '#audio?' do
it 'returns true for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).to be_audio
end
it 'returns false for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png')
expect(custom_class).not_to be_audio
end
it 'returns false for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).not_to be_audio
end
it 'returns false if file has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
expect(custom_class).to be_dangerous_image
expect(custom_class).not_to be_audio
end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
expect(custom_class).not_to be_audio
end
end
describe '#embeddable?' do
it 'returns true for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png')
expect(custom_class).to be_embeddable
end
it 'returns true for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).to be_embeddable
end
it 'returns true for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).to be_embeddable
end
it 'returns false if not an embeddable file' do
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
expect(custom_class).not_to be_embeddable
end
it 'returns false if filename has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
expect(custom_class).to be_dangerous_image
expect(custom_class).not_to be_embeddable
end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
expect(custom_class).not_to be_embeddable
end
end
describe '#dangerous_image?' do
it 'returns true if file has a dangerous image extension' do it 'returns true if file has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('unsanitized.svg') allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
...@@ -219,12 +466,18 @@ describe Gitlab::FileTypeDetection do ...@@ -219,12 +466,18 @@ describe Gitlab::FileTypeDetection do
expect(custom_class).not_to be_dangerous_image expect(custom_class).not_to be_dangerous_image
end end
it 'returns false for any non image file' do it 'returns false for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4') allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).not_to be_dangerous_image expect(custom_class).not_to be_dangerous_image
end end
it 'returns false for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_dangerous_image
end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil) allow(custom_class).to receive(:filename).and_return(nil)
...@@ -232,7 +485,7 @@ describe Gitlab::FileTypeDetection do ...@@ -232,7 +485,7 @@ describe Gitlab::FileTypeDetection do
end end
end end
shared_examples '#dangerous_video? for a regular class' do describe '#dangerous_video?' do
it 'returns false for a safe video file' do it 'returns false for a safe video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4') allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
...@@ -245,6 +498,12 @@ describe Gitlab::FileTypeDetection do ...@@ -245,6 +498,12 @@ describe Gitlab::FileTypeDetection do
expect(custom_class).not_to be_dangerous_video expect(custom_class).not_to be_dangerous_video
end end
it 'returns false for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_dangerous_video
end
it 'returns false if file has a dangerous image extension' do it 'returns false if file has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('unsanitized.svg') allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
...@@ -258,38 +517,74 @@ describe Gitlab::FileTypeDetection do ...@@ -258,38 +517,74 @@ describe Gitlab::FileTypeDetection do
end end
end end
let(:custom_class) do describe '#dangerous_audio?' do
custom_class = Class.new do it 'returns false for a safe audio file' do
include Gitlab::FileTypeDetection allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_dangerous_audio
end end
custom_class.new it 'returns false for an image file' do
end allow(custom_class).to receive(:filename).and_return('dk.png')
describe '#image?' do expect(custom_class).not_to be_dangerous_audio
include_examples '#image? for a regular class' end
end
describe '#video?' do it 'returns false for a video file' do
include_examples '#video? for a regular class' allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
end
describe '#image_or_video?' do expect(custom_class).not_to be_dangerous_audio
include_examples '#image? for a regular class' end
include_examples '#video? for a regular class'
end
describe '#dangerous_image?' do it 'returns false if file has a dangerous image extension' do
include_examples '#dangerous_image? for a regular class' allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
end
describe '#dangerous_video?' do expect(custom_class).not_to be_dangerous_audio
include_examples '#dangerous_video? for a regular class' end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
expect(custom_class).not_to be_dangerous_audio
end
end end
describe '#dangerous_image_or_video?' do describe '#dangerous_embeddable?' do
include_examples '#dangerous_image? for a regular class' it 'returns true if file has a dangerous image extension' do
include_examples '#dangerous_video? for a regular class' allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
expect(custom_class).to be_dangerous_embeddable
end
it 'returns false for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png')
expect(custom_class).not_to be_dangerous_embeddable
end
it 'returns false for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).not_to be_dangerous_embeddable
end
it 'returns false for an audio file' do
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
expect(custom_class).not_to be_dangerous_embeddable
end
it 'returns false for a non-embeddable file' do
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
expect(custom_class).not_to be_dangerous_embeddable
end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
expect(custom_class).not_to be_dangerous_embeddable
end
end end
end end
end end
...@@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do ...@@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do
doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"), doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"),
attr: "src", attr: "src",
node_to_check: -> (doc) { doc.children.first.children.filter("source").first } node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
},
audio: {
doc: HTML::Pipeline.parse("<audio><source src='#{scheme}alert(1);'></audio>"),
attr: "src",
node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
} }
} }
......
...@@ -503,6 +503,8 @@ eos ...@@ -503,6 +503,8 @@ eos
expect(commit.uri_type('files/html')).to be(:tree) expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
expect(commit.uri_type('files/images/wm.svg')).to be(:raw) expect(commit.uri_type('files/images/wm.svg')).to be(:raw)
expect(project.commit('audio').uri_type('files/audio/clip.mp3')).to be(:raw)
expect(project.commit('audio').uri_type('files/audio/sample.wav')).to be(:raw)
expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
expect(commit.uri_type('files/js/application.js')).to be(:blob) expect(commit.uri_type('files/js/application.js')).to be(:blob)
end end
......
...@@ -36,6 +36,7 @@ module TestEnv ...@@ -36,6 +36,7 @@ module TestEnv
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'pages-deploy' => '7897d5b', 'pages-deploy' => '7897d5b',
'pages-deploy-target' => '7975be0', 'pages-deploy-target' => '7975be0',
'audio' => 'c3c21fd',
'video' => '8879059', 'video' => '8879059',
'add-balsamiq-file' => 'b89b56d', 'add-balsamiq-file' => 'b89b56d',
'crlf-diff' => '5938907', 'crlf-diff' => '5938907',
......
...@@ -193,6 +193,17 @@ module MarkdownMatchers ...@@ -193,6 +193,17 @@ module MarkdownMatchers
end end
end end
# AudioLinkFilter
matcher :parse_audio_links do
set_default_markdown_messages
match do |actual|
audio = actual.at_css('audio')
expect(audio['src']).to end_with('/assets/audio/gitlab-demo.wav')
end
end
# ColorFilter # ColorFilter
matcher :parse_colors do matcher :parse_colors do
set_default_markdown_messages set_default_markdown_messages
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment